WinSock 异步I/O模型 转载
如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。Windows操作系统提供了五种I/O模型,分别是:
■ 选择(select);
■ 异步选择(WSAAsyncSelect);
■ 事件选择(WSAEventSelect);
■ 重叠I/O(Overlapped I/O);
■ 完成端口(Completion Port)(+线程池技术) 。
每一种模型适用于一种特定的应用场景。大家应该针对自己应用程序的需求,综合考虑到程序的扩展性和可移植性等因素,作出自己的选择。
① select 模型:
select模型是WinSock中应用最广泛的模型之一,核心就是select函数,它可用于判断套接字上是否存在数据,或者能否向一个套接字写入数据。这个函数可以有效地防止应用程序在套接字处于阻塞模式中时,send或recv进入阻塞状态;同时也可以防止产生大量的WSAEWOULDBLOCK错误select的优势是能够从单个线程的多个套接字上进行多重连接及I/O。这就避免了伴随阻塞套接字和多重连接的线程剧增。
② WSAAsyncSelect 模型:
因为它是以消息为基础的,关键就是WSAAsyncSelect函数,将socket消息发送到hWnd窗口上,然后在那里处理相应的FD_READ、FD_WRITE等等消息。优点:WSAAsyncSelect和WSAEventSelect模型提供了读写数据能力的异步通知,但他们不提供异步数据传送,而重叠及完成端口提供异步数据的传送。而且它可以在系统开销不大的情况下同时处理很多连接,而select模型还需要建立fd_set结构。 缺点:必须要使用一个窗口接收消息,如果处理成千上万的套接字就力不从心了。
③ WSAEventSelect 模型:
这个也是以时间为基础的网络事件通知,但是与WSAAsyncSelect不同的是,它主要是由事件对象句柄完成的,而不是通过窗口。优点:不需要窗口。缺点:每次只能等待64个事件,所以处理多个套接字时有必要组织一个线程池;所以伸缩性就不如后面的完成端口了。
④ 重叠模型:
这个模型可以使程序能达到更佳的系统性能。基本设计原理就是让应用程序使用重叠的数据结构,一次投递一个或多个I/O请求。针对这些提交的请求,在他们完成之后,应用程序可为他们提供服务。它又分为两种实现方法:在事件中使用,还有就是完成例程。
⑤ 完成端口:
完成端口提供了最好的伸缩性,往往可以使系统达到最好的性能,是处理成千上万的套接字的首选。从本质上说,完成端口模型要求创建一个windows完成端口对象,该对象通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。
1.基于事件套接字集合的select模型
select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理!最初设计该模型时,主要面向的是某些使用Unix操作系统的计算机,它们采用的是Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。
select模型本质上是一种分类处理思想,预先声明几个FD_SET(fd_set结构)集合,例如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进行扫描,其函数原型如下:
WINSOCK_API_LINKAGE 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)。例如我们只关注FD_READ事件,则select(0,&ReadSet,NULL,NULL,NULL)。一般来说,这三个fd_set参数至少有一个不为NULL。
调用select会修改每个fd_set结构,它扫描注册到集合ReadSet和WriteSet中的套接字是否有读写事件发生,若有,则对集合进行更新,即将套接字添加到集合ReadSet和WriteSet中。同时,删除那些不存在待决I/O操作的套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数。
然后,我们需要遍历查询之前注册到某个集合中的套接字是否仍为其中一部分。这需要调用FD_ISSET(SOCKET s, fd_set set)来测试套接字是否属于关注同类事件的套接字集合set。若是,则对待决的I/O进行处理。
使用select模型,可能需要调用ioctlsocket函数将一个套接字从锁定模式切换为非锁定模式。
select函数的说明:http://blog.csdn.net/rayzongx/article/details/8030101
2.基于Windows消息处理WSAAsyncSelcet模型
Winsock提供了一个有用的异步I/O模型。利用这个模型,应用程序可在一个套接字上,
接收以Windows消息为基础的网络事件通知。具体的做法是在创建好一个套接字后,调用
WSAAsyncSelect函数,它的函数原型如下:
WINSOCK_API_LINKAGE int WSAAPI
WSAAsyncSelect(
SOCKET s,
HWND hWnd,
u_int wMsg,
long lEvent);
这个函数完成的功能是,将参数一所指定的套接字s(包括监听套接字和会话套接字)上感兴趣的一系列网络事件以位或|掩码组合形式(FD_XXX | FD_XXX)注册到参数四lEvent,然后将lEvent中的网络事件通知绑定到参数二指定的窗口hWnd和参数三指定的自定义消息wMsg进行处理。
对于标准的Windows例程(常称为“WindowProc”),这个模型充分利用了Windows窗口消息处理机制。该模型亦得到了MFC(Microsoft Foundation Class,微软基础类库)对象CSocket的采纳。
由于使用Windows消息机制,故要想在应用程序中使用WSAAsyncSelect模型,首先必须用CreateWindow函数创建一个窗口,再为该窗口提供一个窗口过程处理函数(WindowProc)。然后在WindowProc中读取自定义的WM_SOCKET消息内容,针对不同的网络事件进行相关处理。
网络事件消息的wParam参数为对应发生该事件的套接字句柄,lParam参数的高字位(一般用WSAGETSELECTERROR宏取得HIWORD)包含出错码,lParam参数的低字位(一般用WSAGETSELECTEVENT宏取得LOWORD)则标识了网络事件代码(FD_XXX)。一般先检查高位,再检查低位进行网络事件的处理。
3.基于事件通知的WSAEventSelect模型
在WSAAsyncSelcet模型中,当利用WSAAsyncSelect函数将套接字及其关注的网络事件绑定到一个窗口消息后,当有网络事件发生时,窗口会发出消息通知。我们还可以使用一种基于事件对象传信状态来发出网络事件通知的WSAEventSelect模型。
首先调用与WSAAsyncSelect同工的WSAEventSelect函数,其原型如下:
WINSOCK_API_LINKAGE int
WSAAPI WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject,
long lNetworkEvents );
调用WSAEventSelect函数将指定参数一指定的套接字s关注的网络事件以位或|掩码组合形式(FD_XXX | FD_XXX)注册到参数三lNetworkEvents,并将该套接字绑定到参数二指定的事件对象hEventObject。这样当lNetWorkEvents中的事件发生时,Windows将hEventObject置信(由Unsignaled变为Signaled)。
当事件对象受信后,我们需要获得这个通知。需要调用等待事件对象的同步函数,主要有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函数原型如下:
WINSOCK_API_LINKAGE 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函数来重置事件对象。
将发生的网络事件存储在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即时重置事件对象,以便使其进入下一轮询。