最近研究了一下網絡打洞的相關技術,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文件服務,代碼逐步完善中。
