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地址来的数据包。

    一旦一个内部地址(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、最后,我们可以让两台客户端互相连接,或者一台先发一个请求,打个洞;另一个在去连接;

    测试本机网络NAT类型

    首先下载NAT类型测试工具。下载码是:0898BB0089,下载码是啥?如何下载=》点击查看

    原stun Server:stun.ekiga.net已不可用,需要更改成:stun.qq.com

    

    经过测试,我的网络属于NAT3.

    TCP打洞示例代码

    来源于:blog.csdn.net/roujie3519/article/details/84540049

    服务端代码:

 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和端口后,接着就是要开启一个监听,等待对方的连接。原作者写的是监听自己的外网端口,但我觉得应该监听的是本地的端口。

    也就是原作者写法:int myPort = Convert.ToInt32(ips[1]);//自己的外网端口

    我修改成:int myPort = int.Parse(LocalEndPoint.ToString().Split(':')[1]);//本地端口

string host = "*";//服务端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);

            EndPoint LocalEndPoint = clientSocket.LocalEndPoint;
            Console.WriteLine("LocalEndPoint:" + LocalEndPoint.ToString());

            //获取到双方的ip及端口号
            byte[] recBytes = new byte[100];
            int bytes = clientSocket.Receive(recBytes, recBytes.Length, 0);
            string result = Encoding.ASCII.GetString(recBytes, 0, bytes);
            Console.WriteLine("Recv:" + result);//自己的外网IP:自己的外网端口:对方的外网IP:对方的外网端口
            clientSocket.Close();

            string[] ips = result.Split(':');
            int myPort = 0;//Convert.ToInt32(ips[1]);//自己的外网端口(原作者写法)
            myPort = int.Parse(LocalEndPoint.ToString().Split(':')[1]);//本地端口
            string otherIp = ips[2];//对方的外网IP
            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
                {
                    Console.WriteLine("Connect:{0},{1}...", otherIp, otherPort);
                    mySocket.Connect(otherIp, otherPort);
                    Console.WriteLine("Connect:成功{0},{1}", otherIp, otherPort);
                    break;
                }
                catch (Exception)
                {
                    Console.WriteLine("Connect:失败");
                }

            }
            //成功后可发送消息
            //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);
            //}

    TCP打洞测试

    首先在拥有公网IP的服务器中启动Server.exe。接着在两个不同的网络中启动client.exe,如家里的PC和公司的PC上分别启动client.exe。

    服务器Server.exe,如图:    

    办公室Client.exe,如图:    

    家里PC Client.exe,如图:    

    步骤如下(不能同在本机测试):

    (1)服务器启动一个监听;

    (2)启动一个客户端,连接服务器,发送“hello Server!”;

    (3)再启动一个客户端,连接服务器,发送“hello Server!”;

    (4)服务器向客户端分别发送对方的公网IP和端口,然后关闭连接,至此服务器完成任务;

    (5)客户端各自监听原来的本地端口,然后尝试连接对方的外网IP和端口。

    经过测试,我并没测试成功。按度娘所说,办公室网络和家里PC都属于NAT3,应可以TCP打洞成功的,不知是否是NAT设备不支持呢。

    客户端和服务器的下载码是:5BE56B637F,下载码是啥?如何下载=》点击查看icon-default.png?t=M85Bhttps://www.luweidong.cn/details/88

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值