來源:http://www.fenbi360.net/Content.aspx?id=1021&t=jc
UDP"打洞"原理
1. NAT分類
根據Stun協議(RFC3489),NAT大致分為下面四類
1) Full Cone
這種NAT內部的機器A連接過外網機器C后,NAT會打開一個端口.然后外網的任何發到這個打開的端口的UDP數據報都可以到達A.不管是不是C發過來的.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)
2) Restricted Cone
這種NAT內部的機器A連接過外網的機器C后,NAT打開一個端口.然后C可以用任何端口和A通信.其他的外網機器不行.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何從C發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)
3) Port Restricted Cone
這種NAT內部的機器A連接過外網的機器C后,NAT打開一個端口.然后C可以用原來的端口和A通信.其他的外網機器不行.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
C(202.88.88.88:2000)發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)
以上三種NAT通稱Cone NAT.我們只能用這種NAT進行UDP打洞.
4) Symmetic
對於這種NAT.連接不同的外部目標.原來NAT打開的端口會變化.而Cone NAT不會.雖然可以用端口猜測.但是成功的概率很小.因此放棄這種NAT的UDP打洞.
2. UDP hole punching
對於Cone NAT.要采用UDP打洞.需要一個公網機器C來充當”介紹人”.內網的A,B先分別和C通信.打開各自的NAT端口.C這個時候知道A,B的公網IP: Port. 現在A和B想直接連接.比如A給B發.除非B是Full Cone.否則不能通信.反之亦然.但是我們可以這樣.
A要連接B.A給B發一個UDP包.同時.A讓那個介紹人給B發一個命令,讓B同時給A發一個UDP包.這樣雙方的NAT都會記錄對方的IP,然后就會允許互相通信.
3. 同一個NAT后面的情況
如果A,B在同一個NAT后面.如果用上面的技術來進行互連.那么如果NAT支持loopback(就是本地到本地的轉換),A,B可以連接,但是比較浪費帶寬和NAT.有一種辦法是,A,B和介紹人通信的時候,同時把自己的local IP也告訴服務器.A,B通信的時候,同時發local ip和公網IP.誰先到就用哪個IP.但是local ip就有可能不知道發到什么地方去了.比如A,B在不同的NAT后面但是他們各自的local ip段一樣.A給B的local IP發的UDP就可能發給自己內部網里面的某某某了.
還有一個辦法是服務器來判斷A,B是否在一個NAT后面.(網絡拓朴不同會不會有問題?)
WellKnown.cs

//WellKnown公用庫 using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Net; using System.Net.Sockets; using System.Collections; namespace P2PWellKnown { /// <summary> /// UDP用戶登錄事件委托 /// </summary> /// <param name="sender">事件源對象</param> /// <param name="e">事件實體</param> public delegate void UdpUserLogInDelegate(object sender, UDPSockEventArgs e); /// <summary> /// 一般UDP消息事件委托 /// </summary> /// <param name="sender">事件源對象</param> /// <param name="e">事件實體</param> public delegate void UdpMessageDelegate(object sender, UDPSockEventArgs e); /// <summary> /// 初始化一個新連接的事件委托 /// </summary> /// <param name="sender">事件源對象</param> /// <param name="e">事件實體</param> public delegate void UdpNewConnectDelegate(object sender, UDPSockEventArgs e); /// <summary> /// P2P共享數據類 /// </summary> public class P2PConsts { /// <summary> /// UDP服務器監聽端口 /// </summary> public const int UDP_SRV_PORT = 2280; /// <summary> ///TCP服務器監聽端口 /// </summary> public const int TCP_SRV_PORT = 2000; } /// <summary> /// FormatterHelper 序列化,反序列化消息的幫助類 /// </summary> public class FormatterHelper { public static byte[] Serialize(object obj) { BinaryFormatter binaryF = new BinaryFormatter(); MemoryStream ms = new MemoryStream(1024 * 10); binaryF.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); byte[] buffer = new byte[(int)ms.Length]; ms.Read(buffer, 0, buffer.Length); ms.Close(); return buffer; } public static object Deserialize(byte[] buffer) { BinaryFormatter binaryF = new BinaryFormatter(); MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length, false); object obj = binaryF.Deserialize(ms); ms.Close(); return obj; } } /// <summary> /// 用於承載UDPSock信息的事件類 /// </summary> public class UDPSockEventArgs : EventArgs { /// <summary> /// 要承載的消息 /// </summary> private string m_strMsg; /// <summary> /// 用戶信息 /// </summary> private string m_strUserName; /// <summary> /// 觸發該事件的公共終端 /// </summary> private IPEndPoint m_EndPoint; /// <summary> /// 初始化UDPSock事件 /// </summary> /// <param name="sMsg">用戶發送的信息</param> public UDPSockEventArgs(string sMsg) : base() { this.m_strMsg = sMsg; } /// <summary> /// 遠端用戶名 /// </summary> public string RemoteUserName { get { return m_strUserName; } set { m_strUserName = value; } } /// <summary> /// 一般套接字消息 /// </summary> public string SockMessage { get { return m_strMsg; } set { m_strMsg = value; } } /// <summary> /// 公共遠端節點 /// </summary> public IPEndPoint RemoteEndPoint { get { return m_EndPoint; } set { m_EndPoint = value; } } } }
UDPP2PSock.cs

//UDPP2PSock.cs using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; using P2PWellKnown; namespace UDPP { /// <summary> /// UDPP2P套接字管理類 /// </summary> public class UDPP2PSock { /// <summary> /// 用戶登錄事件 /// </summary> public event UdpUserLogInDelegate OnUserLogInU; /// <summary> /// 一般UDP消息事件 /// </summary> public event UdpMessageDelegate OnSockMessageU; /// <summary> /// 初始化一個新連接事件 /// </summary> public event UdpNewConnectDelegate OnNewConnectU; /// <summary> /// UDP服務器 /// </summary> private UdpClient m_udpServer; /// <summary> /// UDP客戶端 /// </summary> private UdpClient m_udpClient; /// <summary> /// 服務器實際上在本地機器上監聽的 /// 端口,用於當一台計算機上同時啟 /// 動兩個可兩以上服務器進程時,標 /// 識不同的服務器進程 /// </summary> private int m_iMyServerPort; /// <summary> /// 客戶端在本地機器上實際使用的端口, /// 用於當一台計算機上同時有兩個或兩 /// 個以上客戶端進程在運行時,標識不 /// 同的客戶端進程 /// </summary> private int m_iMyClientPort; /// <summary> /// 標識是否已成功創服務器 /// </summary> private bool m_bServerCreated; /// <summary> /// 標識是否已成功創建客戶端 /// </summary> private bool m_bClientCreated; /// <summary> /// 服務器使用的線程 /// </summary> private Thread m_serverThread; /// <summary> /// 客戶端使用的線程 /// </summary> private Thread m_clientThread; /// <summary> /// 打洞線程 /// </summary> //private Thread m_burrowThread; /// <summary> /// 遠端節點 /// </summary> private IPEndPoint m_remotePoint; /// <summary> /// 當前進程作為客戶端的公共終端 /// </summary> private string m_strMyPublicEndPoint; /// <summary> /// 當前進程作為客戶端的私有終端 /// </summary> private string m_strMyPrivateEndPoint; /// <summary> /// 用於接受信息的 StringBuilder實例 /// </summary> private StringBuilder m_sbResponse = new StringBuilder(); /// <summary> /// P2P打洞時標識是否收到回應消息 /// </summary> private bool m_bRecvAck = false; /// <summary> /// 請求向其方向打洞的私有終端 /// </summary> private IPEndPoint m_requestPrivateEndPoint; /// <summary> /// 請求向其方向打洞的公共終端 /// </summary> private IPEndPoint m_requestPublicEndPoint; /// <summary> /// 打洞消息要發向的節點 /// </summary> private ToEndPoint m_toEndPoint; /// <summary> /// 用於標識是否已經和請求客戶端建立點對連接 /// </summary> //private bool m_bHasConnected=false ; /// <summary> /// 創建服務器或客戶端的最大嘗試 /// 次數,為(65536-60000),防止 /// 因不能創建而限入死循環或使用 /// 無效端口 /// </summary> private const int MAX_CREATE_TRY = 5536; /// <summary> /// 打洞時嘗試連接的最大嘗試次數 /// </summary> private const int MAX_CONNECT_TRY = 10; /// <summary> /// 構造函數,初始化UDPP2P實例 /// </summary> public UDPP2PSock() { m_iMyServerPort = P2PConsts.UDP_SRV_PORT; m_iMyClientPort = 60000; m_bClientCreated = false; m_bServerCreated = false; m_toEndPoint = new ToEndPoint(); m_serverThread = new Thread(new ThreadStart(RunUDPServer)); m_clientThread = new Thread(new ThreadStart(RunUDPClient)); //m_burrowThread = new Thread(new ThreadStart(BurrowProc)); } /// <summary> /// 創建UDP 服務器 /// </summary> public void CreateUDPSever() { int iTryNum = 0; //開始嘗試創建服務器 while (!m_bServerCreated && iTryNum < MAX_CREATE_TRY) { try { m_udpServer = new UdpClient(m_iMyServerPort); m_bServerCreated = true; } catch { m_iMyServerPort++; iTryNum++; } } //創建失敗,拋出異常 if (!m_bServerCreated && iTryNum == MAX_CREATE_TRY) { throw new Exception("創建服務器嘗試失敗!"); } m_serverThread.Start(); } /// <summary> /// 創建UDP客戶端 /// </summary> /// <param name="strServerIP"& gt;服務器IP</param> /// <param name="iServerPort"& gt;服務器端口</param> public void CreateUDPClient(string strServerIP, int iServerPort) { int iTryNum = 0; //開始嘗試創建服務器 while (!m_bClientCreated && iTryNum < MAX_CREATE_TRY) { try { m_udpClient = new UdpClient(m_iMyClientPort); m_bClientCreated = true; string strIPAddress = (System.Net.Dns.GetHostAddresses("localhost")[0]).ToString(); m_strMyPrivateEndPoint = strIPAddress + ":" + m_iMyClientPort.ToString(); } catch { m_iMyClientPort++; iTryNum++; } } //創建失敗,拋出異常 if (!m_bClientCreated && iTryNum == MAX_CREATE_TRY) { throw new Exception("創建客戶端嘗試失敗!"); } IPEndPoint hostPoint = new IPEndPoint(IPAddress.Parse(strServerIP), iServerPort); string strLocalIP = (System.Net.Dns.GetHostAddresses("localhost"))[0].ToString(); SendLocalPoint(strLocalIP, m_iMyClientPort, hostPoint); m_clientThread.Start(); } /// <summary> /// 運行UDP 服務器 /// </summary> private void RunUDPServer() { while (true) { byte[] msgBuffer = m_udpServer.Receive(ref m_remotePoint); m_sbResponse.Append(System.Text.Encoding.Default.GetString(msgBuffer)); CheckCommand(); Thread.Sleep(10); } } /// <summary> /// 運行UDP客戶端 /// </summary> private void RunUDPClient() { while (true) { byte[] msgBuffer = m_udpClient.Receive(ref m_remotePoint); m_sbResponse.Append(System.Text.Encoding.Default.GetString(msgBuffer)); CheckCommand(); Thread.Sleep(10); } } /// <summary> /// 銷毀UDP 服務器 /// </summary> public void DisposeUDPServer() { m_serverThread.Abort(); m_udpServer.Close(); } /// <summary> /// 銷毀UDP客房端 /// </summary> public void DisposeUDPClient() { m_clientThread.Abort(); m_udpClient.Close(); } /// <summary> /// 發送消息 /// </summary> /// <param name="strMsg"& gt;消息內容</param> /// <param name="REP"& gt;接收節點</param> public void SendData(string strMsg, IPEndPoint REP) { byte[] byMsg = System.Text.Encoding.Default.GetBytes(strMsg.ToCharArray()); m_udpClient.Send(byMsg, byMsg.Length, REP); } /// <summary> /// 發送消息,服務器專用 /// </summary> /// <param name="strMsg"& gt;消息內容</param> /// <param name="REP"& gt;接收節點</param> private void ServerSendData(string strMsg, IPEndPoint REP) { byte[] byMsg = System.Text.Encoding.Default.GetBytes(strMsg.ToCharArray()); m_udpServer.Send(byMsg, byMsg.Length, REP); } /// <summary> /// 發送本地節點信息 /// </summary> /// <param name="strLocalIP"& gt;本地IP</param> /// <param name="iLocalPort"& gt;本地端口</param> public void SendLocalPoint(string strLocalIP, int iLocalPort, IPEndPoint REP) { string strLocalPoint = "\x01\x02" + strLocalIP + ":" + iLocalPort.ToString() + "\x02\x01"; SendData(strLocalPoint, REP); } /// <summary> /// 同時向指定的終端(包括公共終端和私有終端)打洞 /// </summary> /// <param name="pubEndPoint"& gt;公共終端</param> /// <param name="prEndPoint"& gt;私有終端</param> /// <returns>打洞成功返回true,否則返回false</returns> public void StartBurrowTo(IPEndPoint pubEndPoint, IPEndPoint prEndPoint) { Thread burrowThread = new Thread(new ThreadStart(BurrowProc)); m_toEndPoint.m_privateEndPoint = prEndPoint; m_toEndPoint.m_publicEndPoint = pubEndPoint; burrowThread.Start(); } /// <summary> /// 打洞線程 /// </summary> private void BurrowProc() { IPEndPoint prEndPoint = m_toEndPoint.m_privateEndPoint; IPEndPoint pubEndPoint = m_toEndPoint.m_publicEndPoint; int j = 0; for (int i = 0; i < MAX_CONNECT_TRY; i++) { SendData("\x01\x07\x07\x01", prEndPoint); SendData("\x01\x07\x07\x01", pubEndPoint); // 等待接收線程標記修改 for (j = 0; j < MAX_CONNECT_TRY; j++) { if (m_bRecvAck) { m_bRecvAck = false; SendData("\x01\x07\x07\x01", prEndPoint); Thread.Sleep(50); SendData("\x01\x07\x07\x01", pubEndPoint); UDPSockEventArgs args = new UDPSockEventArgs(""); args.RemoteEndPoint = pubEndPoint; if (OnNewConnectU != null) { OnNewConnectU(this, args); } //Thread .Sleep (System .Threading.Timeout .Infinite ); return; } else { Thread.Sleep(100); } } //如果沒有收到目標主機的回應,表明本次打 // 洞嘗試失敗,等待100毫秒后嘗試下一次打洞 Thread.Sleep(100); } //MAX_CONNECT_TRY 嘗試都失敗,表明打洞失敗,拋出異常 //throw new Exception(" 打洞失敗!"); System.Windows.Forms.MessageBox.Show("打洞失敗!");//////////// } /// <summary> /// 轉發打洞請求消息,在服務器端使用 /// </summary> /// <param name="strSrcPrEndpoint"& gt;請求轉發的源私有終端</param> /// <param name="strSrcPubEndPoint"& gt;請求轉發的源公共終端</param> /// <param name="REP"& gt;轉發消息到達的目的終端</param> public void SendBurrowRequest(string strSrcPrEndpoint, string strSrcPubEndPoint, IPEndPoint REP) { string strBurrowMsg = "\x04\x07" + strSrcPrEndpoint + " " + strSrcPubEndPoint + "\x07\x04"; ServerSendData(strBurrowMsg, REP); } /// <summary> /// 檢查字符串中的命令 /// </summary> private void CheckCommand() { int nPos; string strCmd = m_sbResponse.ToString(); //如果接收遠端用戶名 if ((nPos = strCmd.IndexOf("\x01\x02")) > -1) { ReceiveName(strCmd, nPos); // 反饋公共終給端遠端主機 string strPubEPMsg = "\x03\x07" + m_remotePoint.ToString() + "\x07\x03"; SendData(strPubEPMsg, m_remotePoint); return; } //如果接收我的公共終端 if ((nPos = strCmd.IndexOf("\x03\x07")) > -1) { ReceiveMyPublicEndPoint(strCmd, nPos); return; } //如果是打洞請求消息 if ((nPos = strCmd.IndexOf("\x04\x07")) > -1) { ReceiveAndSendAck(strCmd, nPos); return; } //如果是打洞回應消息 if ((nPos = strCmd.IndexOf("\x01\x07")) > -1) { m_bRecvAck = true; int nPos2 = strCmd.IndexOf("\x07\x01"); if (nPos2 > -1) { m_sbResponse.Remove(nPos, nPos2 - nPos + 2); } return; } //一般聊天消息 m_sbResponse.Remove(0, strCmd.Length); RaiseMessageEvent(strCmd); } /// <summary> /// 接收遠端用戶名 /// </summary> /// <param name="strCmd"& gt;包含用戶名的控制信息</param> /// <param name="nPos"></param> private void ReceiveName(string strCmd, int nPos) { int nPos2 = strCmd.IndexOf("\x02\x01"); if (nPos2 == -1) { return; } m_sbResponse.Remove(nPos, nPos2 - nPos + 2); string strUserName = strCmd.Substring(nPos + 2, nPos2 - nPos - 2); UDPSockEventArgs e = new UDPSockEventArgs(""); e.RemoteUserName = strUserName; e.RemoteEndPoint = m_remotePoint; //觸發用戶登錄事件 if (OnUserLogInU != null) { OnUserLogInU(this, e); } } /// <summary> /// 接收打洞請求的消息並發送回應 /// </summary> /// <param name="strCmd"></param> /// <param name="nPos"></param> private void ReceiveAndSendAck(string strCmd, int nPos) { int nPos2 = strCmd.IndexOf("\x07\x04"); if (nPos2 == -1) { return; } m_sbResponse.Remove(nPos, nPos2 - nPos + 2); string strBurrowMsg = strCmd.Substring(nPos + 2, nPos2 - nPos - 2); string[] strSrcPoint = strBurrowMsg.Split(' '); //分析控制字符串包含的節點信息 string[] strPrEndPoint = strSrcPoint[0].Split(':'); string[] strPubEndPoint = strSrcPoint[1].Split(':'); m_requestPrivateEndPoint = new IPEndPoint(IPAddress.Parse(strPrEndPoint[0]), int.Parse(strPrEndPoint[1])); m_requestPublicEndPoint = new IPEndPoint(IPAddress.Parse(strPubEndPoint[0]), int.Parse(strPubEndPoint[1])); //向請求打洞終端的方向打洞 StartBurrowTo(m_requestPublicEndPoint, m_requestPrivateEndPoint); } /// <summary> /// 接收我的公共終端 /// </summary> /// <param name="strCmd"& gt;包含公共終端的控制信息</param> /// <param name="nPos"& gt;控制字符串的起始位置</param> private void ReceiveMyPublicEndPoint(string strCmd, int nPos) { int nPos2 = strCmd.IndexOf("\x07\x03"); if (nPos2 == -1) { return; } m_sbResponse.Remove(nPos, nPos2 - nPos + 2); m_strMyPublicEndPoint = strCmd.Substring(nPos + 2, nPos2 - nPos - 2); } /// <summary> /// 觸發一般UDP消息事件 /// </summary> /// <param name="strMsg"& gt;消息內容</param> private void RaiseMessageEvent(string strMsg) { UDPSockEventArgs args = new UDPSockEventArgs(""); args.SockMessage = strMsg; args.RemoteEndPoint = m_remotePoint; if (OnSockMessageU != null) { OnSockMessageU(this, args); } } /// <summary> /// 獲取當前進程作為客戶端的公共終端 /// </summary> public string MyPublicEndPoint { get { return m_strMyPublicEndPoint; } } /// <summary> /// 獲取當前進程作為客戶端的私有終端 /// </summary> public string MyPrivateEndPoint { get { return m_strMyPrivateEndPoint; } } } /// <summary> /// 保存打洞消息要發向的節點信息 /// </summary> class ToEndPoint { /// <summary> /// 私有節點 /// </summary> public IPEndPoint m_privateEndPoint; /// <summary> /// 公共節點 /// </summary> public IPEndPoint m_publicEndPoint; } }
關於如何使用上述程序包的一些說明:
主要程序的初始化,參考代碼如下:

using UDPP; using P2PWellKnown; //創建UDP服務器和客戶端 try { string strServerIP = "127.0.0.1"; UDPP2PSock udpSock = new UDPP2PSock(); udpSock.OnUserLogInU += new UdpUserLogInDelegate(OnUserLogInU); udpSock.OnNewConnectU += new UdpNewConnectDelegate(OnNewConnectU); udpSock.CreateUDPSever(); udpSock.CreateUDPClient(strServerIP, P2PConsts.UDP_SRV_PORT); } catch (Exception ex) { }
經上面的初始化后,就可以使用類UDPP2PSock中的方法了。
注:
udpSock.OnUserLogInU +=new UdpUserLogInDelegate(OnUserLogInU);
udpSock.OnNewConnectU +=new UdpNewConnectDelegate(OnNewConnectU);
中的OnUserLogInU和OnNewConnectU是事件名稱,如
private void test(object sender, UDPSockEventArgs e)
{
MessageBox.Show("ok");
}
出處:http://www.cnblogs.com/hcbin/archive/2010/04/10/1709019.html