思路如下(參照源代碼):
1、 frmServer啟動兩個網(wǎng)絡(luò)偵聽,主連接偵聽,協(xié)助打洞的偵聽。
2、 frmClientA和frmClientB分別與frmServer的主連接保持聯(lián)系。
3、 當(dāng)frmClientA需要和frmClientB建立直接的udp連接時,首先連接frmServer的協(xié)助打洞端口,并發(fā)送協(xié)助連接申請,同時在該端口號上啟動偵聽。
4、 frmServer的協(xié)助打洞連接收到frmClientA的申請后通過主連接通知frmClientB,并將frmClientA經(jīng)過NAT-A轉(zhuǎn)換后的公網(wǎng)IP地址和端口等信息告訴frmClientB。
5、 frmClientB收到frmServer的連接通知后首先與frmServer的協(xié)助打洞端口連接,發(fā)送一些數(shù)據(jù)后立即斷開,目的是讓frmServer能知道frmClientB經(jīng)過NAT-B轉(zhuǎn)換后的公網(wǎng)IP和端口號。
6、 frmClientB嘗試與frmClientA的經(jīng)過NAT-A轉(zhuǎn)換后的公網(wǎng)IP地址和端口進行connect,不同的路由器會有不同的結(jié)果,多數(shù)路由器對未知不請自到的SYN請求包直接丟棄而導(dǎo)致connect失敗,但NAT-A會紀(jì)錄此次連接的源地址和端口號,為接下來真正的連接做好了準(zhǔn)備,這就是所謂的打洞,即frmClientB向frmClientA打了一個洞,下次frmClientA就能直接連接到frmClientB剛才使用的端口號了。
7、 客戶端frmClientB打洞的同時在相同的端口上啟動偵聽。frmClientB在一切準(zhǔn)備就緒以后通過與frmServer的主連接回復(fù)消息“可以了,已經(jīng)準(zhǔn)備”,frmServer在收到以后將frmClientB經(jīng)過NAT-B轉(zhuǎn)換后的公網(wǎng)IP和端口號告訴給frmClientA。
8、 frmClientA收到frmServer回復(fù)的frmClientB的公網(wǎng)IP和端口號等信息以后,開始連接到frmClientB公網(wǎng)IP和端口號,由于在步驟6中frmClientB曾經(jīng)嘗試連接過frmClientA的公網(wǎng)IP地址和端口,NAT-A紀(jì)錄了此次連接的信息,所以當(dāng)frmClientA主動連接frmClientB時,NAT-B會認(rèn)為是合法的SYN數(shù)據(jù),并允許通過,從而直接的udp連接建立起來了。
客戶端核心代碼:
private void Run()
{
try
{
byte[] buffer;//接受數(shù)據(jù)用
while (true)
{
buffer = _client.Receive(ref _remotePoint);//_remotePoint變量返回當(dāng)前連接的用戶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))
{
//接受到服務(wù)器的打洞命令
S2C_HolePunchingMessage msgHolePunching = (S2C_HolePunchingMessage)msgObj;
//NAT-B的用戶給NAT-A的用戶發(fā)送消息,此時UDP包肯定會被NAT-A丟棄,
//因為NAT-A上并沒有A->NAT-B的合法Session, 但是現(xiàn)在NAT-B上就建立了有B->NAT-A的合法session了!
P2P_HolePunchingTestMessage msgTest = new P2P_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);
//發(fā)送確認(rèn)消息
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); }
}
服務(wù)端核心代碼:
private void Run()
{
byte[] msgBuffer = null;
while (true)
{
msgBuffer = _server.Receive(ref _remotePoint); //接受消息
try
{
//將消息轉(zhuǎn)換為對象
object msgObject = ObjectSerializer.Deserialize(msgBuffer);
if (msgObject == null) continue;
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) //如果是自己,發(fā)送所有在線用戶列表
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打洞的消息,打洞請求,由客戶端發(fā)送給服務(wù)器端
C2S_HolePunchingRequestMessage msgHoleReq = (C2S_HolePunchingRequestMessage)msgObject;
User userA = _userList.Find(msgHoleReq.FromUserName);
User userB = _userList.Find(msgHoleReq.ToUserName);
// 發(fā)送打洞(Punching Hole)消息
DoWriteLog(string.Format("用戶:[{0} IP:{1}]想與[{2} IP:{3}]建立對話通道.",
userA.UserName, userA.NetPoint.ToString(),
userB.UserName, userB.NetPoint.ToString()));
//由Server發(fā)送消息給B,將A的IP的IP地址信息告訴B,然后由B發(fā)送一個測試消息給A.
S2C_HolePunchingMessage msgHolePunching = new S2C_HolePunchingMessage(_remotePoint);
this.SendMessage(msgHolePunching, userB.NetPoint); //Server->B
}
else if (msgType == typeof(C2S_GetUsersMessage))
{
// 發(fā)送當(dāng)前用戶信息
S2C_UserListMessage srvResMsg = new S2C_UserListMessage(_userList);
this.SendMessage(srvResMsg, _remotePoint);
}
}
catch (Exception ex) { DoWriteLog(ex.Message); }
}
}
該文章在 2018/2/10 10:08:37 編輯過