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連接建立起來了。


圖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>

 轉至:http://blog.csdn.net/zhongguoren666/article/details/7489809
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM