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
 

P2P技术基础: 关于TCP打洞技术的更多相关文章

  1. TCP打洞技术

    //转http://iamgyg.blog.163.com/blog/static/3822325720118202419740/ 建立穿越NAT设备的p2p的TCP连接仅仅比UDP复杂一点点,TCP ...

  2. [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching)

     [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching) http://www.360doc.com/content/12/0428/17/6187784 ...

  3. [p2p]UDP用打洞技术穿透NAT的原理与实现

    首先先介绍一些基本概念:            NAT(Network Address             Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的, ...

  4. P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解

    1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...

  5. p2p 打洞技术

    根据通信双方所处网络环境不同,点对点通信可以划分成以下三类:i> 公网:公网ii>公网:内网iii>内网:内网前两种容易实现,我们这里主要讨论第三种.这其中会涉及到NAT和NAPT的 ...

  6. Cisco路由技术基础知识详解

    第一部分 请写出568A的线序(接触网络第一天就应该会的,只要你掐过,想都能想出来) .网卡MAC地址长度是(  )个二进制位(16进制与2进制的换算关系,只是换种方式问,不用你拿笔去算) A.12  ...

  7. #WPF的3D开发技术基础梳理

    原文:#WPF的3D开发技术基础梳理 自学WPF已经有半年有余了,一遍用,一边学.但是一直没有去触摸WPF的3D开发相关技术,因为总觉得在内心是一座大山,觉得自己没有能力去逾越.最近因为一个项目的相关 ...

  8. JNI技术基础(2)——从零开始编写JNI代码

    书接上文: <JNI技术基础(1)——从零开始编写JNI代码> 2.编译源程序HelloWorld.java并生成HelloWorld.class 3.生成头文件HelloWorld.h ...

  9. AOP技术基础

    1.引言 2.AOP技术基础 3.Java平台AOP技术研究 4..Net平台AOP技术研究 2.1 AOP技术起源 AOP技术的诞生并不算晚,早在1990年开始,来自Xerox Palo Alto ...

随机推荐

  1. EF中限制字段显示长度

    在EF中有些添加的字段 文本显示超多文字,想截取显示又没有截取功能. 怎么办? 我们可以在EF中类的属性中设置 你想限制这个用户名只能有10个字符长度 public String UserName { ...

  2. 二、搭建struts2的开发环境

    二.搭建struts2的开发环境 下载地址:http://struts.apache.org 解压后的目录结构: apps:框架本身提供一些案例(学习) docs:框架本身提供的文档(指南和API). ...

  3. 【python】疯了,掉坑里出不来了

    学软件最头疼的事情就是版本换来换去: 各种配置错误,疯了,疯了--

  4. mapreduce (四) MapReduce实现Grep+sort

    1.txt dong xi cheng xi dong cheng wo ai beijing tian an men qiche dong dong dong 2.txt dong xi cheng ...

  5. ios本地文件内容读取,.json .plist 文件读写

    ios本地文件内容读取,.json .plist 文件读写 本地文件.json .plist文件是较为常用的存储本地数据的文件,对这些文件的操作也是一种常用的基础. 本文同时提供初始化变量的比较标准的 ...

  6. angular js一探

    下一代angular js. 概念:mvc:作为dataModel的$scope. 还必须导入angular的库. ng-app:告诉angular引擎从这里开始是他因该管理的内容.(引入之后,可以在 ...

  7. 用 Freemarker 生成 word 文档

     阅读目录 添加图片 自定义载入模板 1.       用word写一个需要导出的word模板,然后存为xml格式. 2.       将xml中需要动态修改内容的地方,换成freemarker的 ...

  8. element-UI使用中:el-input type为textarea时@change无法触发?

    自己瞎尝试解决了的.官方文档上居然没写@input事件,醉了.

  9. php从数据库中取二进制流文件转换为图片,图片以二进制流存入数据库实现

    php从数据库中取二进制流文件转换为图片,图片以二进制流存入数据库实现 function data_uri($contents, $mime) { $base64 = base64_encode($c ...

  10. algorithm与numeric的一些常用函数

    numeric中的accumulated的基本用法: 来自:https://blog.csdn.net/u011499425/article/details/52756242 #include < ...