分享

C#代码实现TCP穿透(打洞)

 _明心见性_ 2021-12-07

​内网之间实现TCP通讯需要用到内网穿透技术,具体原理网上都有,参考:

https://blog.csdn.net/leisure512/article/details/4900191

https://blog.csdn.net/aaron133/article/details/79206257

TCP穿透成功的条件需要两边网络都是锥形NAT(或者至少一端网络是锥形NAT),具体可以参考

https://blog.csdn.net/h_armony/article/details/45167975

里面有给出各种NAT说明:

有公网IP的宽带:比如联通的ADSL,这类宽带会给每个用户分配一个公网IP,所以其NAT类型取决于用户所选用的路由器,大部分家用路由器都是端口限制锥型NAT;

无公网IP的宽带:比如宽带通,这类宽带给用户分配的是局域网IP,连接公网的NAT是运营商的,一般都是对称型NAT;

移动互联网:跟“无公网IP的宽带”类似,分配给手机的是局域网IP,出口基本都是对称型NAT;

大公司路由器:大部分都把路由器配置成对称型NAT。

这边使用VS2010 C#实现:

服务端代码:

static void Main(string[] args)
{
int port = 555;
IPEndPoint ipe = new IPEndPoint(IPAddress.Any, port);
Socket sSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sSocket.Bind(ipe);
sSocket.Listen(100);
Console.WriteLine(“监听已经打开,请等待”);

        while (true)
        {
            Socket serverSocket1 = sSocket.Accept();
            Console.WriteLine('连接已经建立');
            string recStr = '';
            byte[] recByte = new byte[4096];
            int bytes = serverSocket1.Receive(recByte);
            IPEndPoint ep1 = (IPEndPoint)serverSocket1.RemoteEndPoint;
            Console.WriteLine(' from {0}', ep1.ToString()); 
            recStr = Encoding.ASCII.GetString(recByte, 0, bytes);
            Console.WriteLine('客户端1:{0}', recStr);

            Socket serverSocket2 = sSocket.Accept();
            bytes = serverSocket2.Receive(recByte);
            IPEndPoint ep2 = (IPEndPoint)serverSocket2.RemoteEndPoint;
            Console.WriteLine(' from {0}', ep2.ToString());
            recStr = Encoding.ASCII.GetString(recByte, 0, bytes);
            Console.WriteLine('客户端2:{0}', recStr);


            byte[] sendByte =Encoding.ASCII.GetBytes(ep1.ToString() + ':' + ep2.ToString());  
            serverSocket1.Send(sendByte, sendByte.Length, 0);

            sendByte = Encoding.ASCII.GetBytes(ep2.ToString() + ':' + ep1.ToString());  
            serverSocket2.Send(sendByte, sendByte.Length, 0);

            serverSocket1.Close();
            serverSocket2.Close();
        } 
          
    } 

功能:两边客户端连接服务器后将映射的外网IP和端口号传给双方。

客户端代码

static void Main(string[] args)
{
string host = “115.21.X.X”;//服务端IP地址
int port = 555;
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//设置端口可复用
clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
//连接服务端
clientSocket.Connect(host, port);
Console.WriteLine(“Connect:” + host + ' ' + port);

        string data = 'hello,Server!';
        clientSocket.Send(Encoding.ASCII.GetBytes(data));
        Console.WriteLine('Send:' + data);
        byte[] recBytes = new byte[100];
        //获取到双方的ip及端口号
        int bytes = clientSocket.Receive(recBytes, recBytes.Length, 0);
       string result = Encoding.ASCII.GetString(recBytes, 0, bytes);
       Console.WriteLine('Recv:' +result);
       clientSocket.Close();

       string[] ips = result.Split(':'); 
       int myPort = Convert.ToInt32(ips[1]);
       string otherIp = ips[2];
       int otherPort = Convert.ToInt32(ips[3]);


       Socket mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
       mySocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        //绑定到之前连通过的端口号
       IPEndPoint ipe = new IPEndPoint(IPAddress.Any, Convert.ToInt32(myPort));
       mySocket.Bind(ipe);
        //尝试5次连接
       for (int j = 0; j < 5; j++)
       {
           try
           {
               mySocket.Connect(otherIp, otherPort);
               Console.WriteLine('Connect:成功{0},{1}', otherIp,otherPort);
               break;
           }
           catch (Exception)
           {
               Console.WriteLine('Connect:失败');
                // otherPort++;//如果是对称NAT,则有可能客户端的端口号已经改变,正常有规律的应该是顺序加1,可以尝试+1再试(我使用手机热点连接的时候端口号就变成+1的了)除非是碰到随机端口,那就不行了。
           }

       }
       while (true)
       {
           mySocket.Send(Encoding.ASCII.GetBytes('hello,the other client!'));

           byte[] recv = new byte[4096];
           int len = mySocket.Receive(recv, recv.Length, 0);
           result = Encoding.ASCII.GetString(recv, 0, len);
           Console.WriteLine('recv :' + result);

           Thread.Sleep(1000); 
       }

}

另一边客户端也一样。连接服务器后,可以绑定之前的端口号复用,但如果碰到一端是对称NAT时每次使用端口号会不一样时,这样就得通过预测下次可能使用的端口号来连通。如:使用手机热点网络连接服务器时,获取到它使用的端口号是56324,等到下一次客户端互相连接使用56325才连上。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约