UDP打洞原理及代码
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中的方法了。
注:
private void test(object sender, UDPSockEventArgs e) { MessageBox.Show("ok"); }
- 随机文章
- 热门文章
- 热评文章
- UDP穿透NAT的原理与实现(UDP“打洞”原理)
- 基于C#的socket编程的TCP异步实现
- 在树莓派/linux上使用阿里云DNS解析作为DDNS使用
- C#图像处理(各种旋转、改变大小、柔化、锐化、雾化、底片、浮雕、黑白、滤镜效果)
- C# 生成图片缩略图
- 安装包制作工具 SetupFactory使用1 详解
- C#中的MessageBox消息对话框
- 永恒之蓝病毒是什么 怎么防范ONION勒索软件