有時(shí),花上幾個(gè)小時(shí)閱讀、調(diào)試、跟蹤優(yōu)秀的源碼程序,能夠更快地掌握某些技術(shù)關(guān)鍵點(diǎn)和精髓。當(dāng)然,前提是對(duì)這些技術(shù)大致上有一個(gè)了解。
我通過(guò)幾個(gè)采用 CSocket 類編寫(xiě)并基于 Client/Server (客戶端 / 服務(wù)端)的網(wǎng)絡(luò)聊天和傳輸文件的程序 ( 詳見(jiàn): 源代碼參考 ) ,在調(diào)試這些程序的過(guò)程中,追蹤深入至 CSocket 類核心源碼 Sockcore.cpp , 對(duì)于CSocket 類的運(yùn)行機(jī)制可謂是一覽無(wú)遺,并且對(duì)于阻塞和非阻塞方式下的 socket 程序的編寫(xiě)也是稍有體會(huì)。
閱讀本文請(qǐng)先注意:
這里的阻塞和非阻塞的概念僅適用于 Server 端 socket 程序。socket 意為套接字,它與 Socket 不同,請(qǐng)注意首字母的大小寫(xiě)。
客戶端與服務(wù)端的通信簡(jiǎn)單來(lái)講:服務(wù)端 socket 負(fù)責(zé)監(jiān)聽(tīng),應(yīng)答,接收和發(fā)送消息,而客戶端 socket 只是連接,應(yīng)答,接收,發(fā)送消息。此外,如果你對(duì)于采用 CSocket 類編寫(xiě) Client/Server 網(wǎng)絡(luò)程序的原理不是很了解,請(qǐng)先查詢一下( 詳見(jiàn):參考書(shū)籍和在線幫助 )。
在此之前,有必要先講述一下: 網(wǎng)絡(luò)傳輸服務(wù)提供者, ws2_32.dll , socket 事件 和 socket window 。
1、網(wǎng)絡(luò)傳輸服務(wù)提供者(網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程), Socket 事件, Socket Window
網(wǎng)絡(luò)傳輸服務(wù)提供者 ( transport service provider )是以 DLL 的形式存在的,在 windows 操作系統(tǒng)啟動(dòng)時(shí)由服務(wù)進(jìn)程 svchost.exe 加載。當(dāng) socket 被創(chuàng)建時(shí),調(diào)用 API 函數(shù) Socket (在 ws2_32.dll 中), Socket 函數(shù)會(huì)傳遞三個(gè)參數(shù) : 地址族,套接字類型 ( 注 2 ) 和協(xié)議,這三個(gè)參數(shù)決定了是由哪一個(gè)類型的 網(wǎng)絡(luò)傳輸服務(wù)提供者 來(lái)啟動(dòng)網(wǎng)絡(luò)傳輸服務(wù)功能。所有的網(wǎng)絡(luò)通信正是由網(wǎng)絡(luò)傳輸服務(wù)提供者完成 , 這里將 網(wǎng)絡(luò)傳輸服務(wù)提供者 稱為 網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 更有助于理解,因?yàn)榍拔囊烟岬?網(wǎng)絡(luò)傳輸服務(wù)提供者 是由 svchost.exe 服務(wù)進(jìn)程所加載的。
下圖描述了網(wǎng)絡(luò)應(yīng)用程序、 CSocket ( WSock32.dll )、 Socket API(ws2_32.dll) 和 網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 之間的接口層次關(guān)系:

當(dāng) Client 端 socket 與 Server 端 socket 相互通信時(shí),兩端均會(huì)觸發(fā) socket 事件。這里僅簡(jiǎn)要說(shuō)明兩個(gè) socket 事件:
- FD_CONNECT: 連接事件 , 通常 Client 端 socket 調(diào)用 socket API 函數(shù) Connect 時(shí)所觸發(fā),這個(gè)事件發(fā)生在 Client 端。
- FD_ACCEPT :正在引入的連接事件,通常 Server 端 socket 正在接收來(lái)自 Client 端 socket 連接時(shí)觸發(fā),這個(gè)事件發(fā)生在 Server 端。
網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 將 socket 事件 保存至 socket 的事件隊(duì)列中。此外, 網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 還會(huì)向 socket window 發(fā)送消息 WM_SOCKET_NOTIFY , 通知有 socket 事件 產(chǎn)生,見(jiàn)下文對(duì) socket window 的詳細(xì)說(shuō)明。
調(diào)用 CSocket::Create 函數(shù)后,socket 被創(chuàng)建。 socket 創(chuàng)建過(guò)程中調(diào)用 CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead) 。該函數(shù)的作用是:
- 將 socket 實(shí)例句柄和 socket 指針添加至 當(dāng)前模塊狀態(tài) ( 注 1 )的一個(gè)映射表變量 m_pmapSocketHandle 中。
- 在 AttachHandle 過(guò)程中,會(huì) new 一個(gè) CSocketWnd 實(shí)例 ( 基于 CWnd 派生 ) ,這里將這個(gè)實(shí)例稱之為 socket window ,進(jìn)一步理解為它是存放所有 sockets 的消息池 ( window 消息),請(qǐng)仔細(xì)查看,這里 socket 后多加了一個(gè) s ,表示創(chuàng)建的多個(gè) socket 將共享一個(gè) 消息池 。
- 當(dāng) Client 端 socket 與 Server 端相互通信時(shí) , 此時(shí) 網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 向 socket window 發(fā)送消息 WM_SOCKET_NOTIFY ,需要說(shuō)明的是 CSocketWnd 窗口句柄保存在 當(dāng)前模塊狀態(tài) 的 m_hSocketWindow 變量中。
2、阻塞模式
阻塞模式下 Server 端與 Client 端之間的通信處于同步狀態(tài)下。在 Server 端直接實(shí)例化 CSocket 類,調(diào)用 Create 方法創(chuàng)建 socket ,然后調(diào)用方法 Listen 開(kāi)始偵聽(tīng),最后用一個(gè) while 循環(huán)阻塞調(diào)用 Accept 函數(shù)用于等待來(lái)自 Client 端的連接,如果這個(gè) socket 在主線程(主程序)中運(yùn)行,這將導(dǎo)致主線程的阻塞。因此,需要?jiǎng)?chuàng)建一個(gè)新的線程以運(yùn)行 socket 服務(wù)。
調(diào)試跟蹤至 CSocket::Accept 函數(shù)源碼:
while(!Accept(...))
{
// The socket is marked as nonblocking and no connections are present to be accepted.
if (GetLastError() == WSAEWOULDBLOCK)
PumpMessage(FD_ACCEPT);
else
return FALSE;
}
它不斷調(diào)用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 類)判斷 Server 端 socket 的事件隊(duì)列中是否存在正在引入的連接事件 - FD_ACCEPT (見(jiàn) 1 ),換句話說(shuō),就是判斷是否有來(lái)自 Client 端 socket 的連接請(qǐng)求。
如果當(dāng)前 Server 端 socket 的事件隊(duì)列中存在正在引入的連接事件, Accept 返回一個(gè)非 0 值。否則, Accept 返回 0,此時(shí)調(diào)用 GetLastError 將返回錯(cuò)誤代碼 WSAEWOULDBLOCK ,表示隊(duì)列中無(wú)任何連接請(qǐng)求。注意到在循環(huán)體內(nèi)有一句代碼:
PumpMessage(FD_ACCEPT);
PumpMessage 作為一個(gè)消息泵使得 socket window 中的消息能夠維持在活動(dòng)狀態(tài)。實(shí)際跟蹤進(jìn)入 PumpMessage 中,發(fā)現(xiàn)這個(gè)消息泵與 Accept 函數(shù)的調(diào)用并不相關(guān),它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重繪消息)處于活動(dòng)狀態(tài),而絕大部分的 socket window 消息被阻塞,被阻塞的消息中含有 WM_SOCKET_NOTIFY。
很顯然,如果沒(méi)有來(lái)自 Client 端 socket 的連接請(qǐng)求, CSocket 就會(huì)不斷調(diào)用 Accept 產(chǎn)生循環(huán)阻塞,直到有來(lái)自 Client 端 socket 的連接請(qǐng)求而解除阻塞。
阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功連接, Server 端與 Client 端彼此相互調(diào)用 Send 和 Receive 方法開(kāi)始通信。
3、非阻塞模式
在非阻塞模式下 利用 socket 事件 的消息機(jī)制, Server 端與 Client 端之間的通信處于異步狀態(tài)下。
通常需要從 CSocket 類派生一個(gè)新類,派生新類的目的是重載 socket 事件 的消息函數(shù),然后在 socket 事件 的消息函數(shù)中添入合適的代碼以完成 Client 端與 Server 端之間的通信,與阻塞模式相比,非阻塞模式無(wú)需創(chuàng)建一個(gè)新線程。
這里將討論當(dāng) Server 端 socket 事件 - FD_ACCEPT 被觸發(fā)后,該事件的處理函數(shù) OnAccept 是如何進(jìn)一步被觸發(fā)的。其它事件的處理函數(shù)如 OnConnect, OnReceive 等的觸發(fā)方式與此類似。
在 1 中已提到 Client/Server 端通信時(shí), Server 端 socket 正在接收來(lái)自 Client 端 socket 連接請(qǐng)求,這將會(huì)觸發(fā) FD_ACCEPT 事件,同時(shí) Server 端的 網(wǎng)絡(luò)傳輸服務(wù)進(jìn)程 向 Server 端的 socket window (CSocketWnd )發(fā)送事件通知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件產(chǎn)生 , CsocketWnd 在收到事件通知消息后,調(diào)用消息處理函數(shù) OnSocketNotify:
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
{
CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam);
CSocket::ProcessAuxQueue();
return 0L ;
}
消息參數(shù) wParam 是 socket 的句柄, lParam 是 socket 事件 。這里稍作解釋一下,CSocketWnd 類是作為 CSocket 類的 友元類 ,這意味著它可以訪問(wèn) CSocket 類中的保護(hù)和私有成員函數(shù)和變量, AuxQueueAdd 和 ProcessAuxQueue 是 CSocket 類的靜態(tài)成員函數(shù),如果你對(duì)友元不熟悉,請(qǐng)迅速找本有關(guān) C++ 書(shū)看一下友元的使用方法吧!
ProcessAuxQueue 是實(shí)質(zhì)處理 socket 事件的函數(shù),在該函數(shù)中有這樣一句代碼:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE);
其實(shí)也就是由 socket 句柄得到發(fā)送事件通知消息的 socket 指針 pSocket:從 m_pmapSocketHandle 中查找(見(jiàn) 1 )!
最后, WSAGETSELECTEVENT(lParam) 會(huì)取出事件類型,在一個(gè)簡(jiǎn)單的 switch 語(yǔ)句中判斷事件類型并調(diào)用事件處理函數(shù)。在這里,事件類型是 FD_ACCEPT ,當(dāng)然就調(diào)用 pSocket->OnAccept !
結(jié)束語(yǔ)
Server 端 socket 處于阻塞調(diào)用模式下,它必須在一個(gè)新創(chuàng)建的線程中工作,防止主線程被阻塞。
當(dāng)有多個(gè) Client 端 socket 與 Server 端 socket 連接及通信時(shí), Server 端采用阻塞模式就顯得不適合了,應(yīng)該采用非阻塞模式 , 利用 socket 事件 的消息機(jī)制來(lái)接受多個(gè) Client 端 socket 的連接請(qǐng)求并進(jìn)行通信。
在非阻塞模式下,利用 CSocketWnd 作為所有 sockets 的消息池,是實(shí)現(xiàn) socket 事件 的消息機(jī)制的關(guān)鍵技術(shù)。文中存在用詞不妥和可能存在的技術(shù)問(wèn)題,請(qǐng)大家原諒,也請(qǐng)批評(píng)指正,謝謝!
注:
- 當(dāng)前模塊狀態(tài)——用于保存當(dāng)前線程和模塊狀態(tài)的一個(gè)結(jié)構(gòu),可以通過(guò) AfxGetThreadModule() 獲得。AFX_MODULE_THREAD_STATE 在 CSocket 重新定義為 _AFX_SOCK_THREAD_STATE 。
- socket 類型——在 TCP/IP 協(xié)議中, Client/Server 網(wǎng)絡(luò)程序采用 TCP 協(xié)議:即 socket 類型為 SOCK_STREAM ,它是可靠的連接方式。在這里不采用 UDP 協(xié)議:即 socket 類型為 SOCK_DGRAM ,它是不可靠的連接方式。
源代碼參考:
- http://www.codeproject.com/internet/SocketFileTransfer.asp 采用 CSocket 類編寫(xiě)的基于 Client/Server 的網(wǎng)絡(luò)文件傳輸程序,它是基于阻塞模式的 Client/Server 端網(wǎng)絡(luò)程序典型示例。
- http://www.codeguru.com/Cpp/I-N/network/messaging/article.php/c5453 采用 CSocket 類編寫(xiě)的基于 Client/Server 的網(wǎng)絡(luò)聊天程序,它是基于非阻塞模式的 Client/Server 端網(wǎng)絡(luò)程序典型示例。
參考資料:
- Microsoft MSDN Library – January 2001
- 《Windows 網(wǎng)絡(luò)編程》 清華大學(xué)出版社
from: http://www.vckbase.com/document/viewdoc/?id=1375