思路如下(參照源代碼):
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/
