TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞
这里假设客户端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 ) { 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; }