思路如下(参照源代码):
1、 frmServer启动两个网络侦听,主连接侦听,协助打洞的侦听。
2、 frmClientA和frmClientB分别与frmServer的主连接保持联系。
3、 当frmClientA需要和frmClientB建立直接的udp连接时,首先连接frmServer的协助打洞端口,并发送协助连接申请,同时在该端口号上启动侦听。
4、 frmServer的协助打洞连接收到frmClientA的申请后通过主连接通知frmClientB,并将frmClientA经过NAT-A转换后的公网IP地址和端口等信息告诉frmClientB。
5、 frmClientB收到frmServer的连接通知后首先与frmServer的协助打洞端口连接,发送一些数据后立即断开,目的是让frmServer能知道frmClientB经过NAT-B转换后的公网IP和端口号。
6、 frmClientB尝试与frmClientA的经过NAT-A转换后的公网IP地址和端口进行connect,不同的路由器会有不同的结果,多数路由器对未知不请自到的SYN请求包直接丢弃而导致connect失败,但NAT-A会纪录此次连接的源地址和端口号,为接下来真正的连接做好了准备,这就是所谓的打洞,即frmClientB向frmClientA打了一个洞,下次frmClientA就能直接连接到frmClientB刚才使用的端口号了。
7、 客户端frmClientB打洞的同时在相同的端口上启动侦听。frmClientB在一切准备就绪以后通过与frmServer的主连接回复消息“可以了,已经准备”,frmServer在收到以后将frmClientB经过NAT-B转换后的公网IP和端口号告诉给frmClientA。
8、 frmClientA收到frmServer回复的frmClientB的公网IP和端口号等信息以后,开始连接到frmClientB公网IP和端口号,由于在步骤6中frmClientB曾经尝试连接过frmClientA的公网IP地址和端口,NAT-A纪录了此次连接的信息,所以当frmClientA主动连接frmClientB时,NAT-B会认为是合法的SYN数据,并允许通过,从而直接的udp连接建立起来了。
- frmClientB
客户端核心代码:
1 private void Run() 2 { 3 try 4 { 5 byte[] buffer;//接受数据用 6 while (true) 7 { 8 buffer = _client.Receive(ref _remotePoint);//_remotePoint变量返回当前连接的用户IP地址 9 10 object msgObj = ObjectSerializer.Deserialize(buffer); 11 Type msgType = msgObj.GetType(); 12 DoWriteLog("接收到消息:" + msgType.ToString() + " From:" + _remotePoint.ToString()); 13 14 if (msgType == typeof(S2C_UserListMessage)) 15 { 16 // 更新用户列表 17 S2C_UserListMessage usersMsg = (S2C_UserListMessage)msgObj; 18 _userList.Clear(); 19 20 foreach (User user in usersMsg.UserList) 21 _userList.Add(user); 22 23 this.DisplayUsers(_userList); 24 } 25 else if (msgType == typeof(S2C_UserAction)) 26 { 27 //用户动作,新用户登录/用户登出 28 S2C_UserAction msgAction = (S2C_UserAction)msgObj; 29 if (msgAction.Action == UserAction.Login) 30 { 31 _userList.Add(msgAction.User); 32 this.DisplayUsers(_userList); 33 } 34 else if (msgAction.Action == UserAction.Logout) 35 { 36 User user = _userList.Find(msgAction.User.UserName); 37 if (user != null) _userList.Remove(user); 38 this.DisplayUsers(_userList); 39 } 40 } 41 else if (msgType == typeof(S2C_HolePunchingMessage)) 42 { 43 //接受到服务器的打洞命令 44 S2C_HolePunchingMessage msgHolePunching = (S2C_HolePunchingMessage)msgObj; 45 46 //NAT-B的用户给NAT-A的用户发送消息,此时UDP包肯定会被NAT-A丢弃, 47 //因为NAT-A上并没有A->NAT-B的合法Session, 但是现在NAT-B上就建立了有B->NAT-A的合法session了! 48 P2P_HolePunchingTestMessage msgTest = new P2P_HolePunchingTestMessage(_LocalUserName); 49 this.SendMessage(msgTest, msgHolePunching.RemotePoint); 50 } 51 else if (msgType == typeof(P2P_HolePunchingTestMessage)) 52 { 53 //UDP打洞测试消息 54 //_HoleAccepted = true; 55 P2P_HolePunchingTestMessage msgTest = (P2P_HolePunchingTestMessage)msgObj; 56 UpdateConnection(msgTest.UserName, _remotePoint); 57 58 //发送确认消息 59 P2P_HolePunchingResponse response = new P2P_HolePunchingResponse(_LocalUserName); 60 this.SendMessage(response, _remotePoint); 61 } 62 else if (msgType == typeof(P2P_HolePunchingResponse)) 63 { 64 //_HoleAccepted = true;//打洞成功 65 P2P_HolePunchingResponse msg = msgObj as P2P_HolePunchingResponse; 66 UpdateConnection(msg.UserName, _remotePoint); 67 68 } 69 else if (msgType == typeof(P2P_TalkMessage)) 70 { 71 //用户间对话消息 72 P2P_TalkMessage workMsg = (P2P_TalkMessage)msgObj; 73 DoWriteLog(workMsg.Message); 74 } 75 else 76 { 77 DoWriteLog("收到未知消息!"); 78 } 79 } 80 } 81 catch (Exception ex) { DoWriteLog(ex.Message); } 82 }