這個(gè)標(biāo)題用了兩個(gè)頓號(hào)三個(gè)名稱,其實(shí)說(shuō)得是同一個(gè)東西,只是網(wǎng)上有不同的說(shuō)法罷了,另外好像還有人叫TCP打孔(我的朋友小妞聽(tīng)說(shuō)后問(wèn)“要打孔啊,要不要我?guī)湍闳ソ鑲€(gè)電鉆過(guò)來(lái)啊?”“~!·¥%……·!”)。

閑話少說(shuō),我們先看一下技術(shù)背景:
Internet的迅速發(fā)展以及IPv4 地址數(shù)量的限制使得網(wǎng)絡(luò)地址翻譯(NAT,Network Address Trans2lation)設(shè)備得到廣泛應(yīng)用。NAT設(shè)備允許處于同一NAT后的多臺(tái)主機(jī)共享一個(gè)公網(wǎng)(本文將處于同一NAT后的網(wǎng)絡(luò)稱為私網(wǎng),處于NAT前的網(wǎng)絡(luò)稱為公網(wǎng)) IP 地址。一個(gè)私網(wǎng)IP 地址通過(guò)NAT設(shè)備與公網(wǎng)的其他主機(jī)通信。公網(wǎng)和私網(wǎng)IP地址域,如下圖所示:

廣域網(wǎng)與私網(wǎng)示意圖

一般來(lái)說(shuō)都是由私網(wǎng)內(nèi)主機(jī)(例如上圖中“電腦A-01”)主動(dòng)發(fā)起連接,數(shù)據(jù)包經(jīng)過(guò)NAT地址轉(zhuǎn)換后送給公網(wǎng)上的服務(wù)器(例如上圖中的“Server”),連接建立以后可雙向傳送數(shù)據(jù),NAT設(shè)備允許私網(wǎng)內(nèi)主機(jī)主動(dòng)向公網(wǎng)內(nèi)主機(jī)發(fā)送數(shù)據(jù),但卻禁止反方向的主動(dòng)傳遞,但在一些特殊的場(chǎng)合需要不同私網(wǎng)內(nèi)的主機(jī)進(jìn)行互聯(lián)(例如P2P軟件、網(wǎng)絡(luò)會(huì)議、視頻傳輸?shù)龋琓CP穿越NAT的問(wèn)題必須解決。網(wǎng)上關(guān)于UDP穿越NAT的文章很多,而且還有配套源代碼,但是我個(gè)人認(rèn)為UDP數(shù)據(jù)雖然速度快,但是沒(méi)有保障,而且NAT為UDP準(zhǔn)備的臨時(shí)端口號(hào)有生命周期的限制,使用起來(lái)不夠方便,在需要保證傳輸質(zhì)量的應(yīng)用上TCP連接還是首選(例如:文件傳輸)。
網(wǎng)上也有不少關(guān)于TCP穿越NAT(即TCP打洞)的介紹文章,但不幸我還沒(méi)找到相關(guān)的源代碼可以參考,我利用空余時(shí)間寫了一個(gè)可以實(shí)現(xiàn)TCP穿越NAT,讓不同的私網(wǎng)內(nèi)主機(jī)建立直接的TCP通信的源代碼。

這里需要介紹一下NAT的類型:
NAT設(shè)備的類型對(duì)于TCP穿越NAT,有著十分重要的影響,根據(jù)端口映射方式,NAT可分為如下4類,前3種NAT類型可統(tǒng)稱為cone類型。
(1)全克隆( Full Cone) : NAT把所有來(lái)自相同內(nèi)部IP地址和端口的請(qǐng)求映射到相同的外部IP地址和端口。任何一個(gè)外部主機(jī)均可通過(guò)該映射發(fā)送IP包到該內(nèi)部主機(jī)。
(2)限制性克隆(Restricted Cone) : NAT把所有來(lái)自相同內(nèi)部IP地址和端口的請(qǐng)求映射到相同的外部IP地址和端口。但是,只有當(dāng)內(nèi)部主機(jī)先給IP地址為X的外部主機(jī)發(fā)送IP包,該外部主機(jī)才能向該內(nèi)部主機(jī)發(fā)送IP包。
(3)端口限制性克隆( Port Restricted Cone) :端口限制性克隆與限制性克隆類似,只是多了端口號(hào)的限制,即只有內(nèi)部主機(jī)先向IP地址為X,端口號(hào)為P的外部主機(jī)發(fā)送1個(gè)IP包,該外部主機(jī)才能夠把源端口號(hào)為P的IP包發(fā)送給該內(nèi)部主機(jī)。
(4)對(duì)稱式NAT ( Symmetric NAT) :這種類型的NAT與上述3種類型的不同,在于當(dāng)同一內(nèi)部主機(jī)使用相同的端口與不同地址的外部主機(jī)進(jìn)行通信時(shí), NAT對(duì)該內(nèi)部主機(jī)的映射會(huì)有所不同。對(duì)稱式NAT不保證所有會(huì)話中的私有地址和公開(kāi)IP之間綁定的一致性。相反,它為每個(gè)新的會(huì)話分配一個(gè)新的端口號(hào)。

我們先假設(shè)一下:有一個(gè)服務(wù)器S在公網(wǎng)上有一個(gè)IP,兩個(gè)私網(wǎng)分別由NAT-A和NAT-B連接到公網(wǎng),NAT-A后面有一臺(tái)客戶端A,NAT-B后面有一臺(tái)客戶端B,現(xiàn)在,我們需要借助S將A和B建立直接的TCP連接,即由B向A打一個(gè)洞,讓A可以沿這個(gè)洞直接連接到B主機(jī),就好像NAT-B不存在一樣。
實(shí)現(xiàn)過(guò)程如下(請(qǐng)參照源代碼):
1、 S啟動(dòng)兩個(gè)網(wǎng)絡(luò)偵聽(tīng),一個(gè)叫【主連接】偵聽(tīng),一個(gè)叫【協(xié)助打洞】的偵聽(tīng)。
2、 A和B分別與S的【主連接】保持聯(lián)系。
3、 當(dāng)A需要和B建立直接的TCP連接時(shí),首先連接S的【協(xié)助打洞】端口,并發(fā)送協(xié)助連接申請(qǐng)。同時(shí)在該端口號(hào)上啟動(dòng)偵聽(tīng)。注意由于要在相同的網(wǎng)絡(luò)終端上綁定到不同的套接字上,所以必須為這些套接字設(shè)置 SO_REUSEADDR 屬性(即允許重用),否則偵聽(tīng)會(huì)失敗。
4、 S的【協(xié)助打洞】連接收到A的申請(qǐng)后通過(guò)【主連接】通知B,并將A經(jīng)過(guò)NAT-A轉(zhuǎn)換后的公網(wǎng)IP地址和端口等信息告訴B。
5、 B收到S的連接通知后首先與S的【協(xié)助打洞】端口連接,隨便發(fā)送一些數(shù)據(jù)后立即斷開(kāi),這樣做的目的是讓S能知道B經(jīng)過(guò)NAT-B轉(zhuǎn)換后的公網(wǎng)IP和端口號(hào)。
6、 B嘗試與A的經(jīng)過(guò)NAT-A轉(zhuǎn)換后的公網(wǎng)IP地址和端口進(jìn)行connect,根據(jù)不同的路由器會(huì)有不同的結(jié)果,有些路由器在這個(gè)操作就能建立連接(例如我用的TPLink R402),大多數(shù)路由器對(duì)于不請(qǐng)自到的SYN請(qǐng)求包直接丟棄而導(dǎo)致connect失敗,但NAT-A會(huì)紀(jì)錄此次連接的源地址和端口號(hào),為接下來(lái)真正的連接做好了準(zhǔn)備,這就是所謂的打洞,即B向A打了一個(gè)洞,下次A就能直接連接到B剛才使用的端口號(hào)了。
7、 客戶端B打洞的同時(shí)在相同的端口上啟動(dòng)偵聽(tīng)。B在一切準(zhǔn)備就緒以后通過(guò)與S的【主連接】回復(fù)消息“我已經(jīng)準(zhǔn)備好”,S在收到以后將B經(jīng)過(guò)NAT-B轉(zhuǎn)換后的公網(wǎng)IP和端口號(hào)告訴給A。
8、 A收到S回復(fù)的B的公網(wǎng)IP和端口號(hào)等信息以后,開(kāi)始連接到B公網(wǎng)IP和端口號(hào),由于在步驟6中B曾經(jīng)嘗試連接過(guò)A的公網(wǎng)IP地址和端口,NAT-A紀(jì)錄了此次連接的信息,所以當(dāng)A主動(dòng)連接B時(shí),NAT-B會(huì)認(rèn)為是合法的SYN數(shù)據(jù),并允許通過(guò),從而直接的TCP連接建立起來(lái)了。

整個(gè)實(shí)現(xiàn)過(guò)程靠文字恐怕很難講清楚,再加上我的語(yǔ)言表達(dá)能力很差(高考語(yǔ)文才考75分,總分150分,慚愧),所以只好用代碼來(lái)說(shuō)明問(wèn)題了。

//?服務(wù)器地址和端口號(hào)定義
#define?SRV_TCP_MAIN_PORT????????4000????//?服務(wù)器主連接的端口號(hào)
#define?SRV_TCP_HOLE_PORT????????8000????//?服務(wù)器響應(yīng)客戶端打洞申請(qǐng)的端口號(hào)

這兩個(gè)端口是固定的,服務(wù)器S啟動(dòng)時(shí)就開(kāi)始偵聽(tīng)這兩個(gè)端口了。

//
//?將新客戶端登錄信息發(fā)送給所有已登錄的客戶端,但不發(fā)送給自己
//
BOOL?SendNewUserLoginNotifyToAll?(?LPCTSTR?lpszClientIP,?UINT?nClientPort,?DWORD?dwID?)
{
????ASSERT?(?lpszClientIP?
&&?nClientPort?>?0?);
????g_CSFor_PtrAry_SockClient.Lock();
????
for?(?int?i=0;?i<g_PtrAry_SockClient.GetSize();?i++?)
????
{
????????CSockClient?
*pSockClient?=?(CSockClient*)g_PtrAry_SockClient.GetAt(i);
????????
if?(?pSockClient?&&?pSockClient->m_bMainConn?&&?pSockClient->m_dwID?>?0?&&?pSockClient->m_dwID?!=?dwID?)
????????
{
????????????
if?(?!pSockClient->SendNewUserLoginNotify?(?lpszClientIP,?nClientPort,?dwID?)?)
????????????
{
????????????????g_CSFor_PtrAry_SockClient.Unlock();
????????????????
return?FALSE;
????????????}

????????}

????}


????g_CSFor_PtrAry_SockClient.Unlock?();
????
return?TRUE;
}

當(dāng)有新的客戶端連接到服務(wù)器時(shí),服務(wù)器負(fù)責(zé)將該客戶端的信息(IP地址、端口號(hào))發(fā)送給其他客戶端。

//
//?執(zhí)行者:客戶端A
//?有新客戶端B登錄了,我(客戶端A)連接服務(wù)器端口?SRV_TCP_HOLE_PORT?,申請(qǐng)與客戶端B建立直接的TCP連接
//
BOOL?Handle_NewUserLogin?(?CSocket?&MainSock,?t_NewUserLoginPkt?*pNewUserLoginPkt?)
{
????printf?(?
"New?user?(?%s:%u:%u?)?login?server\n",?pNewUserLoginPkt->szClientIP,
????????pNewUserLoginPkt
->nClientPort,?pNewUserLoginPkt->dwID?);

????BOOL?bRet?
=?FALSE;
????DWORD?dwThreadID?
=?0;
????t_ReqConnClientPkt?ReqConnClientPkt;
????CSocket?Sock;
????CString?csSocketAddress;
????
char?szRecvBuffer[NET_BUFFER_SIZE]?=?{0};
????
int?nRecvBytes?=?0;
????
//?創(chuàng)建打洞Socket,連接服務(wù)器協(xié)助打洞的端口號(hào)?SRV_TCP_HOLE_PORT
????try
????
{
????????
if?(?!Sock.Socket?()?)
????????
{
????????????printf?(?
"Create?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
goto?finished;
????????}

????????UINT?nOptValue?
=?1;
????????
if?(?!Sock.SetSockOpt?(?SO_REUSEADDR,?&nOptValue?,?sizeof(UINT)?)?)
????????
{
????????????printf?(?
"SetSockOpt?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
goto?finished;
????????}

????????
if?(?!Sock.Bind?(?0?)?)
????????
{
????????????printf?(?
"Bind?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
goto?finished;
????????}

????????
if?(?!Sock.Connect?(?g_pServerAddess,?SRV_TCP_HOLE_PORT?)?)
????????
{
????????????printf?(?
"Connect?to?[%s:%d]?failed?:?%s\n",?g_pServerAddess,?
????????????????SRV_TCP_HOLE_PORT,?hwFormatMessage(GetLastError())?);
????????????
goto?finished;
????????}

????}

????
catch?(?CException?e?)
????
{
????????
char?szError[255]?=?{0};
????????e.GetErrorMessage(?szError,?
sizeof(szError)?);
????????printf?(?
"Exception?occur,?%s\n",?szError?);
????????
goto?finished;
????}

????g_pSock_MakeHole?
=?&Sock;
????ASSERT?(?g_nHolePort?
==?0?);
????VERIFY?(?Sock.GetSockName?(?csSocketAddress,?g_nHolePort?)?);

????
//?創(chuàng)建一個(gè)線程來(lái)偵聽(tīng)端口?g_nHolePort?的連接請(qǐng)求
????dwThreadID?=?0;
????g_hThread_Listen?
=?::CreateThread?(?NULL,?0,?::ThreadProc_Listen,?LPVOID(NULL),?0,?&dwThreadID?);
????
if?(!HANDLE_IS_VALID(g_hThread_Listen)?)?return?FALSE;
????Sleep?(?
3000?);

????
//?我(客戶端A)向服務(wù)器協(xié)助打洞的端口號(hào)?SRV_TCP_HOLE_PORT?發(fā)送申請(qǐng),希望與新登錄的客戶端B建立連接
????
//?服務(wù)器會(huì)將我的打洞用的外部IP和端口號(hào)告訴客戶端B
????ASSERT?(?g_WelcomePkt.dwID?>?0?);
????ReqConnClientPkt.dwInviterID?
=?g_WelcomePkt.dwID;
????ReqConnClientPkt.dwInvitedID?
=?pNewUserLoginPkt->dwID;
????
if?(?Sock.Send?(?&ReqConnClientPkt,?sizeof(t_ReqConnClientPkt)?)?!=?sizeof(t_ReqConnClientPkt)?)
????????
goto?finished;

????
//?等待服務(wù)器回應(yīng),將客戶端B的外部IP地址和端口號(hào)告訴我(客戶端A)
????nRecvBytes?=?Sock.Receive?(?szRecvBuffer,?sizeof(szRecvBuffer)?);
????
if?(?nRecvBytes?>?0?)
????
{
????????ASSERT?(?nRecvBytes?
==?sizeof(t_SrvReqDirectConnectPkt)?);
????????PACKET_TYPE?
*pePacketType?=?(PACKET_TYPE*)szRecvBuffer;
????????ASSERT?(?pePacketType?
&&?*pePacketType?==?PACKET_TYPE_TCP_DIRECT_CONNECT?);
????????Sleep?(?
1000?);
????????Handle_SrvReqDirectConnect?(?(t_SrvReqDirectConnectPkt
*)szRecvBuffer?);
????????printf?(?
"Handle_SrvReqDirectConnect?end\n"?);
????}

????
//?對(duì)方斷開(kāi)連接了
????else
????
{
????????
goto?finished;
????}

????
????bRet?
=?TRUE;
finished:
????g_pSock_MakeHole?
=?NULL;
????
return?bRet;

}

這里假設(shè)客戶端A先啟動(dòng),當(dāng)客戶端B啟動(dòng)后客戶端A將收到服務(wù)器S的新客戶端登錄的通知,并得到客戶端B的公網(wǎng)IP和端口,客戶端A啟動(dòng)線程連接S的【協(xié)助打洞】端口(本地端口號(hào)可以用GetSocketName()函數(shù)取得,假設(shè)為M),請(qǐng)求S協(xié)助TCP打洞,然后啟動(dòng)線程偵聽(tīng)該本地端口(前面假設(shè)的M)上的連接請(qǐng)求,然后等待服務(wù)器的回應(yīng)。

//
//?客戶端A請(qǐng)求我(服務(wù)器)協(xié)助連接客戶端B,這個(gè)包應(yīng)該在打洞Socket中收到
//
BOOL?CSockClient::Handle_ReqConnClientPkt(t_ReqConnClientPkt?*pReqConnClientPkt)
{
????ASSERT?(?
!m_bMainConn?);
????CSockClient?
*pSockClient_B?=?FindSocketClient?(?pReqConnClientPkt->dwInvitedID?);
????
if?(?!pSockClient_B?)?return?FALSE;
????printf?(?
"%s:%u:%u?invite?%s:%u:%u?connection\n",?m_csPeerAddress,?m_nPeerPort,?m_dwID,
????????pSockClient_B
->m_csPeerAddress,?pSockClient_B->m_nPeerPort,?pSockClient_B->m_dwID?);

????
//?客戶端A想要和客戶端B建立直接的TCP連接,服務(wù)器負(fù)責(zé)將A的外部IP和端口號(hào)告訴給B
????t_SrvReqMakeHolePkt?SrvReqMakeHolePkt;
????SrvReqMakeHolePkt.dwInviterID?
=?pReqConnClientPkt->dwInviterID;
????SrvReqMakeHolePkt.dwInviterHoleID?
=?m_dwID;
????SrvReqMakeHolePkt.dwInvitedID?
=?pReqConnClientPkt->dwInvitedID;
????STRNCPY_CS?(?SrvReqMakeHolePkt.szClientHoleIP,?m_csPeerAddress?);
????SrvReqMakeHolePkt.nClientHolePort?
=?m_nPeerPort;
????
if?(?pSockClient_B->SendChunk?(?&SrvReqMakeHolePkt,?sizeof(t_SrvReqMakeHolePkt),?0?)?!=?sizeof(t_SrvReqMakeHolePkt)?)
????????
return?FALSE;

????
//?等待客戶端B打洞完成,完成以后通知客戶端A直接連接客戶端外部IP和端口號(hào)
????if?(?!HANDLE_IS_VALID(m_hEvtWaitClientBHole)?)
????????
return?FALSE;
????
if?(?WaitForSingleObject?(?m_hEvtWaitClientBHole,?6000*1000?)?==?WAIT_OBJECT_0?)
????
{
????????
if?(?SendChunk?(?&m_SrvReqDirectConnectPkt,?sizeof(t_SrvReqDirectConnectPkt),?0?)?
????????????????
==?sizeof(t_SrvReqDirectConnectPkt)?)
????????????
return?TRUE;
????}


????
return?FALSE;
}

服務(wù)器S收到客戶端A的協(xié)助打洞請(qǐng)求后通知客戶端B,要求客戶端B向客戶端A打洞,即讓客戶端B嘗試與客戶端A的公網(wǎng)IP和端口進(jìn)行connect。

//
//?執(zhí)行者:客戶端B
//?處理服務(wù)器要我(客戶端B)向另外一個(gè)客戶端(A)打洞,打洞操作在線程中進(jìn)行。
//?先連接服務(wù)器協(xié)助打洞的端口號(hào)?SRV_TCP_HOLE_PORT?,通過(guò)服務(wù)器告訴客戶端A我(客戶端B)的外部IP地址和端口號(hào),然后啟動(dòng)線程進(jìn)行打洞,
//?客戶端A在收到這些信息以后會(huì)發(fā)起對(duì)我(客戶端B)的外部IP地址和端口號(hào)的連接(這個(gè)連接在客戶端B打洞完成以后進(jìn)行,所以
//?客戶端B的NAT不會(huì)丟棄這個(gè)SYN包,從而連接能建立)
//
BOOL?Handle_SrvReqMakeHole?(?CSocket?&MainSock,?t_SrvReqMakeHolePkt?*pSrvReqMakeHolePkt?)
{
????ASSERT?(?pSrvReqMakeHolePkt?);
????
//?創(chuàng)建Socket,連接服務(wù)器協(xié)助打洞的端口號(hào)?SRV_TCP_HOLE_PORT,連接建立以后發(fā)送一個(gè)斷開(kāi)連接的請(qǐng)求給服務(wù)器,然后連接斷開(kāi)
????
//?這里連接的目的是讓服務(wù)器知道我(客戶端B)的外部IP地址和端口號(hào),以通知客戶端A
????CSocket?Sock;
????
try
????
{
????????
if?(?!Sock.Create?()?)
????????
{
????????????printf?(?
"Create?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
return?FALSE;
????????}

????????
if?(?!Sock.Connect?(?g_pServerAddess,?SRV_TCP_HOLE_PORT?)?)
????????
{
????????????printf?(?
"Connect?to?[%s:%d]?failed?:?%s\n",?g_pServerAddess,?
????????????????SRV_TCP_HOLE_PORT,?hwFormatMessage(GetLastError())?);
????????????
return?FALSE;
????????}

????}

????
catch?(?CException?e?)
????
{
????????
char?szError[255]?=?{0};
????????e.GetErrorMessage(?szError,?
sizeof(szError)?);
????????printf?(?
"Exception?occur,?%s\n",?szError?);
????????
return?FALSE;
????}


????CString?csSocketAddress;
????ASSERT?(?g_nHolePort?
==?0?);
????VERIFY?(?Sock.GetSockName?(?csSocketAddress,?g_nHolePort?)?);

????
//?連接服務(wù)器協(xié)助打洞的端口號(hào)?SRV_TCP_HOLE_PORT,發(fā)送一個(gè)斷開(kāi)連接的請(qǐng)求,然后將連接斷開(kāi),服務(wù)器在收到這個(gè)包的時(shí)候也會(huì)將
????
//?連接斷開(kāi)
????t_ReqSrvDisconnectPkt?ReqSrvDisconnectPkt;
????ReqSrvDisconnectPkt.dwInviterID?
=?pSrvReqMakeHolePkt->dwInvitedID;
????ReqSrvDisconnectPkt.dwInviterHoleID?
=?pSrvReqMakeHolePkt->dwInviterHoleID;
????ReqSrvDisconnectPkt.dwInvitedID?
=?pSrvReqMakeHolePkt->dwInvitedID;
????ASSERT?(?ReqSrvDisconnectPkt.dwInvitedID?
==?g_WelcomePkt.dwID?);
????
if?(?Sock.Send?(?&ReqSrvDisconnectPkt,?sizeof(t_ReqSrvDisconnectPkt)?)?!=?sizeof(t_ReqSrvDisconnectPkt)?)
????????
return?FALSE;
????Sleep?(?
100?);
????Sock.Close?();

????
//?創(chuàng)建一個(gè)線程來(lái)向客戶端A的外部IP地址、端口號(hào)打洞
????t_SrvReqMakeHolePkt?*pSrvReqMakeHolePkt_New?=?new?t_SrvReqMakeHolePkt;
????
if?(?!pSrvReqMakeHolePkt_New?)?return?FALSE;
????memcpy?(?pSrvReqMakeHolePkt_New,?pSrvReqMakeHolePkt,?
sizeof(t_SrvReqMakeHolePkt)?);
????DWORD?dwThreadID?
=?0;
????g_hThread_MakeHole?
=?::CreateThread?(?NULL,?0,?::ThreadProc_MakeHole,?
????????LPVOID(pSrvReqMakeHolePkt_New),?
0,?&dwThreadID?);
????
if?(!HANDLE_IS_VALID(g_hThread_MakeHole)?)?return?FALSE;

????
//?創(chuàng)建一個(gè)線程來(lái)偵聽(tīng)端口?g_nHolePort?的連接請(qǐng)求
????dwThreadID?=?0;
????g_hThread_Listen?
=?::CreateThread?(?NULL,?0,?::ThreadProc_Listen,?LPVOID(NULL),?0,?&dwThreadID?);
????
if?(!HANDLE_IS_VALID(g_hThread_Listen)?)?return?FALSE;

????
//?等待打洞和偵聽(tīng)完成
????HANDLE?hEvtAry[]?=?{?g_hEvt_ListenFinished,?g_hEvt_MakeHoleFinished?};
????
if?(?::WaitForMultipleObjects?(?LENGTH(hEvtAry),?hEvtAry,?TRUE,?30*1000?)?==?WAIT_TIMEOUT?)
????????
return?FALSE;
????t_HoleListenReadyPkt?HoleListenReadyPkt;
????HoleListenReadyPkt.dwInvitedID?
=?pSrvReqMakeHolePkt->dwInvitedID;
????HoleListenReadyPkt.dwInviterHoleID?
=?pSrvReqMakeHolePkt->dwInviterHoleID;
????HoleListenReadyPkt.dwInvitedID?
=?pSrvReqMakeHolePkt->dwInvitedID;
????
if?(?MainSock.Send?(?&HoleListenReadyPkt,?sizeof(t_HoleListenReadyPkt)?)?!=?sizeof(t_HoleListenReadyPkt)?)
????
{
????????printf?(?
"Send?HoleListenReadyPkt?to?%s:%u?failed?:?%s\n",?
????????g_WelcomePkt.szClientIP,?g_WelcomePkt.nClientPort,
????????????hwFormatMessage(GetLastError())?);
????????
return?FALSE;
????}

????
????
return?TRUE;
}

客戶端B收到服務(wù)器S的打洞通知后,先連接S的【協(xié)助打洞】端口號(hào)(本地端口號(hào)可以用GetSocketName()函數(shù)取得,假設(shè)為X),啟動(dòng)線程嘗試連接客戶端A的公網(wǎng)IP和端口號(hào),根據(jù)路由器不同,連接情況各異,如果運(yùn)氣好直接連接就成功了,即使連接失敗,但打洞便完成了。同時(shí)還要啟動(dòng)線程在相同的端口(即與S的【協(xié)助打洞】端口號(hào)建立連接的本地端口號(hào)X)上偵聽(tīng)到來(lái)的連接,等待客戶端A直接連接該端口號(hào)。

//
//?執(zhí)行者:客戶端A
//?服務(wù)器要求主動(dòng)端(客戶端A)直接連接被動(dòng)端(客戶端B)的外部IP和端口號(hào)
//
BOOL?Handle_SrvReqDirectConnect?(?t_SrvReqDirectConnectPkt?*pSrvReqDirectConnectPkt?)
{
????ASSERT?(?pSrvReqDirectConnectPkt?);
????printf?(?
"You?can?connect?direct?to?(?IP:%s??PORT:%d??ID:%u?)\n",?pSrvReqDirectConnectPkt->szInvitedIP,
????????pSrvReqDirectConnectPkt
->nInvitedPort,?pSrvReqDirectConnectPkt->dwInvitedID?);

????
//?直接與客戶端B建立TCP連接,如果連接成功說(shuō)明TCP打洞已經(jīng)成功了。
????CSocket?Sock;
????
try
????
{
????????
if?(?!Sock.Socket?()?)
????????
{
????????????printf?(?
"Create?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
return?FALSE;
????????}

????????UINT?nOptValue?
=?1;
????????
if?(?!Sock.SetSockOpt?(?SO_REUSEADDR,?&nOptValue?,?sizeof(UINT)?)?)
????????
{
????????????printf?(?
"SetSockOpt?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
return?FALSE;
????????}

????????
if?(?!Sock.Bind?(?g_nHolePort?)?)
????????
{
????????????printf?(?
"Bind?socket?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????
return?FALSE;
????????}

????????
for?(?int?ii=0;?ii<100;?ii++?)
????????
{
????????????
if?(?WaitForSingleObject?(?g_hEvt_ConnectOK,?0?)?==?WAIT_OBJECT_0?)
????????????????
break;
????????????DWORD?dwArg?
=?1;
????????????
if?(?!Sock.IOCtl?(?FIONBIO,?&dwArg?)?)
????????????
{
????????????????printf?(?
"IOCtl?failed?:?%s\n",?hwFormatMessage(GetLastError())?);
????????????}

????????????
if?(?!Sock.Connect?(?pSrvReqDirectConnectPkt->szInvitedIP,?pSrvReqDirectConnectPkt->nInvitedPort?)?)
????????????
{
????????????????printf?(?
"Connect?to?[%s:%d]?failed?:?%s\n",?
????????????????????pSrvReqDirectConnectPkt
->szInvitedIP,?
????????????????????pSrvReqDirectConnectPkt
->nInvitedPort,?
????????????????????hwFormatMessage(GetLastError())?);
????????????????????Sleep?(
100);
????????????}

????????????
else?break;
????????}

????????
if?(?WaitForSingleObject?(?g_hEvt_ConnectOK,?0?)?!=?WAIT_OBJECT_0?)
????????
{
????????????
if?(?HANDLE_IS_VALID?(?g_hEvt_ConnectOK?)?)?SetEvent?(?g_hEvt_ConnectOK?);
????????????printf?(?
"Connect?to?[%s:%d]?successfully?!!!\n",?
????????????pSrvReqDirectConnectPkt
->szInvitedIP,?pSrvReqDirectConnectPkt->nInvitedPort?);
????????????
????????????
//?接收測(cè)試數(shù)據(jù)
????????????printf?(?"Receiving?data?\n"?);
????????????
char?szRecvBuffer[NET_BUFFER_SIZE]?=?{0};
????????????
int?nRecvBytes?=?0;
????????????
for?(?int?i=0;?i<1000;?i++?)
????????????
{
????????????????nRecvBytes?
=?Sock.Receive?(?szRecvBuffer,?sizeof(szRecvBuffer)?);
????????????????
if?(?nRecvBytes?>?0?)
????????????????
{
????????????????????printf?(?
"-->>>?Received?Data?:?%s\n",?szRecvBuffer?);
????????????????????memset?(?szRecvBuffer,?
0,?sizeof(szRecvBuffer)?);
????????????????????SLEEP_BREAK?(?
1?);
????????????????}

????????????????
else
????????????????
{
????????????????????SLEEP_BREAK?(?
300?);
????????????????}

????????????}

????????}

????}

????
catch?(?CException?e?)
????
{
????????
char?szError[255]?=?{0};
????????e.GetErrorMessage(?szError,?
sizeof(szError)?);
????????printf?(?
"Exception?occur,?%s\n",?szError?);
????????
return?FALSE;
????}


????
return?TRUE;
}

在客戶端B打洞和偵聽(tīng)準(zhǔn)備好以后,服務(wù)器S回復(fù)客戶端A,客戶端A便直接與客戶端B的公網(wǎng)IP和端口進(jìn)行連接,收發(fā)數(shù)據(jù)可以正常進(jìn)行,為了測(cè)試是否真正地直接TCP連接,在數(shù)據(jù)收發(fā)過(guò)程中可以將服務(wù)器S強(qiáng)行終止,看是否數(shù)據(jù)收發(fā)還正常進(jìn)行著。

程序執(zhí)行步驟和方法:

  1. 要準(zhǔn)備好環(huán)境,如果要真實(shí)測(cè)試的話需要用2個(gè)連到公網(wǎng)上的局域網(wǎng),1臺(tái)具有公網(wǎng)地址的電腦(為了協(xié)助我測(cè)試,小曹、小妞可費(fèi)了不少心,我還霸占了他們家的電腦,在此表示感謝)。如果不是這樣的環(huán)境,程序執(zhí)行可能會(huì)不正常,因?yàn)槲視簳r(shí)未做相同局域網(wǎng)的處理。
  2. 在具有公網(wǎng)地址的電腦上執(zhí)行“TcpHoleSrv.exe”程序,假設(shè)這臺(tái)電腦的公網(wǎng)IP地址是“129.208.12.38”。
  3. 在局域網(wǎng)A中的一臺(tái)電腦上執(zhí)行“TcpHoleClt-A.exe 129.208.12.38”
  4. 在局域網(wǎng)B中的一臺(tái)電腦上執(zhí)行“TcpHoleClt-B.exe 129.208.12.38”

程序執(zhí)行成功后的界面:客戶端出現(xiàn)“Send Data”或者“Received Data”表示穿越NAT的TCP連接已經(jīng)建立起來(lái),數(shù)據(jù)收發(fā)已經(jīng)OK。


服務(wù)器S

客戶端A

客戶端B

本代碼在Windows XP、一個(gè)天威局域網(wǎng)、一個(gè)電信局域網(wǎng)、一個(gè)電話撥號(hào)網(wǎng)絡(luò)中測(cè)試通過(guò)。
由于時(shí)間和水平的關(guān)系,代碼和文章寫得都不咋的,但愿能起到拋磚引玉的作用。代碼部分只是實(shí)現(xiàn)了不同局域網(wǎng)之間的客戶端相互連接的問(wèn)題,至于相同局域網(wǎng)內(nèi)的主機(jī)或者其中一臺(tái)客戶端本身就具有公網(wǎng)IP的問(wèn)題這里暫時(shí)未做考慮(因?yàn)槟切┨幚韺?shí)在太簡(jiǎn)單了,比較一下掩碼或者公網(wǎng)IP就能判斷出來(lái)的);另外程序的防錯(cuò)性代碼重用性也做得不好,只是實(shí)現(xiàn)了功能