基於C#的內網穿透學習筆記(附源碼)

如何讓兩臺處在不一樣內網的主機直接互連?你須要內網穿透!
 
 
     上圖是一個非完整版內外網通信圖由內網端先發起,內網設備192.168.1.2:6677發送數據到外網時候必須通過nat會轉換成對應的外網ip+端口,而後在發送給外網設備,外網設備回覆數據也是發給你的外網ip+端口。這只是單向的內去外,那反過來,若是外網的設備須要主動訪問我局域網裏的某一個設備是沒法訪問的,由於這個時候還沒作nat轉換因此外網不知道你內網設備的應用具體對應的是哪一個端口,這個時候咱們就須要內網穿透了,內網穿透也叫NAT穿透;
 
        穿透原理
 
       如上圖所示經NAT轉換後的內外網地址+端口,會緩存一段時間,在這段時間內192.168.1.2:6677和112.48.69.2020:8899的映射關係會一直存在,這樣你的內網主機就獲得一個外網地址,這個對應關係又根據NAT轉換方法類型的不一樣,得用對應的方式實現打洞,NAT轉換方法類型有下列幾種(來源 百度百科NAT):
 
     (1)Full cone NAT:即著名的一對一(one-to-one)NAT。
 
       一旦一個內部地址(iAddr:port1)映射到外部地址(eAddr:port2),全部發自iAddr:port1的包都經由eAddr:port2向外發送。任意外部主機都能經過給eAddr:port2發包到iAddr:port1 (純自然不用打洞!)
 

     (2)Address-Restricted cone NAT :限制地址,即只接收曾經發送到對端的IP地址來的數據包。git

 

       一旦一個內部地址(iAddr:port1)映射到外部地址(eAddr:port2),全部發自iAddr:port1的包都經由eAddr:port2向外發送。任意外部主機(hostAddr:any)都能經過給eAddr:port2發包到達iAddr:port1的前提是:iAddr:port1以前發送過包到hostAddr:any. "any"也就是說端口不受限制(只需知道某個轉換後的外網ip+端口便可。)小程序

 

      (3)Port-Restricted cone NAT:相似受限制錐形NAT(Restricted cone NAT),可是還有端口限制。緩存

 

        一旦一個內部地址(iAddr:port1)映射到外部地址(eAddr:port2),全部發自iAddr:port1的包都經由eAddr:port2向外發送。一個外部主機(hostAddr:port3)可以發包到達iAddr:port1的前提是:iAddr:port1以前發送過包到hostAddr:port3. (雙方須要各自知道對方轉換後的外網ip+端口,而後一方先發一次嘗試鏈接,另外一方在次鏈接過來的時候就能直接連通了。)
 

      (4)Symmetric NAT(對稱NAT)服務器

 

       每個來自相同內部IP與port的請求到一個特定目的地的IP地址和端口,映射到一個獨特的外部來源的IP地址和端口。
 
       同一個內部主機發出一個信息包到不一樣的目的端,不一樣的映射使用外部主機收到了一封包從一個內部主機能夠送一封包回來 (只能和Full cone NAT連,無法打洞,手機流量開熱點就是,同一個本地端口鏈接不一樣的服務器獲得的外網第地址和IP不一樣!)
 
例子:
 
下面用一個例子演示下「受限制錐形NAT」的打洞,實現了這個它前面兩個類型也能通用。對稱型的話不考慮,打不了洞。咱們知道要實現兩臺「受限制錐形NAT」互連重點就是要知道對
方轉換後的外網IP+端口,這樣咱們能夠:
 
1. 準備一臺Full cone NAT 類型的外網服務端,接受來自兩個客戶端的鏈接,並對應告知對方ip+端口;
 
2.知道了對方ip+端口 須要設置socke:Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);這樣才能端口複用;目的就是讓鏈接對外的端口一致;
 
3.最後,咱們可讓兩臺客戶端互相鏈接,或者一臺先發一個請求,打個洞;另外一個在去鏈接;
 
代碼:
1.TCP+IOCP方式,相對 「面向對象」地實現穿透!
服務端 ServerListener類,用SocketAsyncEventArgs:
 
  1   /// <summary>
  2     /// 打洞服務端,很是的簡單,接收兩個鏈接而且轉發給對方;
  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             //是否同步就完成了,同步完成須要本身觸發
 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, $"已經通知雙方!\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         /// 此處留有一個小BUG,接收的字符串大於400的時候會有問題;能夠參考客戶端修改
 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}發來信息:{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實現異步;異步

  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>();
        //是否是和服務器的連接
        public bool IsServerCon = false;
    }
    /// <summary>
    /// 打洞節點客戶端 實現的功能:
    /// 鏈接服務器獲取對方節點ip 
    /// 請求對方ip(打洞)
    /// 根據條件判斷是監聽鏈接仍是監聽等待鏈接
    /// </summary>
    public class PeerClient : IPeerClient
    {
        //ManualResetEvent xxxxDone =  new ManualResetEvent(false);
        //Semaphore 
        /// <summary>
        /// 當前連接
        /// </summary>
        public Socket Client { get;private set; }

        #region 服務端
        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

        //本地綁定的節點
        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>
        /// 初始化客戶端(包括啓動)
        /// </summary>
        public void Init()
        {
            try
            {
                Client.Connect(ServerHostName, ServerPort);
            }
            catch (SocketException ex)
            {
                NoticeMsg?.Invoke(Client, $"鏈接服務器失敗!{ex}!\r\n");
                throw;
            }
            catch (Exception ex)
            {
                NoticeMsg?.Invoke(Client, $"鏈接服務器失敗!{ex}!\r\n");
                throw;
            }
            NoticeMsg?.Invoke(Client, $"鏈接上服務器了!\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, $"接收消息出錯了{e}!\r\n");
            }
        }
        private void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                var state = (StateObject)ar.AsyncState;
                Socket _client = state.workSocket;
                //由於到這邊的常常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, $"連接已經釋放!\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, $"連接已經釋放!{ex}!\r\n");

            }
            catch (Exception e)
            {
                NoticeMsg?.Invoke(Client, $"接收消息出錯了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, $"已經鏈接上:{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, $"嘗試了{retryCon}次都沒有辦法鏈接到:{remoteHostname}:{remotePort},涼了!\r\n");

        }
        /// <summary>
        /// 若是鏈接不成功,由於事先有打洞過了,根據條件監聽 等待對方鏈接來
        /// </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();//只有一個連接 不用BeginAccept
                Client.Close();//關係現有偵聽
                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, $"監聽出錯了{ex}涼了!\r\n");
            }
            //scoket.send



        }
        /// <summary>
        /// 本例子只存在一個成功的連接,對成功的鏈接發送消息!
        /// </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, $"發送消息出錯了{e}!\r\n");
            }
        }
    }

完整代碼:socket

 https://gitee.com/qqljcn/zsg_-peer-to-peertcp

2、面向過程方式:測試

Task+(TcpClient+TcpListener )|(UdpClient)實現 tcp|udp的打洞!這個就不貼代碼了直接放碼雲鏈接this

 https://gitee.com/qqljcn/zsg_-peer-to-peer_-litespa

3、說明: 

1.本人是個老菜鳥代碼僅供參考,都是挺久之前寫的也沒有通過嚴格的測試僅能演示這個例子,有不成熟的地方,煩請各位大神海涵指教;

2.不要都用本機試這個例子,本機不走nat

3.而後udp由於是無鏈接的因此打孔成功後不要等過久再發消息,nat緩存一過就失效了!

4.肯定本身不是對稱型nat的話,若是打洞不成功,那就多試幾回! 

5 .我這個例子代碼名字叫 PeerToPeer 但不是真的p2p, 微軟提供了p2p的實現 在using System.Net.PeerToPeer命名空間下。

 

以上是經過nat的方式,另外還有一種方式是,經過一個有外網ip的第三方服務器轉發像 花生殼、nat123這類軟件,也有作個小程序,而且本身在用之後演示;

相關文章
相關標籤/搜索