Windows Socket IO 模型
套接字架構
.gif)
應用程序使用Winsock與傳輸協(xié)議驅動溝通時AFD.SYS負責緩沖區(qū)的管理。這就意味著當一個程序調(diào)用send或者WSASend發(fā)送數(shù)據(jù)時,數(shù)據(jù)將被復制到AFD.SYS它自己的內(nèi)部緩沖區(qū)中(依賴SO_SNDBUF的設置)WSASend調(diào)用立即返回。然后AFD.SYS在程序后臺將數(shù)據(jù)發(fā)送出去。當然,如果程序想要處理一個比SO_SNDBUF設置的緩沖區(qū)需求更大的發(fā)送請求,WSASend的調(diào)用就會阻塞直到所有的數(shù)據(jù)都被發(fā)送出去。
類似的,從遠程客戶端接收數(shù)據(jù)時,只要SO_RCVBUF設置的緩沖區(qū)還沒有滿,AFD.SYS就會將數(shù)據(jù)復制進它自己的緩沖區(qū)直到所有的發(fā)送都已完成。當程序調(diào)用recv或者是WSARecv,數(shù)據(jù)就從AFD.SYS的緩沖區(qū)復制到了程序提供的緩沖區(qū)中了。
使用Winsock的時候還會間接碰到另外兩種資源的限制。第一個頁面鎖定的限制。注意重疊操作可能偶然性地以ERROR_INSUFFICIENT_RESOURCES調(diào)用失敗,這基本上意味著有太多的發(fā)送和接收操作在等待中。另外一個限制是操作系統(tǒng)的非分頁池(non-paged pool)的限制。
阻塞模型
int recv( SOCKET s, char* buf, int len, int flags ); |
int send( SOCKET s, const char* buf, int len, int flags ); |
這種方式最為大家熟悉,Socket默認的就是阻塞模式。
在recv的時候,Socket會阻塞在那里,直到連接上有數(shù)據(jù)可讀,把數(shù)據(jù)讀到buffer里后recv函數(shù)才會返回,不然就會一直阻塞在那里。
如果在主線程中被阻塞,而數(shù)據(jù)遲遲沒有過來,那么程序就會被鎖死。這樣的問題可以用多線程解決,但是在有多個套接字連接的情況下,這不是一個好的選擇,擴展性很差,而且也容易有鎖的問題。線程過多,也導致上下文切換過于頻繁,導致系統(tǒng)變慢,而且大部分線程是處于非活動狀態(tài)的話,這就大大浪費了系統(tǒng)的資源。
非阻塞模型
int ioctlsocket( IN SOCKET s, IN long cmd, IN OUT u_long FAR * argp ); #define FIONBIO /* set/clear non-blocking i/o */ |
調(diào)用ioctlsocket函數(shù)設置FIONBIO為1就轉為非阻塞模式。
當recv和send函數(shù)沒有準備好數(shù)據(jù)時,函數(shù)不會阻塞,立即返回錯誤值,用GetLastError返回的錯誤碼為WSAEWOULDBLOCK,中文解釋為“無法立即完成一個非阻擋性套接字的操作”。
當然,這里你可以用非阻塞模擬阻塞模式,就是用while循環(huán)不停調(diào)用recv,直到recv返回成功為止。這樣的效率也不高,但好處在于你能在沒接收到數(shù)據(jù)時,有空進行其他操作,或者直接Sleep。
Select模型
int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout ); |
Select模型是非阻塞的,函數(shù)內(nèi)部自動檢測WSAEWOULDBLOCK狀態(tài),還能有超時設定。對read,write,except三種事件進行分別檢測,except指帶外數(shù)據(jù)可讀取,read和write的定義是廣義的,accept,close等消息也納入到read。
Select函數(shù)使用fd_set結構,它的結構非常的簡單,只有一個數(shù)組和計數(shù)器。
Timeval結構里可以設置超時的時間。
Select函數(shù)返回值表示集合中有事件觸發(fā)的sock總數(shù),其余操作使用fd_set的宏來完成。
#ifndef FD_SETSIZE #define FD_SETSIZE 64 #endif /* FD_SETSIZE */typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set; FD_CLR(s, *set) FD_ISSET(s, *set) FD_SET(s, *set) FD_ZERO(*set) |
Select模型流程如下:
fd_set fdread; timeval tv = {1, 0}; while (1) { // 初始化fd_set FD_ZERO(&fdread); for (int i = 0; i < nSock; i ++) FD_SET(socks[i], &fdread); // 等待事件觸發(fā),或超時返回 int ret = select(0, &fdread, NULL, NULL, &tv); for (int i = 0; ret > 0 && i < nSock; i ++) // 檢測哪個sock有事件觸發(fā) if (FD_ISSET(socks[i], &fdread)) { read_buf(socks[i]); ret –; } } |
其實select的原理就是對sock集合進行掃描,有事件或者超時則退出,所以select的效率也是和sock數(shù)量成線性關系,而且需要我們自己循環(huán)檢查哪個sock有事件發(fā)生。
它的優(yōu)點是模型簡單,過程清晰,容易管理,支持多個sock服務。缺點也很明顯,本質還是個循環(huán)的改進版本,而且fd_set里最多只能放64個sock,還有它無法很好的支持sock事件的先后順序。
WSAAsynSelect模型
WSAAsynSelect是Windows特有的,可以在一個套接字上接收以Windows消息為基礎的網(wǎng)絡事件通知。該模型的實現(xiàn)方法是通過調(diào)用WSAAsynSelect函數(shù)自動將套接字設置(轉變)為非阻塞模式,并向Windows注冊一個或多個網(wǎng)絡事件lEvent,并提供一個通知時使用的窗口句柄hWnd。當注冊的事件發(fā)生時,對應的窗口將收到一個基于消息的通知wMsg。
int WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent ); |
WSAAsyncSelect模型流程如下:
#define WM_SOCKET WM_USER+1 int WINAPI WinMain(HINSTANCE hINstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { SOCKET Listen; HWND Window; // 創(chuàng)建窗口,綁定上WinProc // 創(chuàng)建sock WSAStartup(…); Listen = Socket(); bind(…); WSAAsyscSelect(Listen, Window, WM_SOCKET, FD_ACCEPT | FD_CLOSE); listen(Listen, 5); } BOOL CALLBACK WinProc(HWND hDlg, WORD wMsg, WORD wParam, DWORD lParam) { SOCKET Accept; switch(wMsg) { case WM_SOCKET: // lParam的高字節(jié)包含了可能出現(xiàn)的任何的錯誤代碼 // lParam的低字節(jié)指定已經(jīng)發(fā)生的網(wǎng)絡事件 // 發(fā)生錯誤 if(WSAGETSELECTERROR(lParam)) { closesocket… } // 事件觸發(fā) switch( WSAGETSELECTEVENT(lParam) ) { case FD_ACCEPT: case FD_READ: case FD_WRITE: } } } |
WSAAsyncSelect是模仿Windows消息機制來實現(xiàn)的,使用起來很方便,僅僅只是在消息處理中加入了對WM_SOCKET的處理,這樣就能嚴格得按先后順序處理sock事件。
MFC中的CSOCKET也采用了這個模型。
lEvent事件表:
FD_READ |
應用程序想要接收有關是否可讀的通知,以便讀入數(shù)據(jù) |
FD_WRITE |
應用程序想要接收有關是否可寫的通知,以便寫入數(shù)據(jù) |
FD_OOB |
應用程序想接收是否有帶外(OOB)數(shù)據(jù)抵達的通知 |
FD_ACCEPT |
應用程序想接收與進入連接有關的通知 |
FD_CONNECT |
應用程序想接收與一次連接或者多點join操作完成的通知 |
FD_CLOSE |
應用程序想接收與套接字關閉有關的通知 |
FD_QOS |
應用程序想接收套接字“服務質量”(QoS)發(fā)生更改的通知 |
FD_GROUP_QOS |
應用程序想接收套接字組“服務質量”發(fā)生更改的通知(現(xiàn)在沒什么用處,為未來套接字組的使用保留) |
FD_ROUTING_INTERFACE_CHANGE |
應用程序想接收在指定的方向上,與路由接口發(fā)生變化的通知 |
FD_ADDRESS_LIST_CHANGE |
應用程序想接收針對套接字的協(xié)議家族,本地地址列表發(fā)生變化的通知 |
只有在以下3種條件下,會發(fā)送FD_WRITE事件:
- 使用connect。連接首次被建立。
- 使用accept。套接字被接受。
- 使用send,sendto。
它的缺點就是,每個sock事件處理需要一個窗口句柄,如果sock很多的情況下,資源和性能可想而知了。
WSAEventSelect模型
WSAEventSelect模型類似WSAAsynSelect模型,但最主要的區(qū)別是網(wǎng)絡事件發(fā)生時會被發(fā)送到一個Event對象句柄,而不是發(fā)送到一個窗口。這樣你就可以使用Event對象的特性了。但WSAEventSelect模型明顯復雜很多。
它需要由以下函數(shù)一起完成。
// 1. 創(chuàng)建事件對象來接收網(wǎng)絡事件: WSAEVENT WSACreateEvent( void ); // 2. 將事件對象與套接字關聯(lián),同時注冊事件,使事件對象的工作狀態(tài)從未傳信轉變未已傳信。 int WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents ); // 3. I/O處理后,設置事件對象為未傳信 BOOL WSAResetEvent( WSAEVENT hEvent ); // 4. 等待網(wǎng)絡事件來觸發(fā)事件句柄的工作狀態(tài): DWORD WSAWaitForMultipleEvents( DWORD cEvents,const WSAEVENT FAR * lphEvents, BOOL fWaitAll,DWORD dwTimeout, BOOLfAlertable ); // 5. 獲取網(wǎng)絡事件類型 int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents ); |
WSACreateEvent其實跟CreateEvent的效果類似,返回的WSAEVENT類型其實就是HANDLE類型,所以可以直接使用CreateEvent創(chuàng)建特殊的Event。
sock和Event對象是對應的,當一個套接字有事件發(fā)生,WSAWaitForMultipleEvents返回相應的值,通過這個值來索引這個套接字。 但它也和select一樣,在Event數(shù)組大小上也有限制,MAXIMUM_WAIT_OBJECTS的值為64。
有了Event對象的支持,signaled/non-signaled和manual reset/auto reset的概念也就可以應用到程序里,這樣能使sock事件處理的方式比較豐富靈活。而且它也能嚴格按先后順序處理sock事件。
閃電郵PushMail的處理就是WSAEventSelect模型。
Over-Lapped IO模型
它和之前模型不同的是,使用重疊模型的應用程序通知緩沖區(qū)收發(fā)系統(tǒng)直接使用數(shù)據(jù),也就是說,如果應用程序投遞了一個10KB大小的緩沖區(qū)來接收數(shù)據(jù),且數(shù)據(jù)已經(jīng)到達套接字,則該數(shù)據(jù)將直接被拷貝到投遞的緩沖區(qū)。之前的模型都是在套接字的緩沖區(qū)中,當通知應用程序接收后,在把數(shù)據(jù)拷貝到程序的緩沖區(qū)。
這種模型適用于除WindowsCE外的其他Windows平臺,該模型是以Windows的重疊IO機制為基礎,通過ReadFile和WriteFile,針對設備執(zhí)行IO操作。
早先這種機制是用于文件IO,在Socket IO和文件IO統(tǒng)一接口之后,這種機制也被引入Socket IO。但這類模型的實現(xiàn)就相對復雜多了。
有兩個方法可以實現(xiàn)重疊IO請求的完成情況(接到重疊操作完成的通知):
- 事件對象通知(event object notification)。
- 完成例程(completion routines)。注意,這里并不是完成端口。
WSAOVERLAPPED
重疊結構是不得不提的,之后的完成端口模型也需要用到。這個結構等同于OVERLAPPED。
typedef struct _WSAOVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; WSAEVENT hEvent; // 只關注這個參數(shù),用來關聯(lián)WSAEvent對象 } WSAOVERLAPPED, *LPWSAOVERLAPPED; |
使用重疊結構,我們常用的send, sendto, recv, recvfrom也都要被WSASend, WSASendto, WSARecv, WSARecvFrom替換掉了,是因為它們的參數(shù)中都有一個Overlapped參數(shù)。
int WSARecv( SOCKET s, // [in] 套接字 LPWSABUF lpBuffers, // [in,out] 接收緩沖區(qū),WSABUF的數(shù)組 DWORD dwBufferCount, // [in] 數(shù)組中WSABUF的數(shù)量 LPDWORD lpNumberOfBytesRecvd, // [out] 此刻函數(shù)所接收到的字節(jié)數(shù) LPDWORD lpFlags, // [in,out] 這里設置為0 即可 LPWSAOVERLAPPED lpOverlapped, // [in] 綁定重疊結構 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine// [in] 完成例程中將會用到的參數(shù) ); |
沒有錯誤且收取立刻完成時,返回值為0,否則是SOCKET_ERROR。常見的錯誤碼是WSA_IO_PENDING,表示重疊操作正在進行。相應的其他函數(shù)也是類似參數(shù),具體參考MDSN。
獲取重疊操作的結果,由WSAWaitForMultipleEvents函數(shù)來完成。
BOOL WSAGetOverlappedResult( SOCKET s, // [in] 套接字 LPWSAOVERLAPPED lpOverlapped, // [in] 要查詢的重疊結構的指針 LPDWORD lpcbTransfer,// [out] 本次重疊操作的實際接收(或發(fā)送)的字節(jié)數(shù) BOOL fWait, // [in] 設置為TRUE,除非重疊操作完成,否則函數(shù)不會返回 // 設置FALSE,而且操作仍處于掛起狀態(tài),那么函數(shù)就會返回FALSE,錯誤為WSA_IO_INCOMPLETE LPDWORD lpdwFlags // [out] 負責接收結果標志 ); |
事件通知
事件等待函數(shù)和WaitForMultipleObjects類似。
DWORD WSAWaitForMultipleEvents( DWORD cEvents, // [in] 等候事件的總數(shù)量 const WSAEVENT* lphEvents, // [in] 事件數(shù)組的指針 BOOL fWaitAll, // [in] 是否等待所有事件 DWORD dwTimeout, // [in] 超時時間 BOOL fAlertable // [in] 在完成例程中會用到這個參數(shù) ); |
返回值有這么幾個:
WSA_WAIT_TIMEOUT |
超時,我們要繼續(xù)Wait |
WSA_WAIT_FAILED |
出現(xiàn)錯誤 |
WAIT_IO_COMPLETION |
一個或多個完成例程入隊列執(zhí)行 |
WSA_WAIT_EVENT_0 ~ (WSA_WAIT_EVENT_0 + cEvents – 1) |
觸發(fā)的事件下標 |
事件通知的重疊IO模型大致流程如下:
// 1. 建立并初始化buf和overlap WSAOVERLAPPED Overlap; WSABUF DataBuf; char* SendBuf = new char[BufLen]; DWORD Flags = 0;DataBuf.len = BufLen; DataBuf.buf = SendBuf; Overlap.hEvent = EventArray[dwEventTotal ++] = WSACreateEvent(); // 2. 在套接字上投遞WSARecv請求 int ret = WSARecv(Sock, &DataBuf, 1, &NumberOfBytesRecvd, &Flags, &Overlap, NULL); if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) error_handle(…); // 3. 等待事件通知 DWORD dwIndex = WSAWaitForMultipleEvents(dwEventTotal,EventArray, FALSE, WSA_INFINITE, FALSE); if (dwIndex == WSA_WAIT_FAILED || dwIndex == WSA_WAIT_TIMEOUT) error_handle(…); dwIndex -= WSA_WAIT_EVENT_0; // 4. 重置事件對象 WSAResetEvent(EventArray[dwIndex]); // 5. 取得重疊調(diào)用的返回狀態(tài) DWORD dwBytesTransferred; WSAGetOverlappedResult(Sock, Overlap, &dwBytesTransferred, TRUE, &Flags); if (dwBytesTransferred == 0) closesocket(Sock); dosomething(…); |
如果是服務端使用事件通知模型,則需要再起一個線程來循環(huán)Wait事件通知,主線程則接受請求的連接。
實際編碼過程中,要注意緩沖區(qū)不要搞錯,因為全都需要自己來管理,稍有不慎就容易寫臟數(shù)據(jù)和越界。還要注意WSARecv時,可能立即有數(shù)據(jù)返回的情況,即返回值為0且NumberOfBytesRecvd > 0。
完成例程
完成例程(Completion Routine),不是完成端口。它是使用APC(Asynchronous Procedure Calls)異步回調(diào)函數(shù)來實現(xiàn),大致流程和事件通知模型差不多,只不過WSARecv注冊時,加上了lpCompletionRoutine參數(shù)。
Void CALLBACK CompletionROUTINE( DWORD dwError, // [in] 標志咱們投遞的重疊操作完成的狀態(tài) DWORD cbTransferred, // [in] 重疊操作期間,實際傳輸?shù)淖止?jié)量是多大 LPWSAOVERLAPPED lpOverlapped, // [in] 傳遞到最初IO調(diào)用的重疊結構 DWORD dwFlags // [in] 返回操作結束時可能用的標志(一般沒用) ); |
但完成例程有一個比較隱晦的地方,就是APC機制本身。
APC機制
ReadFileEx / WriteFileEx在發(fā)出IO請求的同時,提供一個回調(diào)函數(shù)(APC過程),當IO請求完成后,一旦線程進入可告警狀態(tài),回調(diào)函數(shù)將會執(zhí)行。
以下五個函數(shù)能夠使線程進入告警狀態(tài):
SleepEx
WaitForSingleObjectEx
WaitForMultipleObjectsEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
線程進入告警狀態(tài)時,內(nèi)核將會檢查線程的APC隊列,如果隊列中有APC,將會按FIFO方式依次執(zhí)行。如果隊列為空,線程將會掛起等待事件對象。以后的某個時刻,一旦APC進入隊列,線程將會被喚醒執(zhí)行APC,同時等待函數(shù)返回WAIT_IO_COMPLETION。

回到完成例程的話題上。
需要一個輔助線程,輔助線程的工作是判斷有沒有新的客戶端連接被建立,如果有,就為那個客戶端套接字激活一個異步的WSARecv操作,然后調(diào)用SleepEx使線程處于一種可警告的等待狀態(tài),以使得I/O完成后 CompletionROUTINE可以被內(nèi)核調(diào)用,而CompletionROUTINE會在當初激活WSARecv異步操作的代碼的同一個線程之內(nèi)!而且調(diào)用SleepEx時,需要把bAlertable參數(shù)設為TRUE,這樣當有APC喚醒時立即調(diào)用完成例程,否則例程就不會被執(zhí)行。當然也可以使用WSAWaitForMultipleEvents函數(shù),但這樣就需要一個事件對象。

從圖中就能看到CompletionROUTINE是在輔助線程(調(diào)用過WSARecv)里執(zhí)行的。
Completion Port模型
“完成端口”模型是迄今為止最為復雜的一種I/O模型。
假若一個應用程序同時需要管理為數(shù)眾多的套接字,那么采用這種模型,往往可以達到最佳的系統(tǒng)性能!它能最大限度的減少上下文切換的同時最大限度的提高系統(tǒng)并發(fā)量。但不幸的是,該模型只適用于Windows NT和Windows 2000操作系統(tǒng)。
因其設計的復雜性,只有在你的應用程序需要同時管理數(shù)百乃至上千個套接字的時候,而且希望隨著系統(tǒng)內(nèi)安裝的CPU數(shù)量的增多,應用程序的性能也可以線性提升,才應考慮采用“完成端口”模型。
要記住的一個基本準則是,假如要為Windows NT或Windows 2000開發(fā)高性能的服務器應用,同時希望為大量套接字I/O請求提供服務(Web服務器便是這方面的典型例子),那么I/O完成端口模型便是最佳選擇!
完成端口是一種WINDOWS內(nèi)核對象。完成端口用于異步方式的重疊I/O。簡單地,可以把完成端口看成系統(tǒng)維護的一個隊列,操作系統(tǒng)把重疊IO操作完成的事件通知放到該隊列里,由于是暴露 “操作完成”的事件通知,所以命名為“完成端口”(Completion Ports)。
完成端口內(nèi)部提供了線程池的管理,可以避免反復創(chuàng)建線程的開銷,同時可以根據(jù)CPU的個數(shù)靈活的決定線程個數(shù),而且可以讓減少線程調(diào)度的次數(shù)從而提高性能。

它需要以下函數(shù)的支持,CreateIoCompletionPort函數(shù)用于創(chuàng)建和綁定完成端口。
HANDLE CreateIoCompletionPort( HANDLE FileHandle, // [in] IO句柄對象,這里是套接字 HANDLE ExistingCompletionPort, // [in] 完成端口 ULONG_PTR CompletionKey, // [in] 自定義數(shù)據(jù)指針 DWORD NumberOfConcurrentThreads // [in] 最大線程數(shù),0為自動 ); |
我們還需要類似WSAGetOverlappedResult的函數(shù)來獲取完成端口的狀態(tài)。
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // [in] 完成端口 LPDWORD lpNumberOfBytes, // [out] 此次IO操作的字節(jié)數(shù) PULONG_PTR lpCompletionKey, // [out] 自定義數(shù)據(jù)指針,CreateIoCompletionPort初始化的 LPOVERLAPPED* lpOverlapped, // [out] 投遞請求時的重疊結構指針 DWORD dwMilliseconds // [in] 超時設置 ); |
還有PostQueuedCompletionStatus函數(shù),能模擬一個完成的重疊I/O操作。我們可以當成類似PostMessage的函數(shù),以此控制工作線程。
BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, // [in] 完成端口 DWORD dwNumberOfBytesTransferred, // [in] 此次IO操作的字節(jié)數(shù) ULONG_PTR dwCompletionKey, // [in] 自定義數(shù)據(jù)指針 LPOVERLAPPED lpOverlapped // [in] 重疊結構指針 ); |
完成端口模型大致流程如下:
// 1. 參數(shù)設空,就能創(chuàng)建完成端口 HANDLE CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,0); // 2. 創(chuàng)建工作線程 DWORD dwThreadId; SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); for (int i = 0; i < sysinfo.dwNumberOfProcessors; i++) CreateThread(NULL, 0, iocp_work_thread, CompletionPort, 0, &dwThreadId);// 3. 建立并初始化buf和overlap(參照重疊IO) // 4. 將套接字綁定到完成端口 CreateIoCompletionPort((HANDLE)Sock,CompletionPort,Sock,0); // 5. 在套接字上投遞WSARecv請求(參照重疊IO) // 6. 在工作線程中取本次I/O的相關信息 GetQueuedCompletionStatus(CompletionPort,&dwBytesTransferred, (DWORD*)&Sock,(LPOVERLAPPED*)&lpPerIOData,INFINITE); if (dwBytesTransferred == 0) closesocket(Sock); dosomething(…); |
測試圖例
來自于《Windows網(wǎng)絡編程》的數(shù)據(jù)。

- 阻塞模型難以應對大規(guī)模的客戶連接,因為它在創(chuàng)建線程上耗費了太多的系統(tǒng)資源。因此,服務器創(chuàng)建太多的線程后,再調(diào)用CreateThread函數(shù)時,將返回ERROR_NOT_ENOUGH_MEMORY的錯誤,那些發(fā)出連接請求的客戶則收到WSAECONNREFUSED的錯誤提示,表示連接的嘗試被拒絕。其并發(fā)處理量是極難突破的。
- 非阻塞模型和Select模型的性能要比阻塞模式稍好,但是占用了太多的CPU處理時間。瓶頸在于,fd_set集合的線性掃描上。還需要注意的一個問題就是,非分頁池(即直接在物理內(nèi)存中分配的內(nèi)存)的使用極高。這是因為AFD(Ancillary Function Driver,由afd.sys提供的支持Windows Sockets應用程序的底層驅動程序,其中運行在內(nèi)核模式下afd.sys驅動程序主要管理Winsock TCP/IP通信)和TCP都將使用I/O緩存,因為服務器讀取數(shù)據(jù)的速度是有限的,相對于CPU的處理速度而言,I/O基本是零字節(jié)的吞吐量。
- 基于Windows消息機制的WSAAsyncSelect模型能夠處理一定的客戶連接量,但是擴展性也不是很好。因為消息泵很快就會阻塞,降低了消息處理的速度。在幾次測試中,服務器只能處理大約1/3的客戶端連接。過多的客戶端連接請求都將返回錯誤提示碼WSAECONNREFUSED。上表中的數(shù)據(jù)可以發(fā)現(xiàn),對那些已經(jīng)建立的連接,其平均吞吐量也是極低的。
- 基于事件通知的WSAEventSelect模型表現(xiàn)得出奇的不錯。在所有的測試中,大多數(shù)時候,服務器基本能夠處理所有的客戶連接,并且保持著較高的數(shù)據(jù)吞吐量。這種模型的缺點是,每當有一個新連接時,需要動態(tài)管理線程池,因為每個線程只能夠等待64個事件對象。但最后,服務器不能再接受更多的連接,原因是WSAENOBUFS(無可用的緩沖區(qū)空間),套接字無法創(chuàng)建。另外,客戶端程序也達到了極限,不能維持已經(jīng)建立的連接。
- 事件通知的重疊I/O模型和WSAEventSelect模型在伸縮性上差不多。這兩種模型都依賴于等待事件通知的線程池,處理客戶通信時,大量線程上下文的切換是它們共同的制約因素。重疊I/O模型和WSAEventSelect模型的測試結果很相似,都表現(xiàn)得不錯,直到線程數(shù)量超過極限。
- 例程通知的重疊I/O模型,性能和事件通知的重疊I/O模型相同,但因為以下幾個原因,也不是開發(fā)高性能服務器的最佳選擇。首先,許多擴展功能不允許使用APC完成通知。其次,由于APC在系統(tǒng)內(nèi)部特有的處理機制,應用程序線程可能無限等待而得不到完成通知。當一個線程處于“可警告狀態(tài)”時,所有掛起的APC按照先進先出的順序(FIFO)接受處理。
- 完成端口模型的是所有I/O模型中性能最佳的。內(nèi)存使用率(包括用戶分頁池和非分頁池)基本差不多。真正不同的地方,在于對CPU的占用。完成端口模型只占用了60%的CPU,但是在維持同樣規(guī)模的連接量時,另外兩種模型(基于事件通知的重疊I/O模型和WSAEventSelect模型)占用更多的CPU。完成端口的另外一個明顯的優(yōu)勢是,它維持更大的吞吐量。
總結
客戶端的選擇
為了能在一定程度上提升性能,建議使用重疊IO模型或者WSAEventSelect模型。
如果是窗口程序,且socket不多的情況下,可以使用WSAAsyncSelect模型。
當然,如果性能啥的都不需要考慮的,那簡潔的Select模式值得被考慮。
服務端的選擇
既然是服務端,必然要需要性能不錯的。
重疊IO模型可以使你在給定的時間段內(nèi)同時控制多個套接字。
但是,如果服務器在任意時間里都有大量IO請求,那就用完成端口模型。
參考
[1] Windows核心編程;
[2] 手把手教你玩轉SOCKET模型之重疊I/O篇;
http://dev.csdn.net/htmls/39/39122.html
[3] 手把手教你玩轉網(wǎng)絡編程模型之完成例程(Completion Routine)篇;
http://blog.csdn.net/PiggyXP/archive/2009/02/19/3910726.aspx
[4] Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports;
http://msdn.microsoft.com/zh-cn/magazine/cc302334(en-us).aspx
[5] Inside I/O Completion Ports;
http://hi.baidu.com/jrckkyy/blog/item/401422527c131b070df3e37b.html
[6] Windows 2000 非分頁池被 Afd.sys 耗盡;
http://support.microsoft.com/kb/296265/zh-cn
[7] WinSock五種I/O模型的性能分析;
http://www.rover12421.com/2010/04/02/winsock%E4%BA%94%E7%A7%8Dio%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90.html