TCP实现P2P通信TCP穿越NAT的方法TCP打洞附源代码.docx
- 文档编号:9712666
- 上传时间:2023-02-06
- 格式:DOCX
- 页数:21
- 大小:110.37KB
TCP实现P2P通信TCP穿越NAT的方法TCP打洞附源代码.docx
《TCP实现P2P通信TCP穿越NAT的方法TCP打洞附源代码.docx》由会员分享,可在线阅读,更多相关《TCP实现P2P通信TCP穿越NAT的方法TCP打洞附源代码.docx(21页珍藏版)》请在冰豆网上搜索。
TCP实现P2P通信TCP穿越NAT的方法TCP打洞附源代码
TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞(附源代码)
◆◆◆作者◆◆◆
谢红伟·chrys·chrys@·
◆◆◆日期◆◆◆
2007-07-2401:
34:
57
这个标题用了两个顿号三个名称,其实说得是同一个东西,只是网上有不同的说法罢了,另外好像还有人叫TCP打孔(我的朋友小妞听说后问“要打孔啊,要不要我帮你去借个电钻过来啊?
”“~!
·¥%……·!
”)。
闲话少说,我们先看一下技术背景:
Internet的迅速发展以及IPv4地址数量的限制使得网络地址翻译(NAT,NetworkAddressTrans2lation)设备得到广泛应用。
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)全克隆(FullCone):
NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。
任何一个外部主机均可通过该映射发送IP包到该内部主机。
(2)限制性克隆(RestrictedCone):
NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。
但是,只有当内部主机先给IP地址为X的外部主机发送IP包,该外部主机才能向该内部主机发送IP包。
(3)端口限制性克隆(PortRestrictedCone):
端口限制性克隆与限制性克隆类似,只是多了端口号的限制,即只有内部主机先向IP地址为X,端口号为P的外部主机发送1个IP包,该外部主机才能够把源端口号为P的IP包发送给该内部主机。
(4)对称式NAT(SymmetricNAT):
这种类型的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,根据不同的路由器会有不同的结果,有些路由器在这个操作就能建立连接(例如我用的TPLinkR402),大多数路由器对于不请自到的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分,惭愧),所以只好用代码来说明问题了。
//服务器地址和端口号定义
#defineSRV_TCP_MAIN_PORT4000//服务器主连接的端口号
#defineSRV_TCP_HOLE_PORT8000//服务器响应客户端打洞申请的端口号
这两个端口是固定的,服务器S启动时就开始侦听这两个端口了。
//
//将新客户端登录信息发送给所有已登录的客户端,但不发送给自己
//
BOOLSendNewUserLoginNotifyToAll(LPCTSTRlpszClientIP,UINTnClientPort,DWORDdwID)
{
ASSERT(lpszClientIP&&nClientPort>0);
g_CSFor_PtrAry_SockClient.Lock();
for(inti=0;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(); returnFALSE; } } } g_CSFor_PtrAry_SockClient.Unlock(); returnTRUE; } 当有新的客户端连接到服务器时,服务器负责将该客户端的信息(IP地址、端口号)发送给其他客户端。 // //执行者: 客户端A //有新客户端B登录了,我(客户端A)连接服务器端口SRV_TCP_HOLE_PORT,申请与客户端B建立直接的TCP连接 // BOOLHandle_NewUserLogin(CSocket&MainSock,t_NewUserLoginPkt*pNewUserLoginPkt) { printf("Newuser(%s: %u: %u)loginserver\n",pNewUserLoginPkt->szClientIP, pNewUserLoginPkt->nClientPort,pNewUserLoginPkt->dwID); BOOLbRet=FALSE; DWORDdwThreadID=0; t_ReqConnClientPktReqConnClientPkt; CSocketSock; CStringcsSocketAddress; charszRecvBuffer[NET_BUFFER_SIZE]={0}; intnRecvBytes=0; //创建打洞Socket,连接服务器协助打洞的端口号SRV_TCP_HOLE_PORT try { if(! Sock.Socket()) { printf("Createsocketfailed: %s\n",hwFormatMessage(GetLastError())); gotofinished; } UINTnOptValue=1; if(! Sock.SetSockOpt(SO_REUSEADDR,&nOptValue,sizeof(UINT))) { printf("SetSockOptsocketfailed: %s\n",hwFormatMessage(GetLastError())); gotofinished; } if(! Sock.Bind(0)) { printf("Bindsocketfailed: %s\n",hwFormatMessage(GetLastError())); gotofinished; } if(! Sock.Connect(g_pServerAddess,SRV_TCP_HOLE_PORT)) { printf("Connectto[%s: %d]failed: %s\n",g_pServerAddess,SRV_TCP_HOLE_PORT,hwFormatMessage(GetLastError())); gotofinished; } } catch(CExceptione) { charszError[255]={0}; e.GetErrorMessage(szError,sizeof(szError)); printf("Exceptionoccur,%s\n",szError); gotofinished; } 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))returnFALSE; 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)) gotofinished; //等待服务器回应,将客户端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_SrvReqDirectConnectend\n"); } //对方断开连接了 else { gotofinished; } bRet=TRUE; finished: g_pSock_MakeHole=NULL; returnbRet; } 这里假设客户端A先启动,当客户端B启动后客户端A将收到服务器S的新客户端登录的通知,并得到客户端B的公网IP和端口,客户端A启动线程连接S的【协助打洞】端口(本地端口号可以用GetSocketName()函数取得,假设为M),请求S协助TCP打洞,然后启动线程侦听该本地端口(前面假设的M)上的连接请求,然后等待服务器的回应。 // //客户端A请求我(服务器)协助连接客户端B,这个包应该在打洞Socket中收到 // BOOLCSockClient: : Handle_ReqConnClientPkt(t_ReqConnClientPkt*pReqConnClientPkt) { ASSERT(! m_bMainConn); CSockClient*pSockClient_B=FindSocketClient(pReqConnClientPkt->dwInvitedID); if(! pSockClient_B)returnFALSE; printf("%s: %u: %uinvite%s: %u: %uconnection\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_SrvReqMakeHolePktSrvReqMakeHolePkt; 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)) returnFALSE; //等待客户端B打洞完成,完成以后通知客户端A直接连接客户端外部IP和端口号 if(! HANDLE_IS_VALID(m_hEvtWaitClientBHole)) returnFALSE; if(WaitForSingleObject(m_hEvtWaitClientBHole,6000*1000)==WAIT_OBJECT_0)//d { if(SendChunk(&m_SrvReqDirectConnectPkt,sizeof(t_SrvReqDirectConnectPkt),0)==sizeof(t_SrvReqDirectConnectPkt)) returnTRUE; } returnFALSE; } 服务器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包,从而连接能建立) // BOOLHandle_SrvReqMakeHole(CSocket&MainSock,t_SrvReqMakeHolePkt*pSrvReqMakeHolePkt) { ASSERT(pSrvReqMakeHolePkt); //创建Socket,连接服务器协助打洞的端口号SRV_TCP_HOLE_PORT,连接建立以后发送一个断开连接的请求给服务器,然后连接断开 //这里连接的目的是让服务器知道我(客户端B)的外部IP地址和端口号,以通知客户端A CSocketSock; try { if(! Sock.Create()) { printf("Createsocketfailed: %s\n",hwFormatMessage(GetLastError())); returnFALSE; } if(! Sock.Connect(g_pServerAddess,SRV_TCP_HOLE_PORT)) { printf("Connectto[%s: %d]failed: %s\n",g_pServerAddess,SRV_TCP_HOLE_PORT,hwFormatMessage(GetLastError())); returnFALSE; } } catch(CExceptione) { charszError[255]={0}; e.GetErrorMessage(szError,sizeof(szError)); printf("Exceptionoccur,%s\n",szError); returnFALSE; } CStringcsSocketAddress; ASSERT(g_nHolePort==0); VERIFY(Sock.GetSockName(csSocketAddress,g_nHolePort)); //连接服务器协助打洞的端口号SRV_TCP_HOLE_PORT,发送一个断开连接的请求,然后将连接断开,服务器在收到这个包的时候也会将 //连接断开 t_ReqSrvDisconnectPktReqSrvDisconnectPkt; 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)) returnFALSE; Sleep(100); Sock.Close(); //创建一个线程来向客户端A的外部IP地址、端口号打洞 t_SrvReqMakeHolePkt*pSrvReqMakeHolePkt_New=newt_SrvReqMakeHolePkt; if(! pSrvReqMakeHolePkt_New)returnFALSE; memcpy(pSrvReqMakeHolePkt_New,pSrvReqMakeHolePkt,sizeof(t_SrvReqMakeHolePkt)); DWORDdwThreadID=0; g_hThread_MakeHole=: : CreateThread(NULL,0,: : ThreadProc_MakeHole,LPVOID(pSrvReqMakeHolePkt_New),0,&dwThreadID); if(! HANDLE_IS_VALID(g_hThread_MakeHole))returnFALSE; //创建一个线程来侦听端口g_nHolePort的连接请求 dwThreadID=0; g_hThread_Listen=: : CreateThread(NULL,0,: : ThreadProc_Listen,LPVOID(NULL),0,&dwThreadID); if(! HANDLE_
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- TCP 实现 P2P 通信 穿越 NAT 方法 打洞 源代码