最近研究了一下网络打洞的相关技术,TCP的方式据说可行性不高,各种困难,因此决定采用UDP(UDP是什么就不解释了)的方式。
原理:
我们都知道局域网内的主机想要访问外网的服务器是比较容易的,比如浏览器输入www.baidu.com就可以访问到百度的服务器,但是如果在局域网的主机部署一个服务,让外网的机器进行访问一般是无法访问的,因为外部访问的请求会被路由器给阻碍掉了,这是为什么呢?
比如我内网的主机IP是192.168.1.128,我访问外网的服务器的时候系统会自动给我的访问分配端口(也可以自定义端口),我对外的访问请求会经过路由器,路由器又会分配一个对外的端口,如果还有外部网络路由器,那么每一层都会分配一个独立的对外端口,一直到最终处于公网的路由器通过公网的IP及分配的端口对外部的服务器发起访问请求,服务器收到请求的同时会得到我处于公网的路由器的IP及分配的端口,然后将请求的反馈结果发送给我,反馈的信息会发到我的公网IP及端口,然后路由器内部再逐层向内发送给对应的IP和端口最终到达发起请求的应用程序。
如内网主机(192.168.1.128:12345)访问外网服务器(111.110.213.99:15000),那么实际的请求过程是这样的:内网主机(192.168.1.128:12345)发起请求,请求通过路由器A(假设只有一个路由器),路由器为内网的(192.168.1.128:12345)绑定一个动态的端口(18876)并通过路由器的外网IP(120.145.15.87:18876)访问外网服务器(111.110.213.99:15000),服务器收到请求后发送反馈数据给路由器(120.145.15.87:18876),路由器再根据记录的列表中18876端口绑定的内网地址,将信息转发给内网主机(192.168.1.128:12345),于是主机就收到外部的信息了。
注意,如果内网没有向外部访问,那么路由器就没有分配(18876)这个端口,那么外部发来的数据会被路由器丢弃掉,我们通过先连接服务器,服务器收到的(18876)并使用该端口或将该端口发送给其它客户端使用,那么这个端口其实就是我们打的一个(洞)。
由于一些原因没能用多台内网机器进行试验,只是简单的通过内网主机和外网的服务器进行的试验,下面贴上代码:
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.Text; 5 using System.Threading; 6 7 namespace P2MP 8 { 9 class MainClass 10 { 11 /// <summary> 12 /// 用于UDP发送的网络服务类 13 /// </summary> 14 private static UdpClient udpcSend = null; 15 16 static IPEndPoint localIpep = null; 17 18 public static void Main(string[] args) 19 { 20 Console.Write("IP:"); 21 string ip = Console.ReadLine(); 22 Console.Write("Port:"); 23 int port = int.Parse(Console.ReadLine()); 24 localIpep = new IPEndPoint(IPAddress.Parse(ip), port); // 本机IP,指定的端口号 25 26 udpcSend = new UdpClient(localIpep); 27 28 StartReceive(); 29 30 // 实名发送 31 string msg = null; 32 while ((msg = Console.ReadLine()) != null) 33 { 34 if ("stop" == msg) 35 { 36 StopReceive(); 37 udpcSend.Close(); 38 } 39 else 40 { 41 //string[] arr = Console.ReadLine().Split(' '); 42 Thread thrSend = new Thread(SendMessage); 43 thrSend.Start(msg); 44 } 45 } 46 Console.ReadKey(); 47 } 48 49 /// <summary> 50 /// 发送信息 51 /// </summary> 52 /// <param name="obj"></param> 53 private static void SendMessage(object obj) 54 { 55 try 56 { 57 string message = obj.ToString(); 58 string[] array = message.Split(' '); 59 IPAddress iPAddress = IPAddress.Parse(array[0]); 60 int port = int.Parse(array[1]); 61 byte[] sendbytes = Encoding.Unicode.GetBytes(array[2]); 62 IPEndPoint remoteIpep = new IPEndPoint(iPAddress, port); // 发送到的IP地址和端口号 63 udpcSend.Send(sendbytes, sendbytes.Length, remoteIpep); 64 } 65 catch{} 66 } 67 68 /// <summary> 69 /// 开关:在监听UDP报文阶段为true,否则为false 70 /// </summary> 71 static bool IsUdpcRecvStart = false; 72 /// <summary> 73 /// 线程:不断监听UDP报文 74 /// </summary> 75 static Thread thrRecv; 76 77 private static void StartReceive() 78 { 79 if (!IsUdpcRecvStart) // 未监听的情况,开始监听 80 { 83 thrRecv = new Thread(ReceiveMessage); 84 thrRecv.Start(); 85 IsUdpcRecvStart = true; 86 Console.WriteLine("UDP监听器已成功启动"); 87 } 88 } 89 90 private static void StopReceive() 91 { 92 if (IsUdpcRecvStart) 93 { 94 thrRecv.Abort(); // 必须先关闭这个线程,否则会异常 96 IsUdpcRecvStart = false; 97 Console.WriteLine("UDP监听器已成功关闭"); 98 } 99 } 100 101 /// <summary> 102 /// 接收数据 103 /// </summary> 104 /// <param name="obj"></param> 105 private static void ReceiveMessage(object obj) 106 { 108 while (IsUdpcRecvStart) 109 { 110 try 111 { 112 byte[] bytRecv = udpcSend.Receive(ref localIpep); 113 string message = Encoding.Unicode.GetString(bytRecv, 0, bytRecv.Length); 114 Console.WriteLine(string.Format("{0}[{1}]", localIpep, message)); 115 } 116 catch (Exception ex) 117 { 118 Console.WriteLine(ex.Message); 119 break; 120 } 121 } 122 } 123 } 124 }
可以同时在多个内网主机运行,并且保证其中有一个实在外网的服务器上运行,启动后输入本机的IP和使用的端口,当所有机器都显示“UDP监听器已成功启动”后,分别使用内网程序向 服务器IP地址[空格]端口号[空格]消息内容如:"188.90.9.145 12345 hello你好",发送消息给服务器,服务器收到的消息上附带客户端发来的对外端口,这时候就知道各个客户端的对外IP和端口了,各个主机想要给另一台主机发消息只要从服务器上看其它客户端的IP和端口,并通过“IP 端口 消息”的格式发送消息,网络良好不丢包的情况下就能发送进去了。
由于路由器会定时销毁记录的列表,因此还需要保持客户端跟服务器之间的心跳,比如每10秒发送一个消息,服务器端将各个客户端最新的列表保存下来。
暂时先贴出简单的代码,后续打算开发一个P2P文件服务,代码逐步完善中。