WinSock三种选择I/O模型

在《套接字socketC/S通信的基本概念》和《WinSock编程基础》中,我们介绍了套接字的基本概念和 WinSock 规范,讨论了 阻塞模式/非阻塞模式 和 同步I/O、异步I/O 等话题。

从概念的角度,阻塞模式简洁易用便于快速原型化,但在应付建立连接的多个套接字或在数据的收发量不均、时间不定时等场合却极难管理。另一方面,我们需要对非阻塞模式套接字的 WinSock API 调用频繁返回的 WSAEWOULDBLOCK 错误加以判断处理也显得难于管理。WinSock 套接字 I/O 模型提供了管理 I/O 完成通知的方法,帮助应用程序判断套接字何时可供读写

共有6种类型的套接字 I/O 模型可让 WinSock 应用程序对 I/O 进行管理,它们包括 blocking(阻塞)、select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及 completion port(完成端口)。

本文讨论三种选择模型(都带 select,姑且这样称呼


基于套接字读写检测集合的 select 模型

select 模型概述

该模型最初是在不使用 UNIX 操作系统的计算机上实现的,它们采用的是 Berkeley 套接字方案。select 模型已集成到 Winsock 1.1 中,它使那些想避免在套接字调用过程中被无辜锁定的应用程序,采取一种有序的方式,同时对多个套接字进行管理。

之所以称其为“select模型,是由于它的核心流程是围绕 select 函数调用实现对 I/O 的管理!使用 select 模型,一般需要调用 ioctlsocket(UNIX 下对应 API 为 ioctlfcntl函数将一个套接字锁定模式切换为非锁定模式

// 将套接字s设置为非阻塞模式
unsigned long enableNonblockingMode = 1;
ioctlsocket(s, FIONBIO, (u_long*)&enableNonblockingMode);

select 模型本质上是一种分类处理思想,预先声明几个 FD_SET (fd_set 结构集合(使用 FD_ZERO 初始化),例如 ReadSet、WriteSet。fd_set 数据类型本质上代表着一系列按关注事件分类的套接字集合,类似 libevent 中的 struct event

宏 FD_SET (SOCKET s, fd_set set) 将 添加到 set 集合:调用宏FD_SET(s, &ReadSet)将关注 FD_READ 事件的套接字 添加到 ReadSet 集合;调用宏 FD_SET(s,&WriteSet) 将关注 FD_WRITE 事件的套接字 添加到 WriteSet 集合。FD_SET 类似 libevent 中的 event_add

然后再调用 select 函数(类似 libevent 中的 event_base_dispatch),对声明的集合 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() 微秒级(μs,microseconds延时功能。

select() 函数用于判断套接字上是否存在数据(any data incoming?)或者能否向一个套接字写数据(output buffer available?)。调用 select() 会修改每个 fd_set 结构(值-结果参数:_Inout_),它扫描注册到集合 ReadSet 和 WriteSet 中的套接字是否有读写事件发生。若有,则对集合进行更新,删除那些不存在待决 I/O 操作的套接字句柄,select() 返回所有在 fd_set 集合中的套接字句柄总数,即存在待决 I/O 的套接字句柄个数。

然后,遍历查询之前注册到某个 I/O 集合中的套接字是否仍在其中:调用 FD_ISSET(SOCKET s, fd_set set) 来测试套接字是否属于关注同类事件的套接字集合 set。若是则对待决的 I/O 进行处理(调用 recv()/send() 执行读写/拷贝)。

一般需要定时检查一次 FD_SET 上是否有待决 I/O。

select 模型应用

由于 select 模型源于 Berkeley 套接字方案,故常用作实现跨平台的 POLL 组件。在 UNX 下,select 和 poll 是一个级别的,可参考《UNIX 网络编程》卷1第六章——I/O复用:select 和 poll 函数。

以下梳理了经典开源通信库中用到的 select 模型。

1curl/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,

 * otherwiseselect() is used.

*/

2thttpd/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.

**/

3Apache Httpd/httpd/srclib/apr/下的

include/apr_poll.h中定义了Pollset Methods的枚举变量apr_pollset_method_e

poll/unix/select.c中的apr_poll()impl_pollset_poll()调用。

4nginx/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()

……

select 模型的局限性

select 模型的优势在于能够从单个线程的多个套接字上进行多重连接及 I/O 管理,这样就避免了伴随阻塞套接字和多重连接的线程剧增。但可以加到 fd_set 结构中的最大套接字数量 FD_SETSIZE在 WINSOCK2.H 中定义为64,底层程序强加了一个 fd_set 的最大值,通常情况下是1024)。当然,我们可以分批 FD_SET()→select()→FD_ISSET() 来突破此限制。

select 模型可以跨平台,对于小型 C2C 客户端通信、千路并发规模的中小型服务器差不多够用。在具体平台开发网络通信程序时,可以结合平台特性,发挥平台机制优势。


基于 Windows 消息处理 WSAAsyncSelect 模型

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 窗口消息处理机制。该模型亦得到了MFCMicrosoft 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 消息泵本身的局限性,单窗口程序处理成千上万个套接字中的所有事件也可能成为性能瓶颈。


基于事件通知的WSAEventSelect模型

在 WSAAsyncSelect 模型中,调用 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

当事件对象受信后,我们需要获得这个通知,这需要调用等待事件对象的同步函数,主要有 WaitForSingleObjectWaitForMultipleObjects 和 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_ii=[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

UNIX Network Programming, Volume 1,Third Edition Source Code

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值