WinSock三种选择I/O模型
在《套接字socket及C/S通信的基本概念》和《WinSock編程基礎》中,我們介紹了套接字的基本概念和WinSock API的基本調用規范。我們討論了阻塞模式/非阻塞模式和同步I/O和異步I/O等話題。
從概念的角度,阻塞模式因其簡潔易用便于快速原型化,但在應付建立連接的多個套接字或在數據的收發量不均、時間不定時卻極難管理。另一方面,我們需要對非阻塞模式套接字的?WinSock API調用頻繁返回的WSAEWOULDBLOCK錯誤加以判斷處理也顯得難于管理。WinSock套接字I/O模型提供了管理I/O完成通知的方法,幫助應用程序判斷套接字何時可供讀寫。
共有6中類型的套接字I/O模型可讓WinSock應用程序對I/O進行管理,它們包括blocking(阻塞)、select(選擇)、WSAAsyncSelect(異步選擇)、WSAEventSelect(事件選擇)、overlapped(重疊)以及completionport(完成端口)。
本文討論三種選擇(都帶select)模型。
?
1.基于套接字集合的select模型
(1)select模型概述
該模型時最初設計是在不使用UNIX操作系統的計算機上實現的,它們采用的是Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字調用過程中被無辜“鎖定”的應用程序,采取一種有序的方式,同時進行對多個套接字的管理。
之所以稱其為“select模型”,是由于它的“中心思想”便是利用select函數,實現對I/O的管理!?使用select模型,一般需要調用ioctlsocket函數將一個套接字從鎖定模式切換為非鎖定模式。
//?將套接字s設置為非阻塞模式
unsigned?long?nonBlocking?= 1;
ioctlsocket(s,?FIONBIO, (u_long*)&nonBlocking);
select模型本質上是一種分類處理思想,預先聲明幾個FD_SET(fd_set結構)集合(使用FD_ZERO初始化),例如ReadSet,WriteSet,然后調用宏FD_SET(s,&ReadSet)將關注FD_READ事件的套接字s添加到ReadSet集合,調用宏FD_SET(s,&WriteSet)將關注FD_WRITE事件的套接字s添加到WriteSet集合。其中宏FD_SET(SOCKET s, fd_set set)將s添加到set集合。從根本上說,fd_set數據類型代表著一系列按關注事件分類的套接字集合。
然后再調用select函數,對聲明的集合ReadSet或WriteSet進行掃描,其函數原型如下:
int?WSAAPI?select(
int?nfds,
????????fd_set?FAR?*?readfds,
????????fd_set?FAR?*?writefds,
????????fd_set?FAR?*exceptfds,
????????const struct?timeval?FAR?*?timeout?);
其中,第一個參數?nfds會被忽略,一般賦值0。之所以仍然要提供這個參數,只是為了保持與早期的Berkeley套接字應用程序的兼容。其他的三個fd_set參數,一個用于檢查可讀性(readfds),一個用于檢查可寫性(writefds),另一個用于例外數據(exceptfds)。最后一個參數timeout用于決定select()等待I/O操作完成時最大忍耐時間,在等待時間內select()函數阻塞。當timeout為空時,無限等待直到有I/O完成;當*timeout=0時,select()函數立即返回,用做輪詢。
例如我們只關注FD_READ事件,則select(0,&ReadSet,NULL,NULL,NULL)。WinSock要求這三個fd_set參數至少有一個不為NULL,而在其他平臺下經常只關注最后一個參數用于實現相當于sleep()的延時功能。
select()函數用于判斷套接字上是否存在數據(any data incoming?)或者能否向一個套接字寫數據(output buffer available?)。調用select()會修改每個fd_set結構,它掃描注冊到集合ReadSet和WriteSet中的套接字是否有讀寫事件發生,若有,則對集合進行更新,刪除那些不存在待決I/O操作的套接字句柄。select()完成后,返回所有仍在fd_set集合中的套接字句柄總數。
然后,我們需要遍歷查詢之前注冊到某個集合中的套接字是否仍為其中一部分。這需要調用FD_ISSET(SOCKET s, fd_set set)來測試套接字是否屬于關注同類事件的套接字集合set。若是,則對待決的I/O進行處理(再次recv()/send()執行真正的拷貝)。
(2)select模型的應用實例
由于select模型源于Berkeley套接字方案,故常用作實現跨平臺的POLL組件。在Linux下,select和poll是一個級別的,以下梳理了經典開源通信庫中用到的select模型。
(1)curl/lib/select.h(c)中的Curl_socket_ready()調用。
/*
?* This is an internal function used for waiting for read or write
?* events on a pair of file descriptors.??It uses poll() when a fine
?*?poll()?is available, in order to avoid limits with FD_SETSIZE,
?* otherwise?select()?is used.
*/
(2)thttpd/fdwatch.h(c)中fdwatch()中的WATCH()調用。
/* fdwatch.h - header file for fdwatch package
**
** This package abstracts the use of the select()/poll()/kqueue()
** system calls.??The basic function of these calls is to watch a set
** of file descriptors for activity.
**/
(3)Apache Httpd/httpd/srclib/apr/下的
include/apr_poll.h中定義了Pollset Methods?的枚舉變量apr_pollset_method_e。
poll/unix/select.c中的apr_poll()和impl_pollset_poll()調用。
(4)nginx/Windows使用的是?Win32的?API?,而不是Cygwin模擬的。當前只有select?這種網絡模式,所以你不能指望它擁有高性能和高可擴展性。
nginx/src/event/modules
ngx_poll_module.c
ngx_select_module.c/ngx_win32_select_module.c中的ngx_select_process_events()調用。
(5)其他
C++ Sockets Library中的SocketHandler::ISocketHandler_Select()
Jrtplib中的RTPUDPv4Transmitter::WaitForIncomingData()
live555中的blockUntilReadable()和BasicTaskScheduler::SingleStep()
PeerCast中的WSAClientSocket::checkTimeout()
……
(3)select模型的局限性
select模型的優勢在于能夠從單個線程的多個套接字上進行多重連接及I/O管理,這樣就避免了伴隨阻塞套接字和多重連接的線程劇增。但可以加到fd_set結構中的最大套接字數量FD_SETSIZE在WINSOCK2.H中定義為64,底層程序強加了一個fd_set的最大值,通常情況下是1024。當然,我們可以分批FD_SET()→select()→FD_ISSET()來突破此限制。
select模型可以跨平臺,對于千路并發的中小型服務器差不多夠用。在具體平臺開發網絡通信程序時,可以結合平臺特性,發揮平臺機制優勢。
?
2.基于Windows消息處理WSAAsyncSelcet模型
WinSock提供了一個有用的異步I/O通知模型。利用這個模型,應用程序可在一個套接字上,接收以Windows消息為基礎的網絡事件通知。具體的做法是在創建好一個套接字后,調用WSAAsyncSelect函數,它的函數原型如下:
int?WSAAPI?WSAAsyncSelect(
SOCKET?s,
HWND?hWnd,
u_int?wMsg,
long?lEvent);
調用WSAAsyncSelect()函數時,套接字即自動設置為非阻塞模式。
這個函數完成的功能是,將參數一所指定的套接字s(包括監聽套接字和會話套接字)上感興趣的一系列網絡事件以位或|掩碼組合形式(FD_XXX|FD_XXX)注冊到參數四lEvent中,然后將lEvent中的網絡事件通知綁定到參數二指定的窗口hWnd和參數三指定的自定義消息wMsg進行處理。
對于標準的Windows例程(常稱為“WindowProc”),這個模型充分利用了Windows窗口消息處理機制。該模型亦得到了MFC(Microsoft Foundation Class,微軟基礎類庫)對象CSocket的采納。
由于使用Windows消息機制,故要想在應用程序中使用WSAAsyncSelect模型,首先必須用CreateWindow()函數創建一個窗口,再為該窗口提供一個窗口過程處理函數(WindowProc)。然后在WindowProc中讀取自定義的WM_SOCKET消息內容,針對不同的網絡事件進行相關處理。參考《VC網絡通信API概覽》中的CAsyncSocket/CSocket。
網絡事件消息的wParam參數為對應發生該事件的套接字句柄,lParam參數的高字位(一般用WSAGETSELECTERROR宏取得HIWORD)包含出錯碼,lParam參數的低字位(一般用WSAGETSELECTEVENT宏取得LOWORD)則標識了網絡事件代碼(FD_XXX)。一般先檢查高位,再檢查低位進行網絡事件的處理。在實際使用時,要注意各個網絡事件(FD_XXX)發生的時機判斷并進行合理的I/O處理。
????WSAAsyncSelect模型適合合作性的多任務消息GUI環境,優點是它可以在系統開銷不大的情況下同時處理多個連接,而select模型則需要建立fd_set結構。缺點是即使不需要窗口的CUI應用程序也必須創建一個額外的暗窗口。同時,由于Windows消息泵本身的局限性,單窗口程序處理成千上萬個套接字中的所有事件也可能成為性能瓶頸。
?
3.基于事件通知的WSAEventSelect模型
在WSAAsyncSelcet模型中,當利用WSAAsyncSelect()函數將套接字及其關注的網絡事件綁定到一個窗口消息后,當有網絡事件發生時,窗口會發出消息通知。我們還可以使用一種基于事件對象傳信狀態來發出網絡事件通知的WSAEventSelect模型。
首先調用與WSAAsyncSelect同工的WSAEventSelect函數,其原型如下:
int?WSAAPI?WSAEventSelect(
SOCKET?s,
????????WSAEVENT?hEventObject,
????????long?lNetworkEvents?);
調用WSAEventSelect()函數時,套接字即自動設置為非阻塞模式。
調用WSAEventSelect()函數將參數一指定的套接字s關注的網絡事件以位或|掩碼組合形式(FD_XXX|FD_XXX)注冊到參數三lNetworkEvents,并將該套接字綁定到參數二指定的事件對象hEventObject。這樣當lNetWorkEvents中的事件發生時,Windows將hEventObject置信(由Unsignaled變為Signaled)。
#define?WSAEVENT????????????????HANDLE
當事件對象受信后,我們需要獲得這個通知,這需要調用等待事件對象的同步函數,主要有WaitForSingleObject、WaitForMultipleObjects和WSAWaitForMultipleEvents。
函數WaitForSingleObject定義如下:
WINBASEAPI?DWORD?WINAPI
WaitForSingleObject(
HANDLE?hHandle,
DWORD?dwMilliseconds?);
對于函數WaitForSingleObject,如果超過參數二dwMilliseconds設定的時限,函數返回WAIT_TIMEOUT;在限定時限內,只有當其等待的對象受信(例如線程返回,事件受信等)后,該函數才返回,返回值為WAIT_OBJECT_0,此時,Windows將自動重置該對象。
函數WaitForMultipleObjects定義如下:
WINBASEAPI?DWORD?WINAPI
WaitForMultipleObjects(
????????DWORD?nCount,
????????CONST?HANDLE?*lpHandles,
????????BOOL?bWaitAll,
????????DWORD?dwMilliseconds?);
????WinSock中的WSAWaitForMultipleEvents函數原型如下:
DWORD?WSAAPI?WSAWaitForMultipleEvents(
????????DWORD?cEvents,
????????const?WSAEVENT?FAR?*?lphEvents,
????????BOOL?fWaitAll,
????????DWORD?dwTimeout,
????????BOOL?fAlertable);
和WaitForSingleObject不同的是,WaitForMultipleObjects和WSAWaitForMultipleEvents支持在多個對象的等待。它們支持nCount/cEvents和lpHandles/lphEvents參數定義了由HANDLE/WSAEVENT對象構成的一個數組。在這個數組中,nCount/cEvents指定的是事件對象的數量,而lphEvents對應的是一個指針,用于直接引用該數組。要注意的是,WaitForMultipleObjects/WSAaitForMultipleEvents只能支持由MAXIMUM_WAIT_OBJECTS/WSA_MAXIMUM_WAIT_EVENTS對象規定的一個最大值,在此定義成64個。因此,針對發出WSAWaitForMultipleEvents調用的每個線程,該I/O模型一次最多都只能支持64個套接字。假如想讓這個模型同時管理不止64個套接字,必須創建額外的工作者線程,以便等待更多的事件對象。
WSAWaitForMultipleEvents的最后一個參數是fAlertable,在我們使用WSAEventSelect模型的時候,它是可以忽略,常設為FALSE,該參數主要用于重疊I/O的完成例程處理模型中使用。其他參數意義同WaitForMultipleObjects。
參數一指定了對象個數,參數二則往往是一個對象數組。同樣,若超過參數四設定的時限,它們都會返回WSA_WAIT_TIMEOUT。在設定時限內,若參數三WaitAll =?FALSE,則只要其等待的事件對象中有一個受信,該函數即返回WAIT_OBJECT_i(i=[0,nCount-1])或WSA_WAIT_EVENT_i(i=[0,cEvents-1]);若WaitAll =TRUE,則要等到所有對象都受信后該函數才返回。直到所有等待的對象都受信,系統才將所有受信事件對象狀態重置(由Signaled變為Unsignaled)。應用程序往往根據返回的索引(相對預定義其實索引)使用switch-case分發流程處理不同的事件。對于多個事件,往往WaitAll被設置成FALSE,這樣只要有事件發生就及時處理。
調用WSAWaitForMultipleEvents返回受信事件對象的索引,根據索引也可以知道其對應的套接字。因為在實際程序中,一個套接字綁定一個事件對象:Socket[index]←→WSAEvent[index]。
在Windows消息機制處理WinSock事件中,有網絡事件發生時,Windows根據消息號取出消息內容進行處理。在事件通知模型中,當調用WSAWaitForMultipleEvents接到和消息通知對應的事件通知后,就需要查獲發生的網絡事件(類比消息內容)。WSAEnumNetworkEvents函數負責查獲一個套接字上發生的網絡事件,其原型如下:
WINSOCK_API_LINKAGE?int?WSAAPI
WSAEnumNetworkEvents(
SOCKET?s,
????????WSAEVENT?hEventObject,
????????LPWSANETWORKEVENTS?lpNetworkEvents?);
傳遞套接字參數s,當然這里是上一步中WSAWaitForMultipleEvents?返回的Index對應的socket,調用WSAEnumNetworkEvents函數來獲取套接字s上所發生的事件,并將其保存到lpNetworkEvents結構中。
hEventObject參數則是可選的;它指定了一個事件句柄,對應于打算重設的那個事件對象。當然,如果設置該值,應該為上一步中WSAWaitForMultipleEvents返回的Index對應的socket綁定的hEventObject。由于事件對象處在一個“已傳信”(Signaled)狀態,所以可將它傳入,讓Windows將其重置為“未傳信”(Unsignaled)狀態。如果不想用hEventObject參數,那么必須調用WSAResetEvent/ResetEvent函數來重置事件對象。
將發生的網絡事件存儲在lpNetworkEvents結構中之后,接下來就需要針對事件進行處理(類比WindowProc中的消息處理)。WSANETWORKEVENTS數據結構定義如下:
typedef struct?_WSANETWORKEVENTS?{
????????long?lNetworkEvents;
????????int?iErrorCode[FD_MAX_EVENTS];
}?WSANETWORKEVENTS,?FAR?*?LPWSANETWORKEVENTS;
其中參數一lNetworkEvents存放著套接字s上發生的所有網絡事件。與注冊事件時使用位或|掩碼相反,這里一般采用位與&析取相應的網絡事件代碼,即將lNetworkEvents與FD_XXX進行位與運算,若返回1則表示有FD_XXX網絡事件發生。
這里,我們看到了FD_ISSET的影子。可以看出,WSAEventSelect是select模型和WSAAsyncSelect模型的綜合。這個模型中,每個Socket都有一個事件對象,當有網絡事件發生時,與窗口消息相對應的事件對象受信,然后遍歷該事件對象對應的套接字上發生的網絡事件。而select中是對socket按事件進行分類處理,通過FD_ISSET判斷socket是否屬于某個FD_SET。
參數二iErrorCode指定的是一個錯誤代碼數組,同lNetworkEvents中的事件關聯在一起。針對每種網絡事件,都存在著一個特殊的事件索引,名字與事件類型的名字類似,只是要在事件名字后面添加一個“_BIT”后綴字串即可。例如,對FD_READ事件類型來說,iErrorCode數組的索引標識符便是FD_READ_BIT,若無錯誤,其值為0。下述代碼片斷針對FD_READ事件的處理對此進行了闡釋:
if((NetworkEvents.lNetworkEvents?&?FD_READ)
{
//?錯誤發生
???????if(NetworkEvents.iErrorCode[FD_READ_BIT] != 0))
{
?????????printf("FD_READ failed with error %d/n",NetworkEvents.iErrorCode[FD_READ_BIT]);
}
//?處理FD_READ事件
……
}
另外,由于監聽套接字的特殊性,往往利用一個事件對象來專門通知監聽套接字上客戶端接入事件。當有客戶端請求接入(connect)時,accept返回時,我們可以調用WSASetEvent將事件置信,再調用WSAWaitForMultipleEvents獲取通知,再做一些處理。有時需要主動調用WSAResetEvent即時重置事件對象,以便使其進入下一輪詢。
?
參考:
《Network Programming for Microsoft Windows》??Anthony Jones,Jim Ohlund
總結
以上是生活随笔為你收集整理的WinSock三种选择I/O模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WinSock重叠I/O模型
- 下一篇: WSAAccept()函数使用解析