0%

思路如下(参照源代码):

  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 }

View Code

  • 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 }

View Code

  • frmServer

 

如转载请注明本文来自易学网http://www.vjsdn.com/