TCP打洞技术Word格式文档下载.docx
- 文档编号:7752128
- 上传时间:2023-05-09
- 格式:DOCX
- 页数:20
- 大小:208.78KB
TCP打洞技术Word格式文档下载.docx
《TCP打洞技术Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《TCP打洞技术Word格式文档下载.docx(20页珍藏版)》请在冰点文库上搜索。
相反,它为每个新的会话分配一个新的端口号。
我们先假设一下:
有一个服务器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<
g_PtrAry_SockClient.GetSize();
i++)
{
CSockClient*pSockClient=(CSockClient*)g_PtrAry_SockClient.GetAt(i);
if(pSockClient&
pSockClient->
m_bMainConn&
m_dwID>
0&
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
Sock.Socket())
Createsocketfailed:
%s\n"
hwFormatMessage(GetLastError()));
gotofinished;
UINTnOptValue=1;
Sock.SetSockOpt(SO_REUSEADDR,&
nOptValue,sizeof(UINT)))
SetSockOptsocketfailed:
Sock.Bind(0))
Bindsocketfailed:
Sock.Connect(g_pServerAddess,SRV_TCP_HOLE_PORT))
Connectto[%s:
%d]failed:
g_pServerAddess,
SRV_TCP_HOLE_PORT,hwFormatMessage(GetLastError()));
catch(CExceptione)
charszError[255]={0};
e.GetErrorMessage(szError,sizeof(szError));
Exceptionoccur,%s\n"
szError);
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>
ReqConnClientPkt.dwInviterID=g_WelcomePkt.dwID;
ReqConnClientPkt.dwInvitedID=pNewUserLoginPkt->
dwID;
if(Sock.Send(&
ReqConnClientPkt,sizeof(t_ReqConnClientPkt))!
=sizeof(t_ReqConnClientPkt))
//等待服务器回应,将客户端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);
Handle_SrvReqDirectConnectend\n"
);
//对方断开连接了
else
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);
pSockClient_B)returnFALSE;
%s:
%uinvite%s:
%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))
//等待客户端B打洞完成,完成以后通知客户端A直接连接客户端外部IP和端口号
HANDLE_IS_VALID(m_hEvtWaitClientBHole))
if(WaitForSingleObject(m_hEvtWaitClientBHole,6000*1000)==WAIT_OBJECT_0)
if(SendChunk(&
m_SrvReqDirectConnectPkt,sizeof(t_SrvReqDirectConnectPkt),0)
==sizeof(t_SrvReqDirectConnectPkt))
服务器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
Sock.Create())
//连接服务器协助打洞的端口号SRV_TCP_HOLE_PORT,发送一个断开连接的请求,然后将连接断开,服务器在收到这个包的时候也会将
//连接断开
t_ReqSrvDisconnectPktReqSrvDisconnectPkt;
ReqSrvDisconnectPkt.dwInviterID=pSrvReqMakeHolePkt->
ReqSrvDisconnectPkt.dwInviterHoleID=pSrvReqMakeHolePkt->
dwInviterHoleID;
ReqSrvDisconnectPkt.dwInvitedID=pSrvReqMakeHolePkt->
ASSERT(ReqSrvDisconnectPkt.dwInvitedID==g_WelcomePkt.dwID);
ReqSrvDisconnectPkt,sizeof(t_ReqSrvDisconnectPkt))!
=sizeof(t_ReqSrvDisconnectPkt))
Sleep(100);
Sock.Close();
//创建一个线程来向客户端A的外部IP地址、端口号打洞
t_SrvReqMakeHolePkt*pSrvReqMakeHolePkt_New=newt_SrvReqMakeHolePkt;
pSrvReqMakeHolePkt_New)returnFALSE;
memcpy(pSrvReqMakeHolePkt_New,pSrvReqMakeHolePkt,sizeof(t_SrvReqMakeHolePkt));
g_hThread_MakeHole=:
ThreadProc_MakeHole,
LPVOID(pSrvReqMakeHolePkt_New),0,&
HANDLE_IS_VALID(g_hThread_MakeHole))returnFALSE;
//等待打洞和侦听完成
HANDLEhEvtAry[]={g_hEvt_ListenFinished,g_hEvt_MakeHoleFinished};
if(:
WaitForMultipleObjects(LENGTH(hEvtAry),hEvtAry,TRUE,30*1000)==WAIT_TIMEOUT)
t_HoleListenReadyPktHoleListenReadyPkt;
HoleListenReadyPkt.dwInvitedID
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- TCP 打洞 技术
![提示](https://static.bingdoc.com/images/bang_tan.gif)