TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞
客户端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强行终止,看是否数据收发还正常进行着。