有時花上幾個小時閱讀調試跟蹤優秀的源碼程序能夠更快地掌握某些技術關鍵點和精髓當然前提是對這些技術大致上有一個了解
我通過幾個采用 CSocket 類編寫並基於 Client/Server (客戶端 / 服務端)的網絡聊天和傳輸文件的程序 ( 詳見 源代碼參考 ) 在調試這些程序的過程中追蹤深入至 CSocket 類核心源碼 Sockcorecpp 對於CSocket 類的運行機制可謂是一覽無遺並且對於阻塞和非阻塞方式下的 socket 程序的編寫也是稍有體會
閱讀本文請先注意
這裡的阻塞和非阻塞的概念僅適用於 Server 端 socket 程序socket 意為套接字它與 Socket 不同請注意首字母的大小寫
客戶端與服務端的通信簡單來講服務端 socket 負責監聽應答接收和發送消息而客戶端 socket 只是連接應答接收發送消息此外如果你對於采用 CSocket 類編寫 Client/Server 網絡程序的原理不是很了解請先查詢一下( 詳見參考書籍和在線幫助 )
在此之前有必要先講述一下 網絡傳輸服務提供者 ws_dll socket 事件 和 socket window
網絡傳輸服務提供者(網絡傳輸服務進程) Socket 事件 Socket Window
網絡傳輸服務提供者 ( transport service provider )是以 DLL 的形式存在的在 windows 操作系統啟動時由服務進程 svchostexe 加載當 socket 被創建時調用 API 函數 Socket (在 ws_dll 中) Socket 函數會傳遞三個參數 : 地址族套接字類型 ( 注 ) 和協議這三個參數決定了是由哪一個類型的 網絡傳輸服務提供者 來啟動網絡傳輸服務功能所有的網絡通信正是由網絡傳輸服務提供者完成 這裡將 網絡傳輸服務提供者 稱為 網絡傳輸服務進程 更有助於理解因為前文已提到 網絡傳輸服務提供者 是由 svchostexe 服務進程所加載的
下圖描述了網絡應用程序 CSocket ( WSockdll ) Socket API(ws_dll) 和 網絡傳輸服務進程 之間的接口層次關系
當 Client 端 socket 與 Server 端 socket 相互通信時兩端均會觸發 socket 事件這裡僅簡要說明兩個 socket 事件
FD_CONNECT: 連接事件 通常 Client 端 socket 調用 socket API 函數 Connect 時所觸發這個事件發生在 Client 端
FD_ACCEPT 正在引入的連接事件通常 Server 端 socket 正在接收來自 Client 端 socket 連接時觸發這個事件發生在 Server 端
網絡傳輸服務進程 將 socket 事件 保存至 socket 的事件隊列中此外 網絡傳輸服務進程 還會向 socket window 發送消息 WM_SOCKET_NOTIFY 通知有 socket 事件 產生見下文對 socket window 的詳細說明
調用 CSocket::Create 函數後socket 被創建 socket 創建過程中調用 CAsyncSocket::AttachHandle(SOCKET hSocket CAsyncSocket* pSocket BOOL bDead) 該函數的作用是 將 socket 實例句柄和 socket 指針添加至 當前模塊狀態 ( 注 )的一個映射表變量 m_pmapSocketHandle 中
在 AttachHandle 過程中會 new 一個 CSocketWnd 實例 ( 基於 CWnd 派生 ) 這裡將這個實例稱之為 socket window 進一步理解為它是存放所有 sockets 的消息池 ( window 消息)請仔細查看這裡 socket 後多加了一個 s 表示創建的多個 socket 將共享一個 消息池
當 Client 端 socket 與 Server 端相互通信時 此時 網絡傳輸服務進程 向 socket window 發送消息 WM_SOCKET_NOTIFY 需要說明的是 CSocketWnd 窗口句柄保存在 當前模塊狀態 的 m_hSocketWindow 變量中
阻塞模式
阻塞模式下 Server 端與 Client 端之間的通信處於同步狀態下在 Server 端直接實例化 CSocket 類調用 Create 方法創建 socket 然後調用方法 Listen 開始偵聽最後用一個 while 循環阻塞調用 Accept 函數用於等待來自 Client 端的連接如果這個 socket 在主線程(主程序)中運行這將導致主線程的阻塞因此需要創建一個新的線程以運行 socket 服務
調試跟蹤至 CSocket::Accept 函數源碼
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;
}
它不斷調用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 類)判斷 Server 端 socket 的事件隊列中是否存在正在引入的連接事件 FD_ACCEPT (見 )換句話說就是判斷是否有來自 Client 端 socket 的連接請求
如果當前 Server 端 socket 的事件隊列中存在正在引入的連接事件 Accept 返回一個非 值否則 Accept 返回 此時調用 GetLastError 將返回錯誤代碼 WSAEWOULDBLOCK 表示隊列中無任何連接請求注意到在循環體內有一句代碼 PumpMessage(FD_ACCEPT);
PumpMessage 作為一個消息泵使得 socket window 中的消息能夠維持在活動狀態實際跟蹤進入 PumpMessage 中發現這個消息泵與 Accept 函數的調用並不相關它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重繪消息)處於活動狀態而絕大部分的 socket window 消息被阻塞被阻塞的消息中含有 WM_SOCKET_NOTIFY
很顯然如果沒有來自 Client 端 socket 的連接請求 CSocket 就會不斷調用 Accept 產生循環阻塞直到有來自 Client 端 socket 的連接請求而解除阻塞
阻塞解除後表示 Server 端 socket 和 Client 端 socket 已成功連接 Server 端與 Client 端彼此相互調用 Send 和 Receive 方法開始通信
非阻塞模式
在非阻塞模式下 利用 socket 事件 的消息機制 Server 端與 Client 端之間的通信處於異步狀態下
通常需要從 CSocket 類派生一個新類派生新類的目的是重載 socket 事件 的消息函數然後在 socket 事件 的消息函數中添入合適的代碼以完成 Client 端與 Server 端之間的通信與阻塞模式相比非阻塞模式無需創建一個新線程
這裡將討論當 Server 端 socket 事件 - FD_ACCEPT 被觸發後該事件的處理函數 OnAccept 是如何進一步被觸發的其它事件的處理函數如 OnConnect OnReceive 等的觸發方式與此類似
在 中已提到 Client/Server 端通信時 Server 端 socket 正在接收來自 Client 端 socket 連接請求這將會觸發 FD_ACCEPT 事件同時 Server 端的 網絡傳輸服務進程 向 Server 端的 socket window (CSocketWnd )發送事件通知消息 WM_SOCKET_NOTIFY 通知有 FD_ACCEPT 事件產生 CsocketWnd 在收到事件通知消息後調用消息處理函數 OnSocketNotify:
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam LPARAM lParam)
{
CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY wParam lParam);
CSocket::ProcessAuxQueue();
return L ;
}
消息參數 wParam 是 socket 的句柄 lParam 是 socket 事件 這裡稍作解釋一下CSocketWnd 類是作為 CSocket 類的 友元類 這意味著它可以訪問 CSocket 類中的保護和私有成員函數和變量 AuxQueueAdd 和 ProcessAuxQueue 是 CSocket 類的靜態成員函數如果你對友元不熟悉請迅速找本有關 C++ 書看一下友元的使用方法吧!
ProcessAuxQueue 是實質處理 socket 事件的函數在該函數中有這樣一句代碼 CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam TRUE);
其實也就是由 socket 句柄得到發送事件通知消息的 socket 指針 pSocket從 m_pmapSocketHandle 中查找!
最後 WSAGETSELECTEVENT(lParam) 會取出事件類型在一個簡單的 switch 語句中判斷事件類型並調用事件處理函數在這裡事件類型是 FD_ACCEPT 當然就調用 pSocket>OnAccept !
結束語
Server 端 socket 處於阻塞調用模式下它必須在一個新創建的線程中工作防止主線程被阻塞
當有多個 Client 端 socket 與 Server 端 socket 連接及通信時 Server 端采用阻塞模式就顯得不適合了應該采用非阻塞模式 利用 socket 事件 的消息機制來接受多個 Client 端 socket 的連接請求並進行通信
在非阻塞模式下利用 CSocketWnd 作為所有 sockets 的消息池是實現 socket 事件 的消息機制的關鍵技術文中存在用詞不妥和可能存在的技術問題請大家原諒也請批評指正謝謝!
注
當前模塊狀態——用於保存當前線程和模塊狀態的一個結構可以通過 AfxGetThreadModule() 獲得AFX_MODULE_THREAD_STATE 在 CSocket 重新定義為 _AFX_SOCK_THREAD_STATE
socket 類型——在 TCP/IP 協議中 Client/Server 網絡程序采用 TCP 協議即 socket 類型為 SOCK_STREAM 它是可靠的連接方式在這裡不采用 UDP 協議即 socket 類型為 SOCK_DGRAM 它是不可靠的連接方式
From:http://tw.wingwit.com/Article/program/net/201311/15802.html