C# UDP穿越NAT,UDP打洞,UDP Hole Punching

时间:2021-10-31 07:05:06
C#实现UDP穿越NAT程序运行效果图

C# UDP穿越NAT,UDP打洞,UDP Hole Punching


(图一)运行在公网上的服务器程序,用于转发打洞消息.

C# UDP穿越NAT,UDP打洞,UDP Hole Punching
(图二)运行在公网上的测试客户端程序A

C# UDP穿越NAT,UDP打洞,UDP Hole Punching

(图三)运行在NAT网络上的测试客户端程序B


C# UDP穿越NAT,UDP打洞,UDP Hole Punching

(图四) UDP打洞过程状态图



***阅读下面代码前请先了解UDP穿越NAT原理***


1.服务器主窗体源代码

public partial class frmServer : Form
{
   private Server _server;
   
   public frmServer()
   {
      InitializeComponent();
   }
   
   private void button1_Click(object sender, EventArgs e)
   {
      _server = new Server();
      _server.OnWriteLog += new WriteLogHandle(server_OnWriteLog);
      _server.OnUserChanged += new UserChangedHandle(OnUserChanged);
      try
      {
         _server.Start();
      }
      catch (Exception ex)
      {
         MessageBox.Show(ex.Message);
      }
   }
   
   //刷新用户列表 
   private void OnUserChanged(UserCollection users)
   {
      listBox2.DisplayMember = "FullName";
      listBox2.DataSource = null;
      listBox2.DataSource = users;
   }
   
   //显示跟踪消息 
   public void server_OnWriteLog(string msg)
   {
      listBox1.Items.Add(msg);
      listBox1.SelectedIndex = listBox1.Items.Count - 1;
   }
   
   private void button2_Click(object sender, EventArgs e)
   {
      Application.Exit();
   }
   
   private void frmServer_FormClosing(object sender, FormClosingEventArgs e)
   {
      if (_server != null)
      _server.Stop();
   }
   
   private void button3_Click(object sender, EventArgs e)
   {
      //发送消息给所有在线用户 
      P2P_TalkMessage msg = new P2P_TalkMessage(textBox1.Text);
      foreach (object o in listBox2.Items)
      {
         User user = o as User;
         _server.SendMessage(msg, user.NetPoint);
      }
   }
   
   private void button6_Click(object sender, EventArgs e)
   {
      listBox1.Items.Clear();
   }
}




2.服务器业务类
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using vjsdn.net.library;
using System.Windows.Forms;

namespace vjsdn.net.library
{
   /// <summary> 
   /// 服务器端业务类 
   /// </summary> 
   public class Server
   {
      private UdpClient _server; //服务器端消息监听 
      private UserCollection _userList; //在线用户列表 
      private Thread _serverThread;
      private IPEndPoint _remotePoint; //远程用户请求的IP地址及端口 
      
      private WriteLogHandle _WriteLogHandle = null;
      private UserChangedHandle _UserChangedHandle = null;
      
      /// <summary> 
      /// 显示跟踪消息 
      /// </summary> 
      public WriteLogHandle OnWriteLog
      {
         get { return _WriteLogHandle; }
         set { _WriteLogHandle = value; }
      }
      
      /// <summary> 
      /// 当用户登入/登出时触发此事件 
      /// </summary> 
      public UserChangedHandle OnUserChanged
      {
         get { return _UserChangedHandle; }
         set { _UserChangedHandle = value; }
      }
      
      /// <summary> 
      /// 构造器 
      /// </summary> 
      public Server()
      {
         _userList = new UserCollection();
         _remotePoint = new IPEndPoint(IPAddress.Any, 0);
         _serverThread = new Thread(new ThreadStart(Run));
      }
      
      /// <summary> 
      ///显示跟踪记录 
      /// </summary> 
      /// <param name="log"></param> 
      private void DoWriteLog(string log)
      {
         if (_WriteLogHandle != null)
         (_WriteLogHandle.Target as System.Windows.Forms.Control).Invoke(_WriteLogHandle, log);
      }
      
      /// <summary> 
      /// 刷新用户列表 
      /// </summary> 
      /// <param name="list">用户列表</param> 
      private void DoUserChanged(UserCollection list)
      {
         if (_UserChangedHandle != null)
         (_UserChangedHandle.Target as Control).Invoke(_UserChangedHandle, list);
      }
      
      /// <summary> 
      /// 开始启动线程 
      /// </summary> 
      public void Start()
      {
         try
         {
            _server = new UdpClient(Globals.SERVER_PORT);
            _serverThread.Start();
            DoWriteLog("服务器已经启动,监听端口:" + Globals.SERVER_PORT.ToString() + ",等待客户连接...");
         }
         catch (Exception ex)
         {
            DoWriteLog("启动服务器发生错误: " + ex.Message);
            throw ex;
         }
      }
      
      /// <summary> 
      /// 停止线程 
      /// </summary> 
      public void Stop()
      {
         DoWriteLog("停止服务器...");
         try
         {
            _serverThread.Abort();
            _server.Close();
            DoWriteLog("服务器已停止.");
         }
         catch (Exception ex)
         {
            DoWriteLog("停止服务器发生错误: " + ex.Message);
            throw ex;
         }
      }
      
      //线程主方法 
      private void Run()
      {
         byte[] msgBuffer = null;
         
         while (true)
         {
            msgBuffer = _server.Receive(ref _remotePoint); //接受消息 
            try
            {
               //将消息转换为对象 
               object msgObject = ObjectSerializer.Deserialize(msgBuffer);
               if (msgObject == nullcontinue;
               
               Type msgType = msgObject.GetType();
               DoWriteLog("接收到消息:" + msgType.ToString());
               DoWriteLog("From:" + _remotePoint.ToString());
               
               //新用户登录 
               if (msgType == typeof(C2S_LoginMessage))
               {
                  C2S_LoginMessage lginMsg = (C2S_LoginMessage)msgObject;
                  DoWriteLog(string.Format("用户’{0}’已登录!", lginMsg.FromUserName));
                  
                  // 添加用户到列表 
                  IPEndPoint userEndPoint = new IPEndPoint(_remotePoint.Address, _remotePoint.Port);
                  User user = new User(lginMsg.FromUserName, userEndPoint);
                  _userList.Add(user);
                  
                  this.DoUserChanged(_userList);
                  
                  //通知所有人,有新用户登录 
                  S2C_UserAction msgNewUser = new S2C_UserAction(user, UserAction.Login);
                  foreach (User u in _userList)
                  {
                     if (u.UserName == user.UserName) //如果是自己,发送所有在线用户列表
                     this.SendMessage(new S2C_UserListMessage(_userList), u.NetPoint);
                     else
                     this.SendMessage(msgNewUser, u.NetPoint);
                  }
               }
               else if (msgType == typeof(C2S_LogoutMessage))
               {
                  C2S_LogoutMessage lgoutMsg = (C2S_LogoutMessage)msgObject;
                  DoWriteLog(string.Format("用户’{0}’已登出!", lgoutMsg.FromUserName));
                  
                  // 从列表中删除用户 
                  User logoutUser = _userList.Find(lgoutMsg.FromUserName);
                  if (logoutUser != null) _userList.Remove(logoutUser);
                  
                  this.DoUserChanged(_userList);
                  
                  //通知所有人,有用户登出 
                  S2C_UserAction msgNewUser = new S2C_UserAction(logoutUser, UserAction.Logout);
                  foreach (User u in _userList)
                  this.SendMessage(msgNewUser, u.NetPoint);
               }
               
               else if (msgType == typeof(C2S_HolePunchingRequestMessage))
               {
                  //接收到A给B打洞的消息,打洞请求,由客户端发送给服务器端 
                  C2S_HolePunchingRequestMessage msgHoleReq = (C2S_HolePunchingRequestMessage)msgObject;
                  
                  User userA = _userList.Find(msgHoleReq.FromUserName);
                  User userB = _userList.Find(msgHoleReq.ToUserName);
                  
                  // 发送打洞(Punching Hole)消息 
                  DoWriteLog(string.Format("用户:[{0} IP:{1}]想与[{2} IP:{3}]建立对话通道.",
                  userA.UserName, userA.NetPoint.ToString(),
                  userB.UserName, userB.NetPoint.ToString()));
                  
                  //由Server发送消息给B,将A的IP的IP地址信息告诉B,然后由B发送一个测试消息给A. 
                  S2C_HolePunchingMessage msgHolePunching = newS2C_HolePunchingMessage(_remotePoint);
                  this.SendMessage(msgHolePunching, userB.NetPoint); //Server->B 
               }
               else if (msgType == typeof(C2S_GetUsersMessage))
               {
                  // 发送当前用户信息 
                  S2C_UserListMessage srvResMsg = new S2C_UserListMessage(_userList);
                  this.SendMessage(srvResMsg, _remotePoint);
               }
            }
            catch (Exception ex) { DoWriteLog(ex.Message); }
         }
      }
      /// <summary> 
      /// 发送消息 
      /// </summary> 
      public void SendMessage(MessageBase msg, IPEndPoint remoteIP)
      {
         DoWriteLog("正在发送消息:" + msg.ToString());
         if (msg == nullreturn;
         byte[] buffer = ObjectSerializer.Serialize(msg);
         _server.Send(buffer, buffer.Length, remoteIP);
         DoWriteLog("消息已发送.");
      }
   }
}





3.客户端主窗体源代码
public partial class frmClient : Form
{
   private Client _client;
   
   public frmClient()
   {
      InitializeComponent();
   }
   
   private void frmClient_Load(object sender, EventArgs e)
   {
      _client = new Client();
      _client.OnWriteMessage = this.WriteLog;
      _client.OnUserChanged = this.OnUserChanged;
   }
   
   private void button1_Click(object sender, EventArgs e)
   {
      _client.Login(textBox2.Text, "");
      _client.Start();
   }
   
   private void WriteLog(string msg)
   {
      listBox2.Items.Add(msg);
      listBox2.SelectedIndex = listBox2.Items.Count - 1;
   }
   
   private void button4_Click(object sender, EventArgs e)
   {
      this.Close();
   }
   
   private void button3_Click(object sender, EventArgs e)
   {
      if (_client != null)
      {
         User user = listBox1.SelectedItem as User;
         _client.HolePunching(user);
      }
   }
   
   private void button2_Click(object sender, EventArgs e)
   {
      if (_client != null) _client.DownloadUserList();
   }
   
   private void frmClient_FormClosing(object sender, FormClosingEventArgs e)
   {
      if (_client != null) _client.Logout();
   }
   
   private void OnUserChanged(UserCollection users)
   {
      listBox1.DisplayMember = "FullName";
      listBox1.DataSource = null;
      listBox1.DataSource = users;
   }
   
   private void button5_Click(object sender, EventArgs e)
   {
      P2P_TalkMessage msg = new P2P_TalkMessage(textBox1.Text);
      User user = listBox1.SelectedItem as User;
      _client.SendMessage(msg, user);
   }
   
   private void button6_Click(object sender, EventArgs e)
   {
      listBox2.Items.Clear();
   }
}




4.客户端业务逻辑代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using vjsdn.net.library;
using System.Windows.Forms;
using System.IO;

namespace vjsdn.net.library
{
   /// <summary> 
   /// 客户端业务类 
   /// </summary> 
   public class Client : IDisposable
   {
      //private const int MAX_RETRY_SEND_MSG = 1; //打洞时连接次数,正常情况下一次就能成功 
      
      private UdpClient _client;//客户端监听 
      private IPEndPoint _hostPoint; //主机IP 
      private IPEndPoint _remotePoint; //接收任何远程机器的数据 
      private UserCollection _userList;//在线用户列表 
      private Thread _listenThread; //监听线程 
      private string _LocalUserName; //本地用户名 
      //private bool _HoleAccepted = false; //A->B,接收到B用户的确认消息 
      
      private WriteLogHandle _OnWriteMessage;
      public WriteLogHandle OnWriteMessage
      {
         get { return _OnWriteMessage; }
         set { _OnWriteMessage = value; }
      }
      
      private UserChangedHandle _UserChangedHandle = null;
      public UserChangedHandle OnUserChanged
      {
         get { return _UserChangedHandle; }
         set { _UserChangedHandle = value; }
      }
      
      /// <summary> 
      ///显示跟踪记录 
      /// </summary> 
      /// <param name="log"></param> 
      private void DoWriteLog(string log)
      {
         if (_OnWriteMessage != null)
         (_OnWriteMessage.Target as Control).Invoke(_OnWriteMessage, log);
      }
      
      /// <summary> 
      /// 构造器 
      /// </summary> 
      /// <param name="serverIP"></param> 
      public Client()
      {
         string serverIP = this.GetServerIP();
         _remotePoint = new IPEndPoint(IPAddress.Any, 0); //任何与本地连接的用户IP地址。 
         _hostPoint = new IPEndPoint(IPAddress.Parse(serverIP), Globals.SERVER_PORT); //服务器地址 
         _client = new UdpClient();//不指定端口,系统自动分配 
         _userList = new UserCollection();
         _listenThread = new Thread(new ThreadStart(Run));
      }
      
      /// <summary> 
      /// 获取服务器IP,INI文件内设置 
      /// </summary> 
      /// <returns></returns> 
      private string GetServerIP()
      {
         string file = Application.StartupPath + "\\ip.ini";
         string ip = File.ReadAllText(file);
         return ip.Trim();
      }
      
      /// <summary> 
      /// 启动客户端 
      /// </summary> 
      public void Start()
      {
         if (this._listenThread.ThreadState == ThreadState.Unstarted)
         {
            this._listenThread.Start();
         }
      }
      
      /// <summary> 
      /// 客户登录 
      /// </summary> 
      public void Login(string userName, string password)
      {
         _LocalUserName = userName;
         
         // 发送登录消息到服务器 
         C2S_LoginMessage loginMsg = new C2S_LoginMessage(userName, password);
         this.SendMessage(loginMsg, _hostPoint);
      }
      
      /// <summary> 
      /// 登出 
      /// </summary> 
      public void Logout()
      {
         C2S_LogoutMessage lgoutMsg = new C2S_LogoutMessage(_LocalUserName);
         this.SendMessage(lgoutMsg, _hostPoint);
         
         this.Dispose();
         System.Environment.Exit(0);
      }
      
      /// <summary> 
      /// 发送请求获取用户列表 
      /// </summary> 
      public void DownloadUserList()
      {
         C2S_GetUsersMessage getUserMsg = new C2S_GetUsersMessage(_LocalUserName);
         this.SendMessage(getUserMsg, _hostPoint);
      }
      
      /// <summary> 
      /// 显示在线用户 
      /// </summary> 
      /// <param name="users"></param> 
      private void DisplayUsers(UserCollection users)
      {
         if (_UserChangedHandle != null)
         (_UserChangedHandle.Target as Control).Invoke(_UserChangedHandle, users);
      }
      
      //运行线程 
      private void Run()
      {
         try
         {
            byte[] buffer;//接受数据用 
            while (true)
            {
               buffer = _client.Receive(ref _remotePoint);//_remotePoint变量返回当前连接的用户IP地址 
               
               object msgObj = ObjectSerializer.Deserialize(buffer);
               Type msgType = msgObj.GetType();
               DoWriteLog("接收到消息:" + msgType.ToString() + " From:" + _remotePoint.ToString());
               
               if (msgType == typeof(S2C_UserListMessage))
               {
                  // 更新用户列表 
                  S2C_UserListMessage usersMsg = (S2C_UserListMessage)msgObj;
                  _userList.Clear();
                  
                  foreach (User user in usersMsg.UserList)
                  _userList.Add(user);
                  
                  this.DisplayUsers(_userList);
               }
               else if (msgType == typeof(S2C_UserAction))
               {
                  //用户动作,新用户登录/用户登出 
                  S2C_UserAction msgAction = (S2C_UserAction)msgObj;
                  if (msgAction.Action == UserAction.Login)
                  {
                     _userList.Add(msgAction.User);
                     this.DisplayUsers(_userList);
                  }
                  else if (msgAction.Action == UserAction.Logout)
                  {
                     User user = _userList.Find(msgAction.User.UserName);
                     if (user != null) _userList.Remove(user);
                     this.DisplayUsers(_userList);
                  }
               }
               else if (msgType == typeof(S2C_HolePunchingMessage))
               {
                  //接受到服务器的打洞命令 
                  S2C_HolePunchingMessage msgHolePunching = (S2C_HolePunchingMessage)msgObj;
                  
                  //NAT-B的用户给NAT-A的用户发送消息,此时UDP包肯定会被NAT-A丢弃, 
                  //因为NAT-A上并没有A->NAT-B的合法Session, 但是现在NAT-B上就建立了有B->NAT-A的合法session了! 
                  P2P_HolePunchingTestMessage msgTest = newP2P_HolePunchingTestMessage(_LocalUserName);
                  this.SendMessage(msgTest, msgHolePunching.RemotePoint);
               }
               else if (msgType == typeof(P2P_HolePunchingTestMessage))
               {
                  //UDP打洞测试消息 
                  //_HoleAccepted = true; 
                  P2P_HolePunchingTestMessage msgTest = (P2P_HolePunchingTestMessage)msgObj;
                  UpdateConnection(msgTest.UserName, _remotePoint);
                  
                  //发送确认消息 
                  P2P_HolePunchingResponse response = new P2P_HolePunchingResponse(_LocalUserName);
                  this.SendMessage(response, _remotePoint);
               }
               else if (msgType == typeof(P2P_HolePunchingResponse))
               {
                  //_HoleAccepted = true;//打洞成功 
                  P2P_HolePunchingResponse msg = msgObj as P2P_HolePunchingResponse;
                  UpdateConnection(msg.UserName, _remotePoint);
                  
               }
               else if (msgType == typeof(P2P_TalkMessage))
               {
                  //用户间对话消息 
                  P2P_TalkMessage workMsg = (P2P_TalkMessage)msgObj;
                  DoWriteLog(workMsg.Message);
               }
               else
               {
                  DoWriteLog("收到未知消息!");
               }
            }
         }
         catch (Exception ex) { DoWriteLog(ex.Message); }
      }
      
      private void UpdateConnection(string user, IPEndPoint ep)
      {
         User remoteUser = _userList.Find(user);
         if (remoteUser != null)
         {
            remoteUser.NetPoint = ep;//保存此次连接的IP及端口 
            remoteUser.IsConnected = true;
            DoWriteLog(string.Format("您已经与{0}建立通信通道,IP:{1}!",
            remoteUser.UserName, remoteUser.NetPoint.ToString()));
            this.DisplayUsers(_userList);
         }
      }
      
      #region IDisposable 成员
      
      public void Dispose()
      {
         try
         {
            this._listenThread.Abort();
            this._client.Close();
         }
         catch
         {
            
         }
      }
      
      #endregion
      
      public void SendMessage(MessageBase msg, User user)
      {
         this.SendMessage(msg, user.NetPoint);
      }
      
      public void SendMessage(MessageBase msg, IPEndPoint remoteIP)
      {
         if (msg == nullreturn;
         DoWriteLog("正在发送消息给->" + remoteIP.ToString() + ",内容:" + msg.ToString());
         byte[] buffer = ObjectSerializer.Serialize(msg);
         _client.Send(buffer, buffer.Length, remoteIP);
         DoWriteLog("消息已发送.");
      }
      
      /// <summary> 
      /// UDP打洞过程 
      /// 假设A想连接B.首先A发送打洞消息给Server,让Server告诉B有人想与你建立通话通道,Server将A的IP信息转发给B 
      /// B收到命令后向A发一个UDP包,此时B的NAT会建立一个与A通讯的Session. 然后A再次向B发送UDP包B就能收到了 
      /// </summary> 
      public void HolePunching(User user)
      {
         //A:自己; B:参数user 
         //A发送打洞消息给服务器,请求与B打洞 
         C2S_HolePunchingRequestMessage msg = newC2S_HolePunchingRequestMessage(_LocalUserName, user.UserName);
         this.SendMessage(msg, _hostPoint);
         
         Thread.Sleep(2000);//等待对方发送UDP包并建立Session 
         
         //再向对方发送确认消息,如果对方收到会发送一个P2P_HolePunchingResponse确认消息,此时打洞成功 
         P2P_HolePunchingTestMessage confirmMessage = newP2P_HolePunchingTestMessage(_LocalUserName);
         this.SendMessage(confirmMessage, user);
      }
   }
   
}