三种常规异步WinSock IO模型

xiaoxiao2021-03-01  12

1 .基于事件套接字集合的select 模型

select (选择)模型是Winsock 中最常见的I/O 模型。之所以称其为“select 模型”,是由于它的“中心思想”便是利用select 函数,实现对I/O 的管理!最初设计该模型时,主要面向的是某些使用Unix 操作系统的计算机,它们采用的是Berkeley 套接字方案。select 模型已集成到Winsock 1.1 中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。

select 模型本质上是一种分类处理思想,预先声明几个FD_SET(fd_set 结构) 集合,例如ReadSetWriteSet ,然后调用宏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 函数,对声明的集合ReadSetWriteSet 进行扫描,其函数原型如下:

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 结构,它扫描注册到集合ReadSetWriteSet 中的套接字是否有读写事件发生,若有,则对集合进行更新,即将套接字添加到集合ReadSetWriteSet 中。同时,删除那些不存在待决I/O 操作的套接字句柄。select 完成后,会返回在所有fd_set 集合中设置的套接字句柄总数。

然后,我们需要遍历查询之前注册到某个集合中的套接字是否仍为其中一部分。这需要调用FD_ISSET(SOCKET s, fd_set set) 来测试套接字是否属于关注同类事件的套接字集合set 。若是,则对待决的I/O 进行处理。

使用select 模型,可能需要调用ioctlsocket 函数将一个套接字从锁定模式切换为非锁定模式。

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 窗口消息处理机制。该模型亦得到了MFCMicrosoft 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) 注册到参数三 l NetworkEvents ,并将该套接字绑定到参数二指定的事件对象hEventObject 。这样当lNetWorkEvents 中的事件发生时,WindowshEventObject 置信(由Unsignaled变为Signaled)

当事件对象受信后,我们需要获得这个通知。需要调用等待事件对象的同步函数,主要有WaitForSingleObjectWaitForMultipleObjectsWSAWaitForMultipleEvents

函数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 不同的是,WaitForMultipleObjectsWSAWaitForMultipleEvents 支持在多个对象的等待。它们支持nCount/cEventslpHandles/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 上发生的所有网络事件。与注册事件时使用位或| 掩码相反,这里一般采用位与& 析取相应的网络事件代码,即将lNetworkEventsFD_XXX 进行位与运算,若返回1 则表示有FD_XXX 网络事件发生。

这里,我们看到了FD_ISSET 的影子。可以看出,WSAEventSelectselect 模型和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

相关资源:微信小程序源码-合集1.rar
转载请注明原文地址: https://www.6miu.com/read-3100054.html

最新回复(0)