思路如下(参照源代码):
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 }
- frmClientA
服务端核心代码:

1 private void Run() 2 { 3 byte[] msgBuffer = null; 4 5 while (true) 6 { 7 msgBuffer = _server.Receive(ref _remotePoint); //接受消息 8 try 9 { 10 //将消息转换为对象 11 object msgObject = ObjectSerializer.Deserialize(msgBuffer); 12 if (msgObject == null) continue; 13 14 Type msgType = msgObject.GetType(); 15 DoWriteLog("接收到消息:" + msgType.ToString()); 16 DoWriteLog("From:" + _remotePoint.ToString()); 17 18 //新用户登录 19 if (msgType == typeof(C2S_LoginMessage)) 20 { 21 C2S_LoginMessage lginMsg = (C2S_LoginMessage)msgObject; 22 DoWriteLog(string.Format("用户’{0}’已登录!", lginMsg.FromUserName)); 23 24 // 添加用户到列表 25 IPEndPoint userEndPoint = new IPEndPoint(_remotePoint.Address, _remotePoint.Port); 26 User user = new User(lginMsg.FromUserName, userEndPoint); 27 _userList.Add(user); 28 29 this.DoUserChanged(_userList); 30 31 //通知所有人,有新用户登录 32 S2C_UserAction msgNewUser = new S2C_UserAction(user, UserAction.Login); 33 foreach (User u in _userList) 34 { 35 if (u.UserName == user.UserName) //如果是自己,发送所有在线用户列表 36 this.SendMessage(new S2C_UserListMessage(_userList), u.NetPoint); 37 else 38 this.SendMessage(msgNewUser, u.NetPoint); 39 } 40 } 41 else if (msgType == typeof(C2S_LogoutMessage)) 42 { 43 C2S_LogoutMessage lgoutMsg = (C2S_LogoutMessage)msgObject; 44 DoWriteLog(string.Format("用户’{0}’已登出!", lgoutMsg.FromUserName)); 45 46 // 从列表中删除用户 47 User logoutUser = _userList.Find(lgoutMsg.FromUserName); 48 if (logoutUser != null) _userList.Remove(logoutUser); 49 50 this.DoUserChanged(_userList); 51 52 //通知所有人,有用户登出 53 S2C_UserAction msgNewUser = new S2C_UserAction(logoutUser, UserAction.Logout); 54 foreach (User u in _userList) 55 this.SendMessage(msgNewUser, u.NetPoint); 56 } 57 else if (msgType == typeof(C2S_HolePunchingRequestMessage)) 58 { 59 //接收到A给B打洞的消息,打洞请求,由客户端发送给服务器端 60 C2S_HolePunchingRequestMessage msgHoleReq = (C2S_HolePunchingRequestMessage)msgObject; 61 62 User userA = _userList.Find(msgHoleReq.FromUserName); 63 User userB = _userList.Find(msgHoleReq.ToUserName); 64 65 // 发送打洞(Punching Hole)消息 66 DoWriteLog(string.Format("用户:[{0} IP:{1}]想与[{2} IP:{3}]建立对话通道.", 67 userA.UserName, userA.NetPoint.ToString(), 68 userB.UserName, userB.NetPoint.ToString())); 69 70 //由Server发送消息给B,将A的IP的IP地址信息告诉B,然后由B发送一个测试消息给A. 71 S2C_HolePunchingMessage msgHolePunching = new S2C_HolePunchingMessage(_remotePoint); 72 this.SendMessage(msgHolePunching, userB.NetPoint); //Server->B 73 } 74 else if (msgType == typeof(C2S_GetUsersMessage)) 75 { 76 // 发送当前用户信息 77 S2C_UserListMessage srvResMsg = new S2C_UserListMessage(_userList); 78 this.SendMessage(srvResMsg, _remotePoint); 79 } 80 } 81 catch (Exception ex) { DoWriteLog(ex.Message); } 82 } 83 }
- frmServer
如转载请注明本文来自易学网http://www.vjsdn.com/