TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码)

TCP P2P连接与NAT穿透技术实战 本文还有配套的精品资源,点击获取 简介:本文介绍了如何利用TCP协议实现在NAT环境下两个节点的点对点(P2P)通信。TCP P2P连接示例项目包含源代码,用于展示如何克服NAT带来的挑战,实现网络节点间的直接通信。项目的焦点是NAT穿透技术,包括ICE、STUN、TURN等方法,以及TCP连接的基础原理和NAT的工作机制。通过本示例代码,开发者能够深入理解TCP连接在NA... 阅读详情

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码)

【版   本】

1.0.0

【操作系统】

Windows 系列

【作   者】

谢红伟·chrys ·chrys@163.com ·http://www.howa.com.cn/

【开发公司】

深圳市伟信科技开发有限公司

【开发日期】

2007-07-24 01:34:57

这个标题用了两个顿号三个名称,其实说得是同一个东西,只是网上有不同的说法罢了,另外好像还有人叫TCP打孔(我的朋友小妞听说后问“要打孔啊,要不要我帮你去借个电钻过来啊?”“~!·¥%……·!”)。

 

闲话少说,我们先看一下技术背景:

Internet的迅速发展以及IPv4 地址数量的限制使得网络地址翻译(NAT,Network Address Trans2lation)设备得到广泛应用。NAT设备允许处于同一NAT后的多台主机共享一个公网(本文将处于同一NAT后的网络称为私网,处于NAT前的网络称为公网) IP 地址。一个私网IP 地址通过NAT设备与公网的其他主机通信。公网和私网IP地址域,如下图所示:

一般来说都是由私网内主机(例如上图中“电脑A -01 ” )主动发起连接,数据包经过NAT地址转换后送给公网上的服务器(例如上图中的“Server”),连接建立以后可双向传送数据,NAT设备允许私网内主机主动向公网内主机发送数据,但却禁止反方向的主动传递,但在一些特殊的场合需要不同私网内的主机进行互联(例如P2P软件、网络会议、视频传输等),TCP穿越NAT的问题必须解决。网上关于UDP穿越NAT的文章很多,而且还有配套源代码,但是我个人认为UDP数据虽然速度快,但是没有保障,而且NAT为UDP准备的临时端口号有生命周期的限制,使用起来不够方便,在需要保证传输质量的应用上TCP连接还是首选(例如:文件传输)。

网上也有不少关于TCP穿越NAT(即TCP打洞)的介绍文章,但不幸我还没找到相关的源代码可以参考,我利用空余时间写了一个可以实现TCP穿越NAT,让不同的私网内主机建立直接的TCP通信的源代码。

 

这里需要介绍一下NAT的类型:

NAT设备的类型对于TCP穿越NAT,有着十分重要的影响,根据端口映射方式,NAT可分为如下4类,前3种NAT类型可统称为cone类型。

(1)全克隆( Full Cone) : NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。任何一个外部主机均可通过该映射发送IP包到该内部主机。

(2)限制性克隆(Restricted Cone) : NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。但是,只有当内部主机先给IP地址为X的外部主机发送IP包,该外部主机才能向该内部主机发送IP包。

(3)端口限制性克隆( Port Restricted Cone) :端口限制性克隆与限制性克隆类似,只是多了端口号的限制,即只有内部主机先向IP地址为X,端口号为P的外部主机发送1个IP包,该外部主机才能够把源端口号为P的IP包发送给该内部主机。

(4)对称式NAT ( Symmetric NAT) :这种类型的NAT与上述3种类型的不同,在于当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, NAT对该内部主机的映射会有所不同。对称式NAT不保证所有会话中的私有地址和公开IP之间绑定的一致性。相反,它为每个新的会话分配一个新的端口号。

 

我们先假设一下:有一个服务器S在公网上有一个IP,两个私网分别由NAT-A和NAT-B连接到公网,NAT-A后面有一台客户端A,NAT-B后面有一台客户端B,现在,我们需要借助S将A和B建立直接的TCP连接,即由B向A打一个洞,让A可以沿这个洞直接连接到B主机,就好像NAT-B不存在一样。

实现过程如下(请参照源代码):

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连接建立起来了。

 

整个实现过程靠文字恐怕很难讲清楚,再加上我的语言表达能力很差(高考语文才考75分,总分150分,惭愧),所以只好用代码来说明问题了。

// 服务器地址和端口号定义

#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/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;

       // 创建打洞Socket,连接服务器协助打洞的端口号 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 ) );

 

       // 创建一个线程来侦听端口 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/n" );

       }

       // 对方断开连接了

       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/n", 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 ) //d

       {

              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/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 ) );

 

       // 连接服务器协助打洞的端口号 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/n", 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 )/n", pSrvReqDirectConnectPkt->szInvitedIP,

              pSrvReqDirectConnectPkt->nInvitedPort, pSrvReqDirectConnectPkt->dwInvitedID );

 

       // 直接与客户端B建立TCP连接,如果连接成功说明TCP打洞已经成功了。

       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;

}

在客户端B打洞和侦听准备好以后,服务器S回复客户端A,客户端A便直接与客户端B的公网IP和端口进行连接,收发数据可以正常进行,为了测试是否真正地直接TCP连接,在数据收发过程中可以将服务器S强行终止,看是否数据收发还正常进行着。

 

程序执行步骤和方法:

1、              要准备好环境,如果要真实测试的话需要用2个连到公网上的局域网,1台具有公网地址的电脑(为了协助我测试,小曹、小妞可费了不少心,我还霸占了他们家的电脑,在此表示感谢)。如果不是这样的环境,程序执行可能会不正常,因为我暂时未做相同局域网的处理。

2、              在具有公网地址的电脑上执行“TcpHoleSrv.exe”程序,假设这台电脑的公网IP地址是“129.208. 12.38 ” 。

3、              在局域网A中的一台电脑上执行“TcpHoleClt-A.exe 129.208. 12.38 ”

4、              在局域网B中的一台电脑上执行“TcpHoleClt-B.exe 129.208. 12.38 ”

程序执行成功后的界面:客户端出现“Send Data”或者“Received Data”表示穿越NAT的TCP连接已经建立起来,数据收发已经OK。

服务器S

 

客户端A

 

客户端B

 

 

本代码在Windows XP、一个天威局域网、一个电信局域网、一个电话拨号网络中测试通过。

由于时间和水平的关系,代码和文章写得都不咋的,但愿能起到抛砖引玉的作用。代码部分只是实现了不同局域网之间的客户端相互连接的问题,至于相同局域网内的主机或者其中一台客户端本身就具有公网IP的问题这里暂时未做考虑(因为那些处理实在太简单了,比较一下掩码或者公网IP就能判断出来的);另外程序的防错性代码重用性也做得不好,只是实现了功能,我想以后有时间的话再来修正吧。

 

 

谢红伟

chrys@163.com

 

 

 
深入解析TCP在P2P通信与NAT穿越中的实现技巧 简介:P2P通信和TCP是IT领域内的重要技术,但要让两者在NAT环境下正常工作,需要掌握特定的实现方法。本专题将详细介绍TCP在P2P通信中的应用,包括中继服务器的使用、NAT穿透技术,以及TCP穿越NAT的策略。同时,对TCP打洞技术进行详尽阐述,包括预握手、实际握手和保持连接的步骤,并探讨了这些技术在分布式系统、游戏和实时通信中的关键作用。压缩包中的代码文件和可执行文件将帮助开发者理解和实现这些技术。 阅读详情

相关推荐

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞源码

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞源码

p2p技术之tcp内网穿透 java实现版

如今p2p技术已经十分成熟,然而很多人停留在理论层面,在实现过程中遇到这样那样的问题,甚至有的人说tcp打洞无法实现,本文简单阐述tcp穿透的过程,然后附上完整的java代码。

印链(Inchain) 的技术博客 2万+

Windows下的UDP打洞小demo

UDP打洞过程 假设A想连接B.首先A发送打洞消息给Server,让Server告诉B有人想与你建立通话通道,Server将A的IP信息转发给B B收到命令后向A发一个UDP包,此时B的NAT会建立一个与A通讯的Session. 然后A再次向B发送UDP包B就能收到了

网络穿透 P2P 穿透 UDP打洞、TCP打洞 原理

网络穿透 P2P 穿透 UDP打洞、TCP打洞 原理

Sico2Sico 6074

udp打洞通信、windows服务开发、winform用户控件版本2

udp打洞通信、windows服务开发、winform用户控件测试用例

网卡Inter (R) Wi-Fi 6 AX201突然故障 错误代码10,无法找到无线网络,蓝牙也无效了

问题描述 说来也是坑,简直就莫名其妙。早上在工位上,电脑一切正常(一直用的是无线网络),然后周五准备开周会了,所以拔掉电脑外接设备(电源线,鼠标,外接显示器)拿起电脑到会议室。到了会议室之后,无线网断开了(但一般是不会断开的,都在公司范围内),所以选择重连,但发现已经没有无线网络列表了。 电脑机型:联想YOGA 14s 2021 初步尝试 1、关闭开始飞行模式 - 无果 2、重启并重启 (因为前两天已经提醒过有windows更新,趁这个机会就更新并重启了)- 无果 3、下载鲁大师、驱动精灵、联想驱动管

疯狂的蜗牛 5万+

TCP的p2p网络模式

Peer-to-Peer,中文译为“对等网络”或“点对点技术”,是一种分布式应用架构,其中每个参与者(称为“节点”)都能够同时作为客户端和服务器,直接与其它节点进行数据交互,而不需要通过中央服务器中转。P2P网络的关键特征是去中心化和资源的分散共享,这使得网络更加健壮,更能适应大规模的数据交换和共享;3、其他状态转换过程。

xiaofeilongyu的博客 1309

idea 编译打包nacos2.0.3源码,生成可执行jar 包常见问题

idea 编译打包nacos2.0.3源码,生成可执行jar 包常见问题

fyihdg的专栏 1218

TCP实现P2P通信

Internet的迅速发展以及IPv4 地址数量的限制使得网络地址翻译(NAT,Network Address Trans2lation)设备得到广泛应用。NAT设备允许处于同一NAT后的多台主机共享一个公网(本文将处于同一NAT后的网络称为私网,处于NAT前的网络称为公网) IP 地址。一个私网IP 地址通过NAT设备与公网的其他主机通信。公网和私网IP地址域,如下图所示:

chenzhisi的专栏 9304

LabVIEW调用.so文件的方法

LabVIEW调用.so文件的方法

m0_37878912的博客 3024

MSPM0G3507电赛控制板设计:电机驱动与多传感器融合

微控制器是嵌入式控制系统的核心处理单元,其性能与外设集成度直接决定实时控制能力。基于Arm Cortex-M0+内核的MCU凭借低功耗、高性价比和丰富模拟资源,成为大学生电子设计竞赛等工程实践场景的主流选择。本文围绕MSPM0G3507展开,深入解析双路电机驱动(TB6612FNG)、多源传感采集(MPU6050+ADC电压监测)及抗干扰硬件架构等关键技术,强调电源隔离、信号完整性与鲁棒性设计对72小时封闭竞赛环境的关键支撑作用。典型应用涵盖巡线小车运动控制、姿态解算与蓝牙/Zigbee协同通信,为控制类赛

weixin_42588672的博客 541

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码)[转]

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码) ◆◆◆作者◆◆◆ 谢红伟· chrys · chrys@163.com · http://www.howa.com.cn ◆◆◆日期◆◆◆ 2007-07-24 01:34:57   这个标题用了两个顿号三个名称,其实说得是同一个东西,只是网上有不同的说法罢了,另外好像还有人叫TCP打孔(我的朋友小妞听说后问“要...

weixin_34343689的博客 232

手把手教你完成YOLOv11分类识别界面搭建,使用Qt6设计YOLOv11分类识别系统,前台系统+后台管理系统开发实战,全网最详细教程

本项目结合YOLOv11分类识别算法和PySide6框架,开发了一套图形化分类识别系统。系统支持图片、视频和摄像头实时分类检测,并具备完善的用户管理功能,包括登录验证、个人信息修改等模块。最新版本新增了管理员后台管理系统,包含用户管理、权限控制等功能。系统采用MySQL数据库存储用户数据,界面设计采用QtDesigner完成,实现了高效的目标分类与友好的交互体验。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 1051

TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码)

http://wenku.baidu.com/link?url=8wAFZ7kC4uJrwclOugvVBKJXstGFnrJb-3FomJ3nov4rJ4hY_UcHvSCkSD8yvfX6fTvbYcWsT4k1M28U57ij09320Mxy2dU5KkODDLaBPUa

u010457344的专栏 4179

【雕爷学编程】Arduino动手做(75)---TTL转RS-485模块之二线制可以实现真正的多点双向通信

37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里准备逐一动手尝试系列实验,不管成功(程序走通)与否,都会记录下来---小小的进步或是搞不掂的问题,希望能够抛砖引玉。 【Arduino】168种传感器模块系列实验(资料代码+仿真编程+图形编程) 实验七十五:TTL转RS-485模块 MAX485 RS485模块 MAX485 是一个8.

雕爷学编程 1万+

Uniapp返回上一页并刷新:亲测有效的 goBack 实现技巧!

这个技巧在实际开发中非常常用,特别适合表单提交后返回列表刷新这类场景。比起依赖onShow或复杂的vuex状态同步,这种方式更直接、好维护,也不容易出 bug。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 1841

P2P如何实现内网穿透(打洞)

目录 概述 P2P简介 P2P通信技术 中继(Relaying) 逆向连接(Connection reversal) UDP打洞 端点处于不同NAT 端点处于相同的NAT 固定端口绑定 空闲状态下的超时问题 TCP打洞 套接字和TCP端口的重用 打开P2P的TCP流 TCP同时打开 参考资料 概述 P2P简介 我们知道,内网设备是不能直接访问公网的,如果需要内网...

u014023993的专栏 8万+

uniapp(全端兼容) - 最新详细实现支付键盘功能组件,从底部弹出数字键盘输入支付密码进行付款交互效果,自定义支付密码键盘仿微信、支付宝支付键盘,1~9密码数字键盘,电商支付(详细示例代码复制运行

uniapp,微信小程序,自定义数字键盘,输入支付密码的键盘组件,uniapp小程序如何实现从底部弹出输入支付密码自定义键盘功能,uniapp小程序密码支付键盘组件插件,自定义数字键盘输入页面,怎么封装底部弹窗自定义数字键盘用于输入支付密码,uniapp小程序仿类似电商支付密码输入模态框,uniapp微信小程序平台实现自定义支付密码键盘,uniapp 密码支付,数字键盘,安全键盘,交易键盘,交易密码,交易密码输入框,uniapp自定义支付键盘: uniapp自定义支付键盘,uniapp小程序端实现键盘输入支

✅ 精选推荐文章 2905

P2P技术详解-P2P中的NAT穿越(打洞)方案详解笔记

P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式有明显的区别,在即时通讯方案中应用广泛。在P2P网络中,所有通讯节点的地位都是对等的,每个节点既是客户端又是服务端,节点之间通过直接通信实现文件信息、处理器运算能力、存储空间等资源的共享。P2P网络具有分散性、可扩展性、健壮性等特点,这使得P2P在信息共享、即时通讯、协同工作、分布式计算、网路存储等领域有广阔的应用。图1-经典的CS模式图2-P2P结构模型打洞技术看起来是一项近似乎蛮干的技术,却不失为一种有效的技术手段。

2302_76401432的博客 3164

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

P2P打洞技术详解

原文:https://rebootcat.com/2021/03/28/p2p_nat_traversal/ 何为打洞?(what) 英文翻译 NAT traversal : NAT 穿越 NAT Hole Punching : NAT 打孔 定义 (UDP) 打洞技术是通过中间公网服务器的协助在通信双方的 NAT 网关上建立相关的映射表项,使得双方发送的报文能直接穿透对方的 NAT 网关(防火墙),实现 P2P 直连。 洞:所谓的洞就是映射规则,外部能够主动与之通信的规则 为何要打洞?(why.

人至贱则无敌 9136

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

P2P中NAT穿越方案(UDP/TCP)

  1、P2P简介对等网络,即对等计算机网络,是一种在对等者(Peer)之间分配任务和工作负载的分布式应用架构,是对等计算模型在应用层形成的一种组网或网络形式。因此,从字面上,P2P可以理解为对等计算或对等网络。在P2P网络环境中,彼此连接的多台计算机之间都处于对等的地位,各台计算机有相同的功能,无主从之分,一台计算机既可作为服务器,设定共享资源供网络中其他计算机所使用,又可以作为工作站,整个网络...

do_best_的博客 1万+

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

TCP实现P2P(NAT3-NAT4)

主要讲了用 TCP 实现 NAT3-NAT4 P2P通信的思路 还提供了 UDP 可运行的版本

细粒丁的博客 3645

UDP/TCP 打洞(穿越NAT)技术

转自:http://www.cnblogs.com/LeoWong/archive/2009/09/25/1574265.html内 容概述:在p2p通信领域中,由NAT(Network Address Translation,网络地址转换)引起的问题已经众所周知了,它会导致在NAT内部的p2p客户端在无论以何种有效的公网ip都无法访问的问题。虽 然目前已经发展出多种穿越NAT的技术,但相关的技术文档却很少,用来证明这些技术的稳定性和优点的实际数据更少。本文的目的在于描述和分析在实际中运用 得最广

wenhuiqiao的专栏 1万+

P2P技术详解(三):P2P中的NAT穿越(打洞)方案详解(进阶分析篇)

目录1、NAT和NAPT2、NAT带来的问题3、P2P通信穿越NAT的技术、方法4、NAT穿越技术1:应用层网关4.1、原理4.2、限制5、NAT穿越技术2:中间件技术5.1、原理5.2、限制6、NAT穿越技术3:打洞技术(Hole Punching)6.1、原理6.2、方法6.2.1NAT行为类型与侦测方法6.2.2NAT打洞过程7、NAT穿越技术4:Relay服务器中转技术7.1、UDP协议的Relay7.1.1方式一、Send and Data methods,具体交互过程如下:7.1.2方式二、ch

万事亨通 3332

C++:p2p通信,打洞技术,穿越NAT的实现

UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching) NAT设备的类型介绍:转载http://www.limou.net/?p=120 NAT设备的类型对于TCP穿越NAT,有着十分重要的影响,根据端口映射方式,NAT可分为如下4类,前3种NAT类型可统称为cone类型。 全克隆( Full Cone) : NAT把所有来自相同内部IP地址和

sctq8888的专栏 5841
下一篇: 如何在 Windows Mobile (WinCE 5.0) 中用Vc++编程查找并连接周围的蓝牙(Bluetooth)设备并实现数据通信
chrys2000
博客等级 码龄22年 70粉丝 13原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值