天天看點

P2P技術基礎: 關于TCP打洞技術

4 關于TCP打洞技術

建立穿越NAT裝置的p2p的 TCP 連接配接隻比UDP複雜一點點,TCP協定的“打洞”從協定層來看是與UDP

的“打洞”過程非常相似的。盡管如此,基于TCP協定的打洞至今為止還沒有被很好的了解,這也

造成了對其提供支援的NAT裝置不是很多。

在NAT裝置支援的前提下,基于TCP的“打洞”技術實際上與基于UDP的“打洞”技術一樣快捷、可靠。實際上,隻要NAT裝置支援的話,基于TCP的p2p技術的健壯性将比基于UDP的技術的更強一些,因為TCP協定的狀态機給出了一種标準的方法來精确的擷取某個TCP session的生命期,而UDP協定則無法做到這一點。

4.1 套接字和TCP端口的重用

實作基于TCP協定的p2p“打洞”過程中,最主要的問題不是來自于TCP協定,而是來自于來自于應用程式的API接口。這是由于标準的伯克利(Berkeley)套接字的API是圍繞着建構用戶端/伺服器程式而設計的,API允許TCP流套接字通過調用connect()函數來建立向外的連接配接,或者通過listen()和accept函數接受來自外部的連接配接。

但是, TCP協定并沒有象 UDP那樣的“同一個端口既可以向外連接配接,又能夠接受來自外部的連接配接 ” 的API。而且更糟的是,TCP的套接字通常僅允許建立1對1的響應,即應用程式在将一個套接字綁定到本地的一個端口以後,任何試圖将第二個套接字綁定到該端口的操作都會失敗。

為了讓TCP“打洞”能夠順利工作,我們需要使用一個本地的TCP端口來監聽來自外部的TCP連接配接,同時建立多個向外的TCP連接配接。幸運的是,所有的主流作業系統都能夠支援一個特殊的TCP套接字參數,通常叫做“SO_REUSEADDR”,該參數允許應用程式将多個套接字綁定到本地的一個endpoint(隻要所有要綁定的套接字都設定了SO_REUSEADDR參數即可)。BSD系統引入了SO_REUSEPORT參數,該參數用于區分

端口重用還是位址重用,在這樣的系統裡面,上述所有的參數必須都設定才行。

4.2 打開p2p的TCP流

假定用戶端A希望建立與B的TCP連接配接。我們像通常一樣假定A和B已經與公網上的已知伺服器S建立了TCP連接配接。伺服器記錄下來每個聯入的用戶端的公網和内網的endpoints,如同為UDP服務的時候一樣從協定層來看,TCP“打洞”與UDP“打洞”是幾乎完全相同的過程:

1)、 S啟動兩個網絡偵聽,一個叫【主連接配接】偵聽,一個叫【協助打洞】的偵聽。

2)、 A和B分别與S的【主連接配接】保持聯系。

3)、當A需要和B建立直接的TCP連接配接時,首先連接配接S的【協助打洞】端口,并發送協助連接配接申請。同時在該端口号上啟動偵聽。注意由于要在相同的網絡終端上綁定到不同的套接字上,是以必須為這些套接字設定 SO_REUSEADDR 屬性(即允許重用),否則偵聽會失敗。

4)、 S的【協助打洞】連接配接收到A的申請後通過【主連接配接】通知B,并将A經過NAT-A轉換後的公網IP位址和端口等資訊告訴B。

5)、 B收到S的連接配接通知後首先與S的【協助打洞】端口連接配接,随便發送一些資料後立即斷開,這樣做的目的是讓S能知道B經過NAT-B轉換後的公網IP和端口号。

6)、 B嘗試與A的經過NAT-A轉換後的公網IP位址和端口進行connect,根據不同的路由器會有不同的結果,有些路由器在這個操作就能建立連接配接(例如我用的TPLink R402),大多數路由器對于不請自到的SYN請求包直接丢棄而導緻connect失敗,但NAT-A會紀錄此次連接配接的源位址和端口号,為接下來真正的連接配接做好了準備,這就是所謂的打洞,即B向A打了一個洞,下次A就能直接連接配接到B剛才使用的端口号了。

7)、用戶端B打洞的同時在相同的端口上啟動偵聽。B在一切準備就緒以後通過與S的【主連接配接】回複消息“我已經準備好”,S在收到以後将B經過NAT-B轉換後的公網IP和端口号告訴給A。

8)、 A收到S回複的B的公網IP和端口号等資訊以後,開始連接配接到B公網IP和端口号,由于在步驟6中B曾經嘗試連接配接過A的公網IP位址和端口,NAT-A紀錄了此次連接配接的資訊,是以當A主動連接配接B時,NAT-B會認為是合法的SYN資料,并允許通過,進而直接的TCP連接配接建立起來了。

P2P技術基礎: 關于TCP打洞技術

圖7

與UDP不同的是,使用UDP協定的每個用戶端隻需要一個套接字即可完成與伺服器S通信,

并同時與多個p2p用戶端通信的任務;而TCP用戶端必須處理多個套接字綁定到同一個本地

TCP端口的問題,如圖7所示。

現在來看更加實際的一種情景:A與B分别位于不同的NAT裝置後面。如同使用UDP協定進行“打洞”

操作遇到的問題一樣,TCP的“打洞”操作也會遇到内網的IP與“僞”公網IP重複造成連接配接失敗或者錯誤連接配接之類的問題。

用戶端向彼此公網endpoint發起連接配接的操作,會使得各自的NAT裝置打開新的“洞”以允許A與B的

TCP資料通過。如果NAT裝置支援TCP“打洞”操作的話,一個在用戶端之間的基于TCP協定的流

通道就會自動建立起來。如果A向B發送的第一個SYN包發到了B的NAT裝置,而B在此前沒有向

A發送SYN包,B的NAT裝置會丢棄這個包,這會引起A的“連接配接失敗”或“無法連接配接”問題。而此時,由于A已經向B發送過SYN包,B發往A的SYN包将被看作是由A發往B的包的回應的一部分,

是以B發往A的SYN包會順利地通過A的NAT裝置,到達A,進而建立起A與B的p2p連接配接。

4.3 從應用程式的角度來看TCP“打洞”

從應用程式的角度來看,在進行TCP“打洞”的時候都發生了什麼呢?

假定A首先向B發出SYN包,該包發往B的公網endpoint,并且被B的NAT裝置丢棄,但是B發往A的公網endpoint的SYN包則通過A的NAT到達了A,然後,會發生以下的兩種結果中的一種,具體是哪一種取決于作業系統對TCP協定的實作:

(1)A的TCP實作會發現收到的SYN包就是其發起連接配接并希望聯入的B的SYN包,通俗一點來說

就是“說曹操,曹操到”的意思,本來A要去找B,結果B自己找上門來了。A的TCP協定棧是以

會把B做為A向B發起連接配接connect的一部分,并認為連接配接已經成功。程式A調用的異步connect()

函數将成功傳回,A的listen()等待從外部聯入的函數将沒有任何反映。此時,B聯入A的操作

在A程式的内部被了解為A聯入B連接配接成功,并且A開始使用這個連接配接與B開始p2p通信。

由于A收到的SYN包中不包含A需要的ACK資料,是以,A的TCP将用SYN-ACK包回應B的公網endpoint,

并且将使用先前A發向B的SYN包一樣的序列号。一旦B的TCP收到由A發來的SYN-ACK包,則把自己

的ACK包發給A,然後兩端建立起TCP連接配接。簡單地說,第一種,就是即使A發往B的SYN包被B的NAT

丢棄了,但是由于B發往A的包到達了A。結果是,A認為自己連接配接成功了,B也認為自己連接配接成功

了,不管是誰成功了,總之連接配接是已經建立起來了。

(2)另外一種結果是,A的TCP實作沒有像(1)中所講的那麼“智能”,它沒有發現現在聯入的B

就是自己希望聯入的。就好比在機場接人,明明遇到了自己想要接的人卻不認識,誤認為是其它

的人,安排别人給接走了,後來才知道是自己錯過了機會,但是無論如何,人已經接到了任務

已經完成了。然後,A通過正常的listen()函數和accept()函數得到與B的連接配接,而由A發起的向

B的公網endpoint的連接配接會以失敗告終。盡管A向B的連接配接失敗,A仍然得到了B發起的向A的連接配接,

等效于A與B之間已經聯通,不管中間過程如何,A與B已經連接配接起來了,結果是A和B的基于TCP協定

的p2p連接配接已經建立起來了。

第一種結果适用于基于BSD的作業系統對于TCP的實作,而第二種結果更加普遍一些,多數linux和

windows系統都會按照第二種結果來處理。

代碼:

// 伺服器位址和端口号定義 #define SRV_TCP_MAIN_PORT    4000  // 伺服器主連接配接的端口号 #define SRV_TCP_HOLE_PORT    8000  // 伺服器響應用戶端打洞申請的端口号

這兩個端口是固定的,伺服器S啟動時就開始偵聽這兩個端口了。

// // 将新用戶端登入資訊發送給所有已登入的用戶端,但不發送給自己 // 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; }

當有新的用戶端連接配接到伺服器時,伺服器負責将該用戶端的資訊(IP位址、端口号)發送給其他用戶端。

// // 執行者:用戶端A // 有新用戶端B登入了,我(用戶端A)連接配接伺服器端口 SRV_TCP_HOLE_PORT ,申請與B建立直接的TCP連接配接 // BOOL Handle_NewUserLogin ( CSocket &MainSock, t_NewUserLoginPkt *pNewUserLoginPkt ) {   printf ( "New user ( %s:%u:%u ) login server", 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;   // 建立打洞Socket,連接配接伺服器協助打洞的端口号 SRV_TCP_HOLE_PORT:   try   {     if ( !Sock.Socket () )     {       printf ( "Create socket failed : %s", hwFormatMessage(GetLastError()) );       goto finished;     }      UINT nOptValue = 1;     if ( !Sock.SetSockOpt ( SO_REUSEADDR, &nOptValue , sizeof(UINT) ) )     {       printf ( "SetSockOpt socket failed : %s", hwFormatMessage(GetLastError()) );        goto finished;     }     if ( !Sock.Bind ( 0 ) )     {        printf ( "Bind socket failed : %s", hwFormatMessage(GetLastError()) );       goto finished;     }     if ( !Sock.Connect ( g_pServerAddess, SRV_TCP_HOLE_PORT ) )     {       printf ( "Connect to [%s:%d] failed : %s", 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", szError );     goto finished;   }   g_pSock_MakeHole = &Sock;   ASSERT ( g_nHolePort == 0 );   VERIFY ( Sock.GetSockName ( csSocketAddress, g_nHolePort ) );   // 建立一個線程來偵聽端口 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 );    // 我(用戶端A)向伺服器協助打洞的端口号 SRV_TCP_HOLE_PORT 發送申請,

    // 希望與新登入的用戶端B建立連接配接    // 伺服器會将我的打洞用的外部IP和端口号告訴用戶端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;   // 等待伺服器回應,将用戶端B的外部IP位址和端口号告訴我(用戶端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" );   }   // 對方斷開連接配接了   else   {     goto finished;   }      bRet = TRUE; finished:   g_pSock_MakeHole = NULL;   return bRet; }

這裡假設用戶端A先啟動,當用戶端B啟動後用戶端A将收到伺服器S的新用戶端登入的通知,并得到用戶端B的公網IP和端口,用戶端A啟動線程連接配接S的【協助打洞】端口(本地端口号可以用GetSocketName()函數取得,假設為M),請求S協助TCP打洞,然後啟動線程偵聽該本地端口(前面假設的M)上的連接配接請求,然後等待伺服器的回應。

// // 用戶端A請求我(伺服器)協助連接配接用戶端B,這個包應該在打洞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",

        m_csPeerAddress, m_nPeerPort, m_dwID,

     pSockClient_B->m_csPeerAddress,

        pSockClient_B->m_nPeerPort,

        pSockClient_B->m_dwID );   // 用戶端A想要和用戶端B建立直接的TCP連接配接,伺服器負責将A的外部IP和端口号告訴給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和端口号   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; }

伺服器S收到用戶端A的協助打洞請求後通知用戶端B,要求用戶端B向用戶端A打洞,即讓用戶端B嘗試與用戶端A的公網IP和端口進行connect。

// // 執行者:用戶端B // 處理伺服器要我(用戶端B)向另外一個用戶端(A)打洞,打洞操作線上程中進行。 // 先連接配接伺服器協助打洞的端口号 SRV_TCP_HOLE_PORT ,通過伺服器告訴用戶端A我(用戶端B)的外部IP位址和端口号,然後啟動線程進行打洞, // 用戶端A在收到這些資訊以後會發起對我(用戶端B)的外部IP位址和端口号的連接配接(這個連接配接在用戶端B打洞完成以後進行,是以 // 用戶端B的NAT不會丢棄這個SYN包,進而連接配接能建立) // BOOL Handle_SrvReqMakeHole(CSocket &MainSock, t_SrvReqMakeHolePkt *pSrvReqMakeHolePkt) {    ASSERT ( pSrvReqMakeHolePkt );   // 建立Socket,連接配接伺服器協助打洞的端口号 SRV_TCP_HOLE_PORT,連接配接建立以後發送一個斷開連接配接的請求給伺服器,然後連接配接斷開   // 這裡連接配接的目的是讓伺服器知道我(用戶端B)的外部IP位址和端口号,以通知用戶端A   CSocket Sock;   try    {     if ( !Sock.Create () )     {       printf ( "Create socket failed : %s", hwFormatMessage(GetLastError()) );       return FALSE;     }     if ( !Sock.Connect ( g_pServerAddess, SRV_TCP_HOLE_PORT ) )     {       printf ( "Connect to [%s:%d] failed : %s", 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", szError );     return FALSE;   }   CString csSocketAddress;   ASSERT ( g_nHolePort == 0 );   VERIFY ( Sock.GetSockName ( csSocketAddress, g_nHolePort ) );   // 連接配接伺服器協助打洞的端口号 SRV_TCP_HOLE_PORT,發送一個斷開連接配接的請求,然後将連接配接斷開,伺服器在收到這個包的時候也會将    // 連接配接斷開    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 ();    // 建立一個線程來向用戶端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;   // 建立一個線程來偵聽端口 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;   

     // 等待打洞和偵聽完成    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",          g_WelcomePkt.szClientIP, g_WelcomePkt.nClientPort,        hwFormatMessage(GetLastError()) );     return FALSE;   }       return TRUE; }

用戶端B收到伺服器S的打洞通知後,先連接配接S的【協助打洞】端口号(本地端口号可以用 GetSocketName()函數取得,假設為X),啟動線程嘗試連接配接用戶端A的公網IP和端口号,根據路由器不同,連接配接情況各異,如果運氣好直接連接配接就成功了,即使連接配接失敗,但打洞便完成了。同時還要啟動線程在相同的端口(即與S的【協助打洞】端口号建立連接配接的本地端口号X)上偵聽到來的連接配接,等待用戶端A直接連接配接該端口号。

// // 執行者:用戶端A // 伺服器要求主動端(用戶端A)直接連接配接被動端(用戶端B)的外部IP和端口号 // BOOL Handle_SrvReqDirectConnect ( t_SrvReqDirectConnectPkt *pSrvReqDirectConnectPkt ) {   ASSERT ( pSrvReqDirectConnectPkt );    printf ( "You can connect direct to ( IP:%s PORT:%d ID:%u )",

        pSrvReqDirectConnectPkt->szInvitedIP,      pSrvReqDirectConnectPkt->nInvitedPort, pSrvReqDirectConnectPkt->dwInvitedID );   // 直接與用戶端B建立TCP連接配接,如果連接配接成功說明TCP打洞已經成功了。   CSocket Sock;   try   {      if ( !Sock.Socket () )     {       printf ( "Create socket failed : %s", hwFormatMessage(GetLastError()) );       return FALSE;     }     UINT nOptValue = 1;     if ( !Sock.SetSockOpt ( SO_REUSEADDR, &nOptValue , sizeof(UINT) ) )     {        printf( "SetSockOpt socket failed : %s", hwFormatMessage(GetLastError()));       return FALSE;     }      if ( !Sock.Bind ( g_nHolePort ) )     {       printf ( "Bind socket failed : %s", 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", hwFormatMessage(GetLastError()) );       }       if ( !Sock.Connect ( pSrvReqDirectConnectPkt->szInvitedIP, pSrvReqDirectConnectPkt->nInvitedPort ) )       {          printf ( "Connect to [%s:%d] failed : %s",            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 !!!",            pSrvReqDirectConnectPkt->szInvitedIP,

                pSrvReqDirectConnectPkt->nInvitedPort );              // 接收測試資料        printf ( "Receiving data ..." );       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", 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", szError );      return FALSE;   }   return TRUE; }

  在用戶端B打洞和偵聽準備好以後,伺服器S回複用戶端A,用戶端A便直接與用戶端B的公網IP和端口進行連接配接,收發資料可以正常進行,為了測試是否真正地直接TCP連接配接,在資料收發過程中可以将伺服器S強行終止,看是否資料收發還正常進行着。

<end>

繼續閱讀