TCP實(shí)現P2P通信、TCP穿越NAT的方法、TCP打洞
作者:謝紅偉
下載源代碼
這個(gè)標題用了兩個(gè)頓號三個(gè)名稱(chēng),其實(shí)說(shuō)得是同一個(gè)東西,只是網(wǎng)上有不同的說(shuō)法罷了,另外好像還有人叫TCP打孔(我的朋友小妞聽(tīng)說(shuō)后問(wèn)“要打孔啊,要不要我幫你去借個(gè)電鉆過(guò)來(lái)???”“~!·¥%……·!”)。
閑話(huà)少說(shuō),我們先看一下技術(shù)背景:
Internet的迅速發(fā)展以及IPv4 地址數量的限制使得網(wǎng)絡(luò )地址翻譯(NAT,Network Address Trans2lation)設備得到廣泛應用。NAT設備允許處于同一NAT后的多臺主機共享一個(gè)公網(wǎng)(本文將處于同一NAT后的網(wǎng)絡(luò )稱(chēng)為私網(wǎng),處于NAT前的網(wǎng)絡(luò )稱(chēng)為公網(wǎng)) IP 地址。一個(gè)私網(wǎng)IP 地址通過(guò)NAT設備與公網(wǎng)的其他主機通信。公網(wǎng)和私網(wǎng)IP地址域,如下圖所示:

這里需要介紹一下NAT的類(lèi)型:
NAT設備的類(lèi)型對于TCP穿越NAT,有著(zhù)十分重要的影響,根據端口映射方式,NAT可分為如下4類(lèi),前3種NAT類(lèi)型可統稱(chēng)為cone類(lèi)型。
(1)全克隆( Full Cone) : NAT把所有來(lái)自相同內部IP地址和端口的請求映射到相同的外部IP地址和端口。任何一個(gè)外部主機均可通過(guò)該映射發(fā)送IP包到該內部主機。
(2)限制性克隆(Restricted Cone) : NAT把所有來(lái)自相同內部IP地址和端口的請求映射到相同的外部IP地址和端口。但是,只有當內部主機先給IP地址為X的外部主機發(fā)送IP包,該外部主機才能向該內部主機發(fā)送IP包。
(3)端口限制性克隆( Port Restricted Cone) :端口限制性克隆與限制性克隆類(lèi)似,只是多了端口號的限制,即只有內部主機先向IP地址為X,端口號為P的外部主機發(fā)送1個(gè)IP包,該外部主機才能夠把源端口號為P的IP包發(fā)送給該內部主機。
(4)對稱(chēng)式NAT ( Symmetric NAT) :這種類(lèi)型的NAT與上述3種類(lèi)型的不同,在于當同一內部主機使用相同的端口與不同地址的外部主機進(jìn)行通信時(shí), NAT對該內部主機的映射會(huì )有所不同。對稱(chēng)式NAT不保證所有會(huì )話(huà)中的私有地址和公開(kāi)IP之間綁定的一致性。相反,它為每個(gè)新的會(huì )話(huà)分配一個(gè)新的端口號。
我們先假設一下:有一個(gè)服務(wù)器S在公網(wǎng)上有一個(gè)IP,兩個(gè)私網(wǎng)分別由NAT-A和NAT-B連接到公網(wǎng),NAT-A后面有一臺客戶(hù)端A,NAT-B后面有一臺客戶(hù)端B,現在,我們需要借助S將A和B建立直接的TCP連接,即由B向A打一個(gè)洞,讓A可以沿這個(gè)洞直接連接到B主機,就好像NAT-B不存在一樣。
實(shí)現過(guò)程如下(請參照源代碼):
1、 S啟動(dòng)兩個(gè)網(wǎng)絡(luò )偵聽(tīng),一個(gè)叫【主連接】偵聽(tīng),一個(gè)叫【協(xié)助打洞】的偵聽(tīng)。
2、 A和B分別與S的【主連接】保持聯(lián)系。
3、 當A需要和B建立直接的TCP連接時(shí),首先連接S的【協(xié)助打洞】端口,并發(fā)送協(xié)助連接申請。同時(shí)在該端口號上啟動(dòng)偵聽(tīng)。注意由于要在相同的網(wǎng)絡(luò )終端上綁定到不同的套接字上,所以必須為這些套接字設置 SO_REUSEADDR 屬性(即允許重用),否則偵聽(tīng)會(huì )失敗。
4、 S的【協(xié)助打洞】連接收到A的申請后通過(guò)【主連接】通知B,并將A經(jīng)過(guò)NAT-A轉換后的公網(wǎng)IP地址和端口等信息告訴B。
5、 B收到S的連接通知后首先與S的【協(xié)助打洞】端口連接,隨便發(fā)送一些數據后立即斷開(kāi),這樣做的目的是讓S能知道B經(jīng)過(guò)NAT-B轉換后的公網(wǎng)IP和端口號。
6、 B嘗試與A的經(jīng)過(guò)NAT-A轉換后的公網(wǎng)IP地址和端口進(jìn)行connect,根據不同的路由器會(huì )有不同的結果,有些路由器在這個(gè)操作就能建立連接(例如我用的TPLink R402),大多數路由器對于不請自到的SYN請求包直接丟棄而導致connect失敗,但NAT-A會(huì )紀錄此次連接的源地址和端口號,為接下來(lái)真正的連接做好了準備,這就是所謂的打洞,即B向A打了一個(gè)洞,下次A就能直接連接到B剛才使用的端口號了。
7、 客戶(hù)端B打洞的同時(shí)在相同的端口上啟動(dòng)偵聽(tīng)。B在一切準備就緒以后通過(guò)與S的【主連接】回復消息“我已經(jīng)準備好”,S在收到以后將B經(jīng)過(guò)NAT-B轉換后的公網(wǎng)IP和端口號告訴給A。
8、 A收到S回復的B的公網(wǎng)IP和端口號等信息以后,開(kāi)始連接到B公網(wǎng)IP和端口號,由于在步驟6中B曾經(jīng)嘗試連接過(guò)A的公網(wǎng)IP地址和端口,NAT-A紀錄了此次連接的信息,所以當A主動(dòng)連接B時(shí),NAT-B會(huì )認為是合法的SYN數據,并允許通過(guò),從而直接的TCP連接建立起來(lái)了。
整個(gè)實(shí)現過(guò)程靠文字恐怕很難講清楚,再加上我的語(yǔ)言表達能力很差(高考語(yǔ)文才考75分,總分150分,慚愧),所以只好用代碼來(lái)說(shuō)明問(wèn)題了。
// 服務(wù)器地址和端口號定義#define SRV_TCP_MAIN_PORT 4000 // 服務(wù)器主連接的端口號#define SRV_TCP_HOLE_PORT 8000 // 服務(wù)器響應客戶(hù)端打洞申請的端口號這兩個(gè)端口是固定的,服務(wù)器S啟動(dòng)時(shí)就開(kāi)始偵聽(tīng)這兩個(gè)端口了。
//// 將新客戶(hù)端登錄信息發(fā)送給所有已登錄的客戶(hù)端,但不發(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;}當有新的客戶(hù)端連接到服務(wù)器時(shí),服務(wù)器負責將該客戶(hù)端的信息(IP地址、端口號)發(fā)送給其他客戶(hù)端。//// 執行者:客戶(hù)端A// 有新客戶(hù)端B登錄了,我(客戶(hù)端A)連接服務(wù)器端口 SRV_TCP_HOLE_PORT ,申請與客戶(hù)端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é)助打洞的端口號 SRV_TCP_HOLE_PORTtry{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è)線(xiàn)程來(lái)偵聽(tīng)端口 g_nHolePort 的連接請求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 );// 我(客戶(hù)端A)向服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT 發(fā)送申請,希望與新登錄的客戶(hù)端B建立連接// 服務(wù)器會(huì )將我的打洞用的外部IP和端口號告訴客戶(hù)端BASSERT ( 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ù)器回應,將客戶(hù)端B的外部IP地址和端口號告訴我(客戶(hù)端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" );}// 對方斷開(kāi)連接了else{goto finished;}bRet = TRUE;finished:g_pSock_MakeHole = NULL;return bRet;}這里假設客戶(hù)端A先啟動(dòng),當客戶(hù)端B啟動(dòng)后客戶(hù)端A將收到服務(wù)器S的新客戶(hù)端登錄的通知,并得到客戶(hù)端B的公網(wǎng)IP和端口,客戶(hù)端A啟動(dòng)線(xiàn)程連接S的【協(xié)助打洞】端口(本地端口號可以用GetSocketName()函數取得,假設為M),請求S協(xié)助TCP打洞,然后啟動(dòng)線(xiàn)程偵聽(tīng)該本地端口(前面假設的M)上的連接請求,然后等待服務(wù)器的回應。//// 客戶(hù)端A請求我(服務(wù)器)協(xié)助連接客戶(hù)端B,這個(gè)包應該在打洞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 );// 客戶(hù)端A想要和客戶(hù)端B建立直接的TCP連接,服務(wù)器負責將A的外部IP和端口號告訴給Bt_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;// 等待客戶(hù)端B打洞完成,完成以后通知客戶(hù)端A直接連接客戶(hù)端外部IP和端口號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收到客戶(hù)端A的協(xié)助打洞請求后通知客戶(hù)端B,要求客戶(hù)端B向客戶(hù)端A打洞,即讓客戶(hù)端B嘗試與客戶(hù)端A的公網(wǎng)IP和端口進(jìn)行connect。//// 執行者:客戶(hù)端B// 處理服務(wù)器要我(客戶(hù)端B)向另外一個(gè)客戶(hù)端(A)打洞,打洞操作在線(xiàn)程中進(jìn)行。// 先連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT ,通過(guò)服務(wù)器告訴客戶(hù)端A我(客戶(hù)端B)的外部IP地址和端口號,然后啟動(dòng)線(xiàn)程進(jìn)行打洞,// 客戶(hù)端A在收到這些信息以后會(huì )發(fā)起對我(客戶(hù)端B)的外部IP地址和端口號的連接(這個(gè)連接在客戶(hù)端B打洞完成以后進(jìn)行,所以// 客戶(hù)端B的NAT不會(huì )丟棄這個(gè)SYN包,從而連接能建立)//BOOL Handle_SrvReqMakeHole ( CSocket &MainSock, t_SrvReqMakeHolePkt *pSrvReqMakeHolePkt ){ASSERT ( pSrvReqMakeHolePkt );// 創(chuàng )建Socket,連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT,連接建立以后發(fā)送一個(gè)斷開(kāi)連接的請求給服務(wù)器,然后連接斷開(kāi)// 這里連接的目的是讓服務(wù)器知道我(客戶(hù)端B)的外部IP地址和端口號,以通知客戶(hù)端ACSocket 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é)助打洞的端口號 SRV_TCP_HOLE_PORT,發(fā)送一個(gè)斷開(kāi)連接的請求,然后將連接斷開(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è)線(xiàn)程來(lái)向客戶(hù)端A的外部IP地址、端口號打洞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è)線(xiàn)程來(lái)偵聽(tīng)端口 g_nHolePort 的連接請求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;}客戶(hù)端B收到服務(wù)器S的打洞通知后,先連接S的【協(xié)助打洞】端口號(本地端口號可以用GetSocketName()函數取得,假設為X),啟動(dòng)線(xiàn)程嘗試連接客戶(hù)端A的公網(wǎng)IP和端口號,根據路由器不同,連接情況各異,如果運氣好直接連接就成功了,即使連接失敗,但打洞便完成了。同時(shí)還要啟動(dòng)線(xiàn)程在相同的端口(即與S的【協(xié)助打洞】端口號建立連接的本地端口號X)上偵聽(tīng)到來(lái)的連接,等待客戶(hù)端A直接連接該端口號。//// 執行者:客戶(hù)端A// 服務(wù)器要求主動(dòng)端(客戶(hù)端A)直接連接被動(dòng)端(客戶(hù)端B)的外部IP和端口號//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 );// 直接與客戶(hù)端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 );// 接收測試數據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;}在客戶(hù)端B打洞和偵聽(tīng)準備好以后,服務(wù)器S回復客戶(hù)端A,客戶(hù)端A便直接與客戶(hù)端B的公網(wǎng)IP和端口進(jìn)行連接,收發(fā)數據可以正常進(jìn)行,為了測試是否真正地直接TCP連接,在數據收發(fā)過(guò)程中可以將服務(wù)器S強行終止,看是否數據收發(fā)還正常進(jìn)行著(zhù)。
程序執行步驟和方法:
程序執行成功后的界面:客戶(hù)端出現“Send Data”或者“Received Data”表示穿越NAT的TCP連接已經(jīng)建立起來(lái),數據收發(fā)已經(jīng)OK。



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