CSocket 編程之阻塞和非阻塞模式

來源:互聯網
上載者:User
有時,花上幾個小時閱讀、調試、跟蹤優秀的源碼程式,能夠更快地掌握某些技術關鍵點和精髓。當然,前提是對這些技術大致上有一個瞭解。

  我通過幾個採用 CSocket 類編寫並基於 Client/Server (用戶端 / 服務端)的網路聊天和傳輸檔案的程式 ( 詳見: 原始碼參考 ) ,在調試這些程式的過程中,追蹤深入至 CSocket 類核心源碼 Sockcore.cpp , 對於CSocket 類的運行機制可謂是一覽無遺,並且對於阻塞和非阻塞方式下的 socket 程式的編寫也是稍有體會。

閱讀本文請先注意:

  這裡的阻塞和非阻塞的概念僅適用於 Server 端 socket 程式。socket 意為通訊端,它與 Socket 不同,請注意首字母的大小寫。

  用戶端與服務端的通訊簡單來講:服務端 socket 負責監聽,應答,接收和發送訊息,而用戶端 socket 只是串連,應答,接收,發送訊息。此外,如果你對於採用 CSocket 類編寫 Client/Server 網路程式的原理不是很瞭解,請先查詢一下( 詳見:參考書籍和線上協助 )。
在此之前,有必要先講述一下: 網路傳輸服務提供者, ws2_32.dll , socket 事件 和 socket window 。

1、網路傳輸服務提供者(網路傳輸服務進程), Socket 事件, Socket Window

  網路傳輸服務提供者 ( transport service provider )是以 DLL 的形式存在的,在 windows 作業系統啟動時由服務進程 svchost.exe 載入。當 socket 被建立時,調用 API 函數 Socket (在 ws2_32.dll 中), Socket 函數會傳遞三個參數 : 地址族,通訊端類型 ( 注 2 ) 和協議,這三個參數決定了是由哪一個類型的 網路傳輸服務提供者 來啟動網路傳輸服務功能。所有的網路通訊正是由網路傳輸服務提供者完成 , 這裡將 網路傳輸服務提供者 稱為 網路傳輸服務進程 更有助於理解,因為前文已提到 網路傳輸服務提供者 是由 svchost.exe 服務進程所載入的。
  描述了網路應用程式、 CSocket ( WSock32.dll )、 Socket API(ws2_32.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 指標添加至 當前模組狀態 ( 注 1 )的一個映射表變數 m_pmapSocketHandle 中。

  在 AttachHandle 過程中,會 new 一個 CSocketWnd 執行個體 ( 基於 CWnd 派生 ) ,這裡將這個執行個體稱之為 socket window ,進一步理解為它是存放所有 sockets 的訊息池 ( window 訊息),請仔細查看,這裡 socket 後多加了一個 s ,表示建立的多個 socket 將共用一個 訊息池 。

  當 Client 端 socket 與 Server 端相互連信時 , 此時 網路傳輸服務進程 向 socket window 發送訊息 WM_SOCKET_NOTIFY ,需要說明的是 CSocketWnd 視窗控制代碼儲存在 當前模組狀態 的 m_hSocketWindow 變數中。

2、阻塞模式

  阻塞模式下 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 (見 1 ),換句話說,就是判斷是否有來自 Client 端 socket 的串連請求。

  如果當前 Server 端 socket 的事件隊列中存在正在引入的串連事件, Accept 返回一個非 0 值。否則, Accept 返回 0,此時調用 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 方法開始通訊。

3、非阻塞模式

  在非阻塞模式下 利用 socket 事件 的訊息機制, Server 端與 Client 端之間的通訊處於非同步狀態下。

  通常需要從 CSocket 類派生一個新類,派生新類的目的是重載 socket 事件 的訊息函數,然後在 socket 事件 的訊息函數中添入合適的代碼以完成 Client 端與 Server 端之間的通訊,與阻塞模式相比,非阻塞模式無需建立一個新線程。

  這裡將討論當 Server 端 socket 事件 - FD_ACCEPT 被觸發後,該事件的處理函數 OnAccept 是如何進一步被觸發的。其它事件的處理函數如 OnConnect, OnReceive 等的觸發方式與此類似。

  在 1 中已提到 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 0L ;
}

  訊息參數 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 ,它是不可靠的串連方式。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.