|
TCP實現(xiàn)P2P通信、TCP穿越NAT的方法、TCP打洞 下載源代碼 閑話少說,我們先看一下技術(shù)背景: 這里需要介紹一下NAT的類型: 我們先假設(shè)一下:有一個服務(wù)器S在公網(wǎng)上有一個IP,兩個私網(wǎng)分別由NAT-A和NAT-B連接到公網(wǎng),NAT-A后面有一臺客戶端A,NAT-B后面有一臺客戶端B,現(xiàn)在,我們需要借助S將A和B建立直接的TCP連接,即由B向A打一個洞,讓A可以沿這個洞直接連接到B主機(jī),就好像NAT-B不存在一樣。 整個實現(xiàn)過程靠文字恐怕很難講清楚,再加上我的語言表達(dá)能力很差(高考語文才考75分,總分150分,慚愧),所以只好用代碼來說明問題了。 // 服務(wù)器地址和端口號定義 #define SRV_TCP_MAIN_PORT 4000 // 服務(wù)器主連接的端口號 #define SRV_TCP_HOLE_PORT 8000 // 服務(wù)器響應(yīng)客戶端打洞申請的端口號這兩個端口是固定的,服務(wù)器S啟動時就開始偵聽這兩個端口了。 //
// 將新客戶端登錄信息發(fā)送給所有已登錄的客戶端,但不發(fā)送給自己
//
BOOL SendNewUserLoginNotifyToAll ( LPCTSTR lpszClientIP, UINT nClientPort, DWORD dwID )
{
ASSERT ( lpszClientIP && nClientPort > 0 );
g_CSFor_PtrAry_SockClient.Lock();
for ( int i=0; i<g_PtrAry_SockClient.GetSize(); 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();
return FALSE;
}
}
}
g_CSFor_PtrAry_SockClient.Unlock ();
return TRUE;
}
當(dāng)有新的客戶端連接到服務(wù)器時,服務(wù)器負(fù)責(zé)將該客戶端的信息(IP地址、端口號)發(fā)送給其他客戶端。
//
// 執(zhí)行者:客戶端A
// 有新客戶端B登錄了,我(客戶端A)連接服務(wù)器端口 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;
// 創(chuàng)建打洞Socket,連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT
try
{
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 );
VERIFY ( Sock.GetSockName ( csSocketAddress, g_nHolePort ) );
// 創(chuàng)建一個線程來偵聽端口 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)向服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT 發(fā)送申請,希望與新登錄的客戶端B建立連接
// 服務(wù)器會將我的打洞用的外部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;
// 等待服務(wù)器回應(yīng),將客戶端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;
}
bRet = TRUE;
finished:
g_pSock_MakeHole = NULL;
return bRet;
}
這里假設(shè)客戶端A先啟動,當(dāng)客戶端B啟動后客戶端A將收到服務(wù)器S的新客戶端登錄的通知,并得到客戶端B的公網(wǎng)IP和端口,客戶端A啟動線程連接S的【協(xié)助打洞】端口(本地端口號可以用GetSocketName()函數(shù)取得,假設(shè)為M),請求S協(xié)助TCP打洞,然后啟動線程偵聽該本地端口(前面假設(shè)的M)上的連接請求,然后等待服務(wù)器的回應(yīng)。
//
// 客戶端A請求我(服務(wù)器)協(xié)助連接客戶端B,這個包應(yīng)該在打洞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連接,服務(wù)器負(fù)責(zé)將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;
}
服務(wù)器S收到客戶端A的協(xié)助打洞請求后通知客戶端B,要求客戶端B向客戶端A打洞,即讓客戶端B嘗試與客戶端A的公網(wǎng)IP和端口進(jìn)行connect。
//
// 執(zhí)行者:客戶端B
// 處理服務(wù)器要我(客戶端B)向另外一個客戶端(A)打洞,打洞操作在線程中進(jìn)行。
// 先連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT ,通過服務(wù)器告訴客戶端A我(客戶端B)的外部IP地址和端口號,然后啟動線程進(jìn)行打洞,
// 客戶端A在收到這些信息以后會發(fā)起對我(客戶端B)的外部IP地址和端口號的連接(這個連接在客戶端B打洞完成以后進(jìn)行,所以
// 客戶端B的NAT不會丟棄這個SYN包,從而連接能建立)
//
BOOL Handle_SrvReqMakeHole ( CSocket &MainSock, t_SrvReqMakeHolePkt *pSrvReqMakeHolePkt )
{
ASSERT ( pSrvReqMakeHolePkt );
// 創(chuàng)建Socket,連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT,連接建立以后發(fā)送一個斷開連接的請求給服務(wù)器,然后連接斷開
// 這里連接的目的是讓服務(wù)器知道我(客戶端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 ) );
// 連接服務(wù)器協(xié)助打洞的端口號 SRV_TCP_HOLE_PORT,發(fā)送一個斷開連接的請求,然后將連接斷開,服務(wù)器在收到這個包的時候也會將
// 連接斷開
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 ();
// 創(chuàng)建一個線程來向客戶端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;
// 創(chuàng)建一個線程來偵聽端口 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;
}
客戶端B收到服務(wù)器S的打洞通知后,先連接S的【協(xié)助打洞】端口號(本地端口號可以用GetSocketName()函數(shù)取得,假設(shè)為X),啟動線程嘗試連接客戶端A的公網(wǎng)IP和端口號,根據(jù)路由器不同,連接情況各異,如果運氣好直接連接就成功了,即使連接失敗,但打洞便完成了。同時還要啟動線程在相同的端口(即與S的【協(xié)助打洞】端口號建立連接的本地端口號X)上偵聽到來的連接,等待客戶端A直接連接該端口號。
//
// 執(zhí)行者:客戶端A
// 服務(wù)器要求主動端(客戶端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打洞已經(jīng)成功了。
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 );
// 接收測試數(shù)據(jù)
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打洞和偵聽準(zhǔn)備好以后,服務(wù)器S回復(fù)客戶端A,客戶端A便直接與客戶端B的公網(wǎng)IP和端口進(jìn)行連接,收發(fā)數(shù)據(jù)可以正常進(jìn)行,為了測試是否真正地直接TCP連接,在數(shù)據(jù)收發(fā)過程中可以將服務(wù)器S強(qiáng)行終止,看是否數(shù)據(jù)收發(fā)還正常進(jìn)行著。 程序執(zhí)行步驟和方法:
程序執(zhí)行成功后的界面:客戶端出現(xiàn)“Send Data”或者“Received Data”表示穿越NAT的TCP連接已經(jīng)建立起來,數(shù)據(jù)收發(fā)已經(jīng)OK。 ![]() 服務(wù)器S ![]() 客戶端A ![]() 客戶端B 本代碼在Windows XP、一個天威局域網(wǎng)、一個電信局域網(wǎng)、一個電話撥號網(wǎng)絡(luò)中測試通過。 |
|
|