阻塞模型

這個模型是講解計算機網路時被作為例子介紹的,也是最簡單的。其基本原理是:首先建立一個socket連線,然後對其進行操作,比如,從該socket讀資料。因為網路傳輸是要一定的時間的,即使網路通暢的情況下,接受資料的操作也要花費時間。對於一個簡單的單執行緒程式,接收資料的過程是無法處理其他操作的。比如一個視窗程式,當你接收資料時,點選按鈕或關閉視窗操作都不會有效。它的缺點顯而易見,一個執行緒你只能處理一個 socket,用來教課還行,實際使用效果就不行了。

select模型

為了處理多個socket連線,聰明的人們發明了select模型。該模型以集合來管理socket連線,每次去查詢集合中的socket狀態,從而達到處理多連線的能力,其函式原型是int select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout)。比如我們判斷某個socket是否有資料可讀,我們首先將一個fdread集合置空,然後將socket加入到該集合,呼叫 select(0,&fdread,NULL,NULL,NULL),之後我們判斷socket是否還在fdread中,如果還在,則說明有資料可讀。資料的讀取和阻塞模型相同,呼叫recv函式。但是每個集合容量都有一個限值,預設情況下是64個,當然你可以重新定義它的大小,但還是有一個最上限,自己設定也不能超過該值,一般情況下是1024。儘管select模型可以處理多連線,但集合的管理多少讓人感到繁瑣。

非同步選擇模型

熟悉windows作業系統的都知道,其視窗處理是基於訊息的。人們又發明了一種新的網路模型——WSAAsyncSelect模型,即非同步選擇模型。該模型為每個socket繫結一個訊息,當socket上出現事先設定的socket事件時,作業系統就會給應用程式傳送這個訊息,從而對該 socket事件進行處理,其函式原型是int WSAAsynSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent)。hWnd指明接收訊息的控制代碼,wMsg指定訊息ID,lEvent按位設定感興趣的網路事件,入 WSAAsyncSelect(s,hwnd,WM_SOCKET, FD_CONNECT | FD_READ | FD_CLOSE)。該模型的優點是在系統開銷不大的情況下同時處理許多連線,也不需要什麼集合管理。缺點很明顯,即使你的程式不需要視窗,也要專門為 WSAAsyncSelect模型定義一個視窗。另外,讓單個視窗去處理成千上萬的socket操作事件,很可能成為效能瓶頸。

事件選擇模型

與WSAAsynSelect模型類似,人們還發明瞭WSAEventSelect模型,即事件選擇模型。看名字就可以猜測出來,它是基於事件的。WSAAsynSelect模型在出現感興趣的socket事件時,系統會發一個相應的訊息。而WSAEventSelect模型在出現感興趣的socket事件時,系統會將相應WSAEVENT事件設為傳信。可能你現在對sokect事件和普通WSAEVENT事件還不是很清楚。 socket事件是與socket操作相關的一些事件,如FD_READ,FD_WRITE,FD_ACCEPT等。而WSAEVENT事件是傳統的事件,該事件有兩種狀態,傳信(signaled)和未傳信(non-signaled)。所謂傳信,就是事件發生了,未傳信就是還沒有發生。我們每次建立一個連線,都為其繫結一個事件,等到該連線變化時,事件就會變為傳信狀態。那麼,誰去接受這個事件變化呢?我們通過一個 WSAWaitForMultipleEvents(...)函式來等待事件發生,傳入引數中的事件陣列中,只有有一個事件發生,該函式就會返回(也可以設定為所有事件發生才返回,在這裡沒用),返回值為事件的陣列序號,這樣我們就知道了哪個事件發生了,也就是該事件對應的socket有了socket操作事件。該模型比起WSAAsynSelect模型的優勢很明顯,不需要視窗。唯一缺點是,該模型每次只能等待64個事件,這一限制使得在處理多 socket時,有必要組織一個執行緒池,伸縮性不如後面要講的重疊模型。

重疊I/O(Overlapped I/O)模型

重疊I/O(Overlapped I/O)模型使應用程式達到更佳的系統性能。重疊模型的基本設計原理是讓應用程式使用重疊資料結構,一次投遞一個或多個Winsock I/O請求。重疊模型到底是什麼東西呢?可以與WSAEventSelect模型做類比(其實不恰當,後面再說),事件選擇模型為每個socket連線綁定了一個事件,而重疊模型為每個socket連線綁定了一個重疊。當連線上發生socket事件時,對應的重疊就會被更新。其實重疊的高明之處在於,它在更新重疊的同時,還把網路資料傳到了實現指定的快取區中。我們知道,前面的網路模型都要使用者自己通過recv函式來接受資料,這樣就降低了效率。我們打個比方,WSAEventSelect模型就像郵局的包裹通知,使用者收到通知後要自己去郵局取包裹。而重疊模型就像送貨上門,郵遞員發給你通知時,也把包裹放到了你事先指定的倉庫中。
    重疊模型又分為事件通知和完成例程兩種模式。在分析這兩種模式之前,我們還是來看看重疊資料結構:
    typedef struct WSAOVERLAPPED
    {
       DWORD Internal;
       DWORD InternalHigh;
       DWORD Offset;
       DWORD OffsetHigh;
       WSAEVENT hEvent;
    }WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;
    該資料結構中,Internal、InternalHigh、Offset、OffsetHigh都是系統使用的,使用者不用去管,唯一關注的就是 hEvent。如果使用事件通知模式,那麼hEvent就指向相應的事件控制代碼。如果是完成例程模式,hEvent設為NULL。我們現在來看事件通知模式,首先建立一個事件hEvent,並建立一個重疊結構AcceptOverlapped,並設定AcceptOverlapped.hEvent = hEvent,DataBuf是我們事先設定的資料快取區。呼叫 WSARecv(AcceptSocket,&DataBuf,1,&RecvBytes,&Flags,&AcceptOverlapped,NULL),則將AcceptSocket與AcceptOverlapped重疊繫結在了一起。當接收到資料以後,hEvent就會設為傳信,而資料就會放到 DataBuf中。我們再通過WSAWaitForMultipleEvents(...)接收到該事件通知。這裡我們要注意,既然是基於事件通知的,那它就有一個事件處理上限,一般為64。
    完成例程和事件通知模式的區別在於,當相應的socket事件出現時,系統會呼叫使用者事先指定的回撥函式,而不是設定事件。其實就是將WSARecv的最後一個引數設為函式指標。該回調函式的原型如下:
    void CALLBACK CompletionROUTINE(
        DWORD dwError,
        DWORD cbTransferred,
        LPWSAOVERLAPPED lpOverlapped,
        DWORD dwFlags
    );
    其中,cbTransferred表示傳輸的位元組數,lpOverlapped是發生socket事件的重疊指標。我們呼叫 WSARecv(AcceptSocket,&DataBuf,1,&RecvBytes,&Flags,&AcceptOverlapped,WorkerRoutine) 將AcceptSocket與WorkRoutine例程繫結。這裡有一點小提示,當我們建立多個socket的連線時,最好把重疊與相應的資料快取區用一個大的資料結構放到一塊,這樣,我們在例程中通過lpOverlapped指標就可以直接找到相應的資料快取區。這裡要注意,不能將多個重疊使用同一個資料快取區,這樣在多個重疊都在處理時,就會出現資料混亂。

完成埠模型

下面我們來介紹專門用於處理為數眾多socket連線的網路模型——完成埠。因為需要做出大量的工作以便將socket新增到一個完成埠,而其他方法的初始化步驟則省事多了,所以對新手來說,完成埠模型好像過於複雜了。然而,一旦弄明白是怎麼回事,就會發現步驟其實並非那麼複雜。所謂完成埠,實際是Windows採用的一種I/O構造機制,除套接字控制代碼之外,還可以接受其他東西。使用這種模式之前,首先要建立一個I/O完成埠物件,該函式定義如下:
    HANDLE CreateIoCompletionPort(
       HANDLE FileHandle,
       HANDLE ExistingCompletionPort,
       DWORD CompletionKey,
       DWORD NumberOfConcurrentThreads
    );
    該函式用於兩個截然不同的目的:1)用於建立一個完成埠物件。2)將一個控制代碼同完成埠關聯到一起。
    通過引數NumberOfConcurrentThreads,我們可以指定同時執行的執行緒數。理想狀態下,我們希望每個處理器各自負責一個執行緒的執行,為完成埠提供服務,避免過於頻繁的執行緒任務切換。對於一個socket連線,我們通過 CreateIoCompletionPort((HANDLE)Accept,CompletionPort, (DWORD)PerHandleData,0)將Accept連線與CompletionPort完成埠繫結到一起,CompetionPort對應的那些執行緒不斷通過GetQueuedCompletionStatus來查詢與其關聯的socket連線是否有I/O操作完成,如果有,則做相應的資料處理,然後通過WSARecv將該socket連線再次投遞,繼續工作。完成埠在效能和伸縮性方面表現都很好,相關聯的socket連線數目沒有限制。