C# UDP打洞通信个人总结

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


以上为个人学习总结,如有不正确之处,希望指出纠正,谢谢。

 

posted @ 2017-10-08 01:04  事理  阅读(1492)  评论(2编辑  收藏  举报