來源:http://www.fenbi360.net/Content.aspx?id=1021&t=jchtml
UDP"打洞"原理服務器
1. NAT分類網絡
根據Stun協議(RFC3489),NAT大體分爲下面四類併發
1) Full Coneide
這種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)oop
2) Restricted Conepost
這種NAT內部的機器A鏈接過外網的機器C後,NAT打開一個端口.而後C能夠用任何端口和A通訊.其餘的外網機器不行.ui
例如 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)this
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