如何讓兩臺(tái)處在不同內(nèi)網(wǎng)的主機(jī)直接互連,?你需要內(nèi)網(wǎng)穿透,!
上圖是一個(gè)非完整版內(nèi)外網(wǎng)通訊圖由內(nèi)網(wǎng)端先發(fā)起,內(nèi)網(wǎng)設(shè)備192.168.1.2:6677發(fā)送數(shù)據(jù)到外網(wǎng)時(shí)候必須經(jīng)過nat會(huì)轉(zhuǎn)換成對(duì)應(yīng)的外網(wǎng)ip+端口,,然后在發(fā)送給外網(wǎng)設(shè)備,外網(wǎng)設(shè)備回復(fù)數(shù)據(jù)也是發(fā)給你的外網(wǎng)ip+端口,。這只是單向的內(nèi)去外,那反過來,,如果外網(wǎng)的設(shè)備需要主動(dòng)訪問我局域網(wǎng)里的某一個(gè)設(shè)備是無法訪問的,因?yàn)檫@個(gè)時(shí)候還沒做nat轉(zhuǎn)換所以外網(wǎng)不知道你內(nèi)網(wǎng)設(shè)備的應(yīng)用具體對(duì)應(yīng)的是哪個(gè)端口,,這個(gè)時(shí)候我們就需要內(nèi)網(wǎng)穿透了,內(nèi)網(wǎng)穿透也叫NAT穿透,;
穿透原理
如上圖所示經(jīng)NAT轉(zhuǎn)換后的內(nèi)外網(wǎng)地址+端口,會(huì)緩存一段時(shí)間,,在這段時(shí)間內(nèi)192.168.1.2:6677和112.48.69.2020:8899的映射關(guān)系會(huì)一直存在,,這樣你的內(nèi)網(wǎng)主機(jī)就得到一個(gè)外網(wǎng)地址,,這個(gè)對(duì)應(yīng)關(guān)系又根據(jù)NAT轉(zhuǎn)換方法類型的不同,得用對(duì)應(yīng)的方式實(shí)現(xiàn)打洞,,NAT轉(zhuǎn)換方法類型有下列幾種(來源百度百科NAT):
(1)Full cone NAT:即著名的一對(duì)一(one-to-one)NAT。
一旦一個(gè)內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),,所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送。任意外部主機(jī)都能通過給eAddr:port2發(fā)包到iAddr:port1(純天然不用打洞?。?/span>
(2)Address-Restricted cone NAT :限制地址,,即只接收曾經(jīng)發(fā)送到對(duì)端的IP地址來的數(shù)據(jù)包。
一旦一個(gè)內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),,所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送。任意外部主機(jī)(hostAddr:any)都能通過給eAddr:port2發(fā)包到達(dá)iAddr:port1的前提是:iAddr:port1之前發(fā)送過包到hostAddr:any. "any"也就是說端口不受限制(只需知道某個(gè)轉(zhuǎn)換后的外網(wǎng)ip+端口即可,。)
(3)Port-Restricted cone NAT:類似受限制錐形NAT(Restricted cone NAT),,但是還有端口限制。
一旦一個(gè)內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),,所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送,。一個(gè)外部主機(jī)(hostAddr:port3)能夠發(fā)包到達(dá)iAddr:port1的前提是:iAddr:port1之前發(fā)送過包到hostAddr:port3.(雙方需要各自知道對(duì)方轉(zhuǎn)換后的外網(wǎng)ip+端口,,然后一方先發(fā)一次嘗試連接,另一方在次連接過來的時(shí)候就能直接連通了,。)
(4)Symmetric NAT(對(duì)稱NAT)
每一個(gè)來自相同內(nèi)部IP與port的請(qǐng)求到一個(gè)特定目的地的IP地址和端口,映射到一個(gè)獨(dú)特的外部來源的IP地址和端口,。
同一個(gè)內(nèi)部主機(jī)發(fā)出一個(gè)信息包到不同的目的端,不同的映射使用外部主機(jī)收到了一封包從一個(gè)內(nèi)部主機(jī)可以送一封包回來(只能和Full cone NAT連,沒法打洞,手機(jī)流量開熱點(diǎn)就是,,同一個(gè)本地端口連接不同的服務(wù)器得到的外網(wǎng)第地址和IP不同!)
例子:
下面用一個(gè)例子演示下“受限制錐形NAT”的打洞,,實(shí)現(xiàn)了這個(gè)它前面兩個(gè)類型也能通用,。對(duì)稱型的話不考慮,打不了洞,。我們知道要實(shí)現(xiàn)兩臺(tái)“受限制錐形NAT”互連重點(diǎn)就是要知道對(duì)
方轉(zhuǎn)換后的外網(wǎng)IP+端口,這樣我們可以:
1. 準(zhǔn)備一臺(tái)Full cone NAT 類型的外網(wǎng)服務(wù)端,,接受來自兩個(gè)客戶端的連接,,并對(duì)應(yīng)告知對(duì)方ip+端口,;
2.知道了對(duì)方ip+端口 需要設(shè)置socke:Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);這樣才能端口復(fù)用,;目的就是讓連接對(duì)外的端口一致,;
3.最后,我們可以讓兩臺(tái)客戶端互相連接,,或者一臺(tái)先發(fā)一個(gè)請(qǐng)求,打個(gè)洞,;另一個(gè)在去連接;
代碼:
1.TCP+IOCP方式,,相對(duì) “面向?qū)ο蟆钡貙?shí)現(xiàn)穿透!
服務(wù)端 ServerListener類,,用SocketAsyncEventArgs:
1 /// <summary> 2 /// 打洞服務(wù)端,非常的簡單,,接收兩個(gè)連接并且轉(zhuǎn)發(fā)給對(duì)方; 3 /// </summary> 4 public class ServerListener : IServerListener 5 { 6 IPEndPoint EndPoint { get; set; } 7 //消息委托 8 public delegate void EventMsg(object sender, string e); 9 public static object obj = new object(); 10 //通知消息 11 public event EventMsg NoticeMsg; 12 //接收事件 13 public event EventMsg ReceivedMsg; 14 /// <summary> 15 /// 上次鏈接的 16 /// </summary> 17 private Socket Previous; 18 public ServerListener(IPEndPoint endpoint) 19 { 20 this.EndPoint = endpoint; 21 } 22 private Socket listener; 23 public void Start() 24 { 25 this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 26 var connectArgs = new SocketAsyncEventArgs(); 27 listener.Bind(EndPoint); 28 listener.Listen(2); 29 EndPoint = (IPEndPoint)listener.LocalEndPoint; 30 connectArgs.Completed += OnAccept; 31 //是否同步就完成了,,同步完成需要自己觸發(fā) 32 if (!listener.AcceptAsync(connectArgs)) 33 OnAccept(listener, connectArgs); 34 } 35 byte[] bytes = new byte[400]; 36 private void OnAccept(object sender, SocketAsyncEventArgs e) 37 { 38 Socket socket = null; 39 try 40 { 41 var remoteEndPoint1 = e.AcceptSocket.RemoteEndPoint.ToString(); 42 NoticeMsg?.Invoke(sender, $"客戶端:{remoteEndPoint1}連接上我了!\r\n"); 43 SocketAsyncEventArgs readEventArgs = new SocketAsyncEventArgs(); 44 readEventArgs.Completed += OnSocketReceived; 45 readEventArgs.UserToken = e.AcceptSocket; 46 readEventArgs.SetBuffer(bytes, 0, 400); 47 if (!e.AcceptSocket.ReceiveAsync(readEventArgs)) 48 OnSocketReceived(e.AcceptSocket, readEventArgs); 49 lock (obj) 50 { 51 socket = e.AcceptSocket; 52 //上次有鏈接并且鏈接還”健在“ 53 if (Previous == null||! Previous.Connected) 54 { 55 Previous = e.AcceptSocket; 56 } 57 else 58 { 59 //Previous.SendAsync()..? 60 Previous.Send(Encoding.UTF8.GetBytes(remoteEndPoint1 + "_1")); 61 socket.Send(Encoding.UTF8.GetBytes(Previous.RemoteEndPoint.ToString() + "_2")); 62 NoticeMsg?.Invoke(sender, $"已經(jīng)通知雙方!\r\n"); 63 Previous = null; 64 } 65 } 66 67 e.AcceptSocket = null; 68 if (e.SocketError != SocketError.Success) 69 throw new SocketException((int)e.SocketError); 70 71 if(!listener.AcceptAsync(e)) 72 OnAccept(listener, e); 73 } 74 catch 75 { 76 socket?.Close(); 77 } 78 79 } 80 public void Close() 81 { 82 using (listener) 83 { 84 // listener.Shutdown(SocketShutdown.Both); 85 listener.Close(); 86 } 87 //throw new NotImplementedException(); 88 } 89 /// <summary> 90 /// 此處留有一個(gè)小BUG,,接收的字符串大于400的時(shí)候會(huì)有問題,;可以參考客戶端修改 91 /// </summary> 92 public void OnSocketReceived(object sender, SocketAsyncEventArgs e) 93 { 94 Socket socket = e.UserToken as Socket; 95 var remoteEndPoint = socket.RemoteEndPoint.ToString(); 96 try 97 { 98 if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success) 99 { 100 101 ReceivedMsg?.Invoke(sender, $"收到:{remoteEndPoint}發(fā)來信息:{Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred)}\r\n"); 102 103 } 104 else 105 { 106 socket?.Close(); 107 NoticeMsg?.Invoke(sender, $"鏈接:{remoteEndPoint}釋放啦,!\r\n"); 108 return; 109 } 110 if (!socket.ReceiveAsync(e)) 111 OnSocketReceived(socket, e); 112 } 113 catch 114 { 115 socket?.Close(); 116 } 117 118 //{ 119 // if (!((Socket)sender).AcceptAsync(e)) 120 // OnSocketReceived(sender, e); 121 //} 122 //catch 123 //{ 124 // return; 125 //} 126 } 127 }
2.客戶端類 PeerClient用BeginReceive和EndReceive實(shí)現(xiàn)異步,; public class StateObject { public Socket workSocket = null; public const int BufferSize = 100; public byte[] buffer = new byte[BufferSize]; public List<byte> buffers = new List<byte>(); //是不是和服務(wù)器的鏈接 public bool IsServerCon = false; } /// <summary> /// 打洞節(jié)點(diǎn)客戶端 實(shí)現(xiàn)的功能: /// 連接服務(wù)器獲取對(duì)方節(jié)點(diǎn)ip /// 請(qǐng)求對(duì)方ip(打洞) /// 根據(jù)條件判斷是監(jiān)聽連接還是監(jiān)聽等待連接 /// </summary> public class PeerClient : IPeerClient { //ManualResetEvent xxxxDone = new ManualResetEvent(false); //Semaphore /// <summary> /// 當(dāng)前鏈接 /// </summary> public Socket Client { get;private set; } #region 服務(wù)端 public string ServerHostName { get;private set; } public int ServerPort { get; private set; } #endregion #region 接收和通知事件 public delegate void EventMsg(object sender, string e); //接收事件 public event EventMsg ReceivedMsg; //通知消息 public event EventMsg NoticeMsg; #endregion //本地綁定的節(jié)點(diǎn) private IPEndPoint LocalEP; public PeerClient(string hostname, int port) { Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.ServerHostName = hostname; this.ServerPort = port; } /// <summary> /// 初始化客戶端(包括啟動(dòng)) /// </summary> public void Init() { try { Client.Connect(ServerHostName, ServerPort); } catch (SocketException ex) { NoticeMsg?.Invoke(Client, $"連接服務(wù)器失?。ex},!\r\n"); throw; } catch (Exception ex) { NoticeMsg?.Invoke(Client, $"連接服務(wù)器失敗,!{ex},!\r\n"); throw; } NoticeMsg?.Invoke(Client, $"連接上服務(wù)器了,!\r\n"); var _localEndPoint = Client.LocalEndPoint.ToString(); LocalEP = new IPEndPoint(IPAddress.Parse(_localEndPoint.Split(':')[0]) , int.Parse(_localEndPoint.Split(':')[1])); Receive(Client); } private void Receive(Socket client) { try { StateObject state = new StateObject(); state.workSocket = client; state.IsServerCon = true; client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } catch (Exception e) { NoticeMsg?.Invoke(Client, $"接收消息出錯(cuò)了{(lán)e}!\r\n"); } } private void ReceiveCallback(IAsyncResult ar) { try { var state = (StateObject)ar.AsyncState; Socket _client = state.workSocket; //因?yàn)榈竭@邊的經(jīng)常Connected 還是true //if (!_client.Connected) //{ // _client.Close(); // return; //} SocketError error = SocketError.Success; int bytesRead = _client.EndReceive(ar,out error); if (error == SocketError.ConnectionReset) { NoticeMsg?.Invoke(Client, $"鏈接已經(jīng)釋放,!\r\n"); _client.Close(); _client.Dispose(); return; } if (SocketError.Success!= error) { throw new SocketException((int)error); } var arr = state.buffer.AsQueryable().Take(bytesRead).ToArray(); state.buffers.AddRange(arr); if (bytesRead >= state.buffer.Length) { _client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); ////state.buffers.CopyTo(state.buffers.Count, state.buffer, 0, bytesRead); //_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, // new AsyncCallback(ReceiveCallback), state); } else { var _msg = Encoding.UTF8.GetString(state.buffers.ToArray()); ReceivedMsg?.Invoke(_client, _msg); if (state.IsServerCon) { _client.Shutdown(SocketShutdown.Both); _client.Close(); int retryCon = _msg.Contains("_1") ? 1 : 100; _msg = _msg.Replace("_1", "").Replace("_2", ""); TryConnection(_msg.Split(':')[0], int.Parse(_msg.Split(':')[1]), retryCon); return; } state = new StateObject(); state.IsServerCon = false; state.workSocket = _client; _client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } } catch (SocketException ex) { //10054 NoticeMsg?.Invoke(Client, $"鏈接已經(jīng)釋放!{ex},!\r\n"); } catch (Exception e) { NoticeMsg?.Invoke(Client, $"接收消息出錯(cuò)了2{e},!\r\n"); } } /// <summary> /// 打洞或者嘗試鏈接 /// </summary> private void TryConnection(string remoteHostname, int remotePort,int retryCon) { Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); var _iPRemotePoint = new IPEndPoint(IPAddress.Parse(remoteHostname), remotePort); Client.Bind(LocalEP); System.Threading.Thread.Sleep(retryCon==1?1:3*1000); for (int i = 0; i < retryCon; i++) { try { Client.Connect(_iPRemotePoint); NoticeMsg?.Invoke(Client, $"已經(jīng)連接上:{remoteHostname}:{remotePort},!\r\n"); StateObject state = new StateObject(); state.workSocket = Client; state.IsServerCon = false; Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); return; } catch { NoticeMsg?.Invoke(Client, $"嘗試第{i+1}次鏈接:{remoteHostname}:{remotePort},!\r\n"); } } if (retryCon==1) { Listening(LocalEP.Port); return; } NoticeMsg?.Invoke(Client, $"嘗試了{(lán)retryCon}次都沒有辦法連接到:{remoteHostname}:{remotePort},,涼了!\r\n"); } /// <summary> /// 如果連接不成功,,因?yàn)槭孪扔写蚨催^了,,根據(jù)條件監(jiān)聽 等待對(duì)方連接來 /// </summary> private void Listening(int Port) { try { Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); Client.Bind(new IPEndPoint(IPAddress.Any, Port)); Client.Listen((int)SocketOptionName.MaxConnections); NoticeMsg?.Invoke(Client, $"開始偵聽斷開等待鏈接過來!\r\n"); StateObject state = new StateObject(); state.IsServerCon = false; var _socket = Client.Accept();//只有一個(gè)鏈接 不用BeginAccept Client.Close();//關(guān)系現(xiàn)有偵聽 Client = _socket; state.workSocket = Client; NoticeMsg?.Invoke(Client, $"接收到來自{Client.RemoteEndPoint}的連接,!\r\n"); Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } catch (Exception ex) { NoticeMsg?.Invoke(Client, $"監(jiān)聽出錯(cuò)了{(lán)ex}涼了,!\r\n"); } //scoket.send } /// <summary> /// 本例子只存在一個(gè)成功的鏈接,對(duì)成功的連接發(fā)送消息,! /// </summary> /// <param name="strMsg"></param> public void Send(string strMsg) { byte[] bytes = Encoding.UTF8.GetBytes(strMsg); Client.BeginSend(bytes, 0, bytes.Length, 0, new AsyncCallback(SendCallback), Client); } private void SendCallback(IAsyncResult ar) { try { Socket _socket = (Socket)ar.AsyncState; //if(ar.IsCompleted) _socket.EndSend(ar); } catch (Exception e) { NoticeMsg?.Invoke(Client, $"發(fā)送消息出錯(cuò)了{(lán)e},!\r\n"); } } } 完整代碼: https:///qqljcn/zsg_-peer-to-peer Task+(TcpClient+TcpListener )|(UdpClient)實(shí)現(xiàn) tcp|udp的打洞,!這個(gè)就不貼代碼了直接放碼云連接 https:///qqljcn/zsg_-peer-to-peer_-lite 1.本人是個(gè)老菜鳥代碼僅供參考,,都是挺久以前寫的也沒有經(jīng)過嚴(yán)格的測(cè)試僅能演示這個(gè)例子,,有不成熟的地方,煩請(qǐng)各位大神海涵指教,; 2.不要都用本機(jī)試這個(gè)例子,,本機(jī)不走nat 3.然后udp因?yàn)槭菬o連接的所以打孔成功后不要等太久再發(fā)消息,nat緩存一過就失效了,! 4.確定自己不是對(duì)稱型nat的話,,如果打洞不成功,那就多試幾次! 5 .我這個(gè)例子代碼名字叫 PeerToPeer 但不是真的p2p,, 微軟提供了p2p的實(shí)現(xiàn) 在using System.Net.PeerToPeer命名空間下,。
以上是通過nat的方式,另外還有一種方式是,,通過一個(gè)有外網(wǎng)ip的第三方服務(wù)器轉(zhuǎn)發(fā)像 花生殼,、nat123這類軟件,也有做個(gè)小程序,,并且自己在用以后演示,;
|
|