TCP打洞技术
- 格式:pdf
- 大小:433.45 KB
- 文档页数:16
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 );
{ 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 {
广域网与私网示意图 一般来说都是由私网内主机(例如上图中“电脑A-01”)主动发起连 接,数据包经过NAT地址转换后送给公网上的服务器(例如上图中的 “Server”),连接建立以后可双向传送数据,NAT设备允许私网内主 机主动向公网内主机发送数据,但却禁止反方向的主动传递,但在一些 特殊的场合需 要不同私网内的主机进行互联(例如P2P软件、网络会 议、视频传输等),TCP穿越NAT的问题必须解决。网上关于UDP穿越NAT 的文章很多,而且还有 配套源代码,但是我个人认为UDP数据虽然速度 快,但是没有保障,而且NAT为UDP准备的临时端口号有生命周期的限 制,使用起来不够方便,在需要保证传 输质量的应用上TCP连接还是首 选(例如:文件传输)。 网上也有不少关于TCP穿越NAT(即TCP打洞)的介绍文章,但不幸我还
技术背景: Internet的迅速发展以及IPv4 地址数量的限制使得网络地址翻译(NAT,Network Address Trans2lation)设备得到广泛应用。NAT设备允许处于同一NAT后的多台主机共享一个公网(本文 将处于同一NAT后的网络称为私网,处于 NAT前的网络称为公网) IP 地址。一个私网IP 地址通过 NAT设备与公网的其他机通信。公网和私网IP地址域,如下图所示:
没找到相关的源代码可以参考,我利用空余时间写了一个可以实现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的【协助打洞】端口连接,随便发送
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;