1、为什么要udp打洞
现在大多数电脑上网都是通过路由器分配的网络进行上网的,当其中一台电脑请求网络时,路由器中的NAT软件会给这台电脑分配一个随机的端口号并将内网ip转换为公网ip,提供与外部网络的通信,当不是同一个局域网(不同路由器用户、路由器用户和猫用户)中的两台电脑相互请求通信时,由于不知道对方路由器分配的随机端口号,所以就无法直接进行通信了,这样就需要udp打洞了。(NAT可有效解决IP资源紧缺和日益匮乏的问题)
2、打洞过程
UDP打洞是通过一个Server端(ip固定),来接收两个请求相互通信的电脑的公网IP和随机端口号,然后发送给对方,这样两个请求相互通信的电脑就可以根据对方的公网ip和路由器分配的随机端口号进行通信了
(1)客户端A请求Server
(2)客户端B请求Server
(3)Server把客户端A的公网IP和端口信息发给客户端B、Server把客户端B的公网IP和端口信息发给客户端A
(4) 客户端A和客户端B通过对方公网IP和端口相互通信
注: 个人测试时发现,客户端A和客户端B请求顺序无关,发送信息顺序无关,只要知道对方ip和端口号即可马上通信,没有网络上说的,建立信任过程(可能和路由器有关,我测试的不一定正确)
3、实现代码
客户端代码
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Windows.Forms; namespace UdpClientTest { public partial class frmClient : Form { public frmClient() { InitializeComponent(); } //发送信息给服务端 UdpClient udpClient; string requesterIPStr = null;//请求通信的另一个客户端的ip信息 string flag = "hello!"; private void button2_Click(object sender, EventArgs e) { if (button2.Text == "连接") { IPEndPoint udpClientIP = new IPEndPoint(IPAddress.Any, 5001);//客户端电脑启用5001端口进行通信 udpClient = new UdpClient(udpClientIP); //请求Server端,以便Server端获取当前客户端由路由器NAT分配的公网ip和临时端口,再由Server端发送给另一个通信的客户端 string serverIPStr = txtIP.Text + ":" + txtPoint.Text; SendMessage(flag, serverIPStr); timer1.Start(); button2.Text = "连接中"; } else { udpClient.Close(); udpClient = null; timer1.Stop(); button2.Text = "连接"; txtMessage.Text = string.Empty; } } //接收信息 private void timer1_Tick(object sender, EventArgs e) { //不断读取发送的本地端口5001上的数据 if (udpClient.Client.Available > 0)//可防止报错 { IPEndPoint senderIP = null; byte[] recvData = udpClient.Receive(ref senderIP); string reciveMessage = Encoding.Default.GetString(recvData); if (reciveMessage.StartsWith("Addr:")) { //包含Addr:表示是由server端发送的请求与当前客户端通信的另一个客户端的ip信息 requesterIPStr = reciveMessage.Replace("Addr:", string.Empty); txtMessage.AppendText("udp打洞成功,请输入要发送的信息\r\n" + " " + DateTime.Now.ToString() + "\r\n"); txtMessage.ScrollToCaret(); } else { //和另一个客户端相互发送的信息 txtMessage.AppendText(senderIP.Address.ToString() + ":" + senderIP.Port + "说:" + reciveMessage + " " + DateTime.Now.ToString() + "\r\n"); txtMessage.ScrollToCaret(); txtContent.Focus(); } } Application.DoEvents(); } //发送信息 private void button1_Click(object sender, EventArgs e) { if (udpClient != null && requesterIPStr != null) { string sendMsg = txtContent.Text; SendMessage(sendMsg, requesterIPStr); } else { MessageBox.Show("还未打洞成功!"); } } //发送信息 private void SendMessage(string sendMsg, string requestIP) { string[] requesterIPTemp = requestIP.Split(':'); byte[] sendData = Encoding.Default.GetBytes(sendMsg); udpClient.Send(sendData, sendData.Length, requesterIPTemp[0], int.Parse(requesterIPTemp[1])); if (sendMsg != flag)//不是请求server的信息 { txtMessage.AppendText("我说:" + sendMsg + " " + DateTime.Now.ToString() + "\r\n"); txtMessage.ScrollToCaret(); txtContent.Text = string.Empty; txtContent.Focus(); } } } }
服务端代码
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Windows.Forms; using System.Collections.Generic; using System.Linq; namespace UdpServer { public partial class frmServer : Form { public frmServer() { InitializeComponent(); } UdpClient udpServer; List<string> clientIPList = new List<string>(); //开始监听 private void button1_Click(object sender, EventArgs e) { if (button1.Text == "监听") { button1.Text = "正在监听"; IPEndPoint udpServerIP = new IPEndPoint(IPAddress.Any, int.Parse(txtPoint.Text)); udpServer = new UdpClient(udpServerIP); timer1.Enabled = true; timer1.Start(); } else { timer1.Stop(); timer1.Enabled = false; udpServer.Close(); button1.Text = "监听"; clientIPList.Clear(); txtMessage.Text = string.Empty; } } //接收信息 private void timer1_Tick(object sender, EventArgs e) { if (udpServer.Client.Available > 0)//可防止报错 { IPEndPoint requesterIpPort = null;//当前连接的客户端的ip信息 byte[] recvData = udpServer.Receive(ref requesterIpPort); string recvMessage = Encoding.Default.GetString(recvData); string clientIpPort = requesterIpPort.Address.ToString() + ":" + requesterIpPort.Port; if (recvMessage == "hello!") { if (clientIPList.Contains(clientIpPort) == false && clientIPList.Count <= 2) { clientIPList.Add(clientIpPort); txtMessage.AppendText(clientIpPort + " 已连接 " + DateTime.Now.ToString() + "\r\n"); txtMessage.ScrollToCaret(); SendMessage("您已成功连接服务端!", clientIpPort); if (clientIPList.Count == 2) { //将客户端A的公网ip地址发给客户端B byte[] clientIpDataA = Encoding.Default.GetBytes("Addr:" + clientIPList[0]); string[] clientIpB = clientIPList[1].Split(':'); udpServer.Send(clientIpDataA, clientIpDataA.Length, clientIpB[0], int.Parse(clientIpB[1])); //将客户端B的公网ip地址发给客户端A byte[] clientIpDataB = Encoding.Default.GetBytes("Addr:" + clientIPList[1]); string[] clientIpA = clientIPList[0].Split(':'); udpServer.Send(clientIpDataB, clientIpDataB.Length, clientIpA[0], int.Parse(clientIpA[1])); //注:当客户端A和客户端B相互获取到对方的ip地址后,即可直接通信,不用经过server端 } } else { SendMessage("服务端提示客户端连接已满,示例只提供2个客户端同时连接通信!", clientIpPort); } } } Application.DoEvents(); } private void SendMessage(string sendMsg, string clientIpPort) { string[] clientAddress = clientIpPort.Split(':'); byte[] sendData = Encoding.Default.GetBytes(sendMsg); udpServer.Send(sendData, sendData.Length, clientAddress[0], int.Parse(clientAddress[1]));//客户端的 txtMessage.AppendText(clientIpPort + sendMsg + " " + DateTime.Now.ToString() + " \r\n"); txtMessage.ScrollToCaret(); txtContent.Text = string.Empty; } } }
4、界面效果
5、使用步骤
将服务端放到一个非路由器的网络中(服务器),启动服务端,点击监听,然后查看公网ip,将客户端复制到两台不同的电脑中(两台电脑不在同一个局域网),启动客户端1,连接IP地址填服务端所在的公网ip,启动客户端2,连接IP地址填服务端所在的公网ip,当两个客户端都提示可以相互通信后,就可以相互发送信息了,发送信息顺序无关。
参考文章:
http://blog.csdn.net/u011580175/article/details/71001796
http://blog.csdn.net/jdh99/article/details/6667648
https://www.cnblogs.com/dzqdzq/p/3856425.html
http://www.cnblogs.com/zxyc2000/articles/2846662.html
以上为个人学习总结,如有不正确之处,希望指出纠正,谢谢。