WinSock 异步I/O模型[4]---重叠 I/O - Overlapped I/O

在 Winsock 中,重叠 I/O(Overlapped I/O)模型能达到更佳的系统性能,高于之前讲过的三种。因为它和这3种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据。也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。而这3种模型中,数据到达并拷贝到单套接字接收缓冲区(Per Socket Buffer)中,此时应用程序会被系统通知可以读入的字节数。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区。这样就减少了一次从I/O缓冲区到应用程序缓冲区的拷贝,差别就在于此。

重叠模型的基本设计原理便是让应用程序使用一个重叠的数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O 请求。针对这些提交的请求,在它们完成之后,我们的应用程序会收到通知,于是我们就可以对数据进行处理了。

█ 要想在一个套接字上使用重叠 I/O 模型,首先必须使用 WSA_FLAG_OVERLAPPED 这个标志,创建一个套接字例如:
SOCKET s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

创建套接字的时候,假如使用的是 socket 函数,那么会默认设置 WSA_FLAG_OVERLAPPED 标志。

成功建好一个套接字,同时将其与一个本地接口绑定到一起后,便可开始进行重叠 I/O 操作,
为了要使用重叠结构,我们常用的 send、recv 等收发数据的函数也都要被 WSASend、WSARecv 替换掉了,
方法是调用下述的 Winsock 函数,同时指定一个 WSAOVERLAPPED 结构(可选),它们的用法我后面会讲到:
■ WSASend
■ WSASendTo
■ WSARecv
■ WSARecvFrom
■ WSAIoctl
■ AcceptEx
■ TrnasmitFile

WSA_IO_PENDING : 最常见的返回值,这是说明我们的重叠函数调用成功了,但是 I/O 操作还没有完成。

█ 若随一个 WSAOVERLAPPED 结构一起调用这些函数,函数会立即完成并返回,无论套接字是否设为阻塞模式。
那么我们如何来得知我们的 I/O 请求是否成功了呢?方法有两个:
■ 等待“事件对象通知”;
■ 通过“完成例程”。

★ 下面主要讲事件通知方式。完成例程方式在最后补充说明。

█ 这里只需要注意一点,重叠函数(如:WSARecv)的参数中都有一个 Overlapped 参数,我们可以假设是把我们的WSARecv这样的操作“绑定”到这个重叠结构上,提交一个请求,而不是将操作立即完成,其他的事情就交给重叠结构去做,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完 WSARecv 以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要的数据了。

重叠 I/O 的事件通知方法要求将 Win32事件对象与 WSAOVERLAPPED 结构关联在一起,
当 I/O 操作完成后,事件的状态会变成“已传信”状态,即激发态(是不是跟上节课给大家讲的事件选择模型很像?);下面来看一下 WSAOVERLAPPED 结构的定义:

typedef struct _WSAOVERLAPPED {
    DWORD    Internal;
    DWORD    InternalHigh;
    DWORD    Offset;
    DWORD    OffsetHigh;
    WSAEVENT hEvent;
} WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;

typedef struct _WSAOVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
     struct {
       DWORD Offset;
       DWORD OffsetHigh;
     };    PVOID Pointer;
  };
  HANDLE hEvent;
} WSAOVERLAPPED,  *LPWSAOVERLAPPED;

其中,Internal、InternalHigh、Offset 和 OffsetHigh 字段均由系统在内部使用,不应由应用程序直接进行处理或使用。而另一方面,hEvent 字段有点儿特殊,它允许应用程序将一个事件对象句柄同一个套接字关联起来。
大家可能会觉得奇怪,如何将一个事件对象句柄分配给该字段呢?正如我们早先在 WSAEventSelect 模型中讲述的那样,可用 WSACreateEvent 函数来创建一个事件对象句柄。一旦创建好一个事件句柄,简单地将重叠结构的 hEvent 字段分配给事件句柄,再使用重叠结构,调用一个Winsock函数即可,比如 WSASend 或 WSARecv。

█ 一个重叠 I/O 请求最终完成后,我们的应用程序要负责取回重叠 I/O 操作的结果。一个重叠请求操作最终完成之后,在事件通知方法中,Winsock会更改与一个 WSAOVERLAPPED 结构对应的一个事件对象的事件传信状态,
将其从“未传信”变成“已传信”。由于一个事件对象已分配给 WSAOVERLAPPED 结构,所以只需简单地调用 WSAWaitForMultipleEvents 函数,从而判断出一个重叠 I/O 调用在什么时候完成。该函数已在我们前面介绍过了。

█ 发现一次重叠请求完成之后,接着需要调用 WSAGetOverlappedResult(取得重叠结构)函数,判断那个重叠调用到底是成功,还是失败。对于读,直到I/O完成,接收缓冲区才有效(参考IRP缓冲区管理),对于写,要知道写是否成功。该函数的定义如下:
BOOL WSAAPI WSAGetOverlappedResult(
  __in          SOCKET s,
  __in          LPWSAOVERLAPPED lpOverlapped,
  __out         LPDWORD lpcbTransfer,
  __in          BOOL fWait,
  __out         LPDWORD lpdwFlags
);

■ s 参数用于指定在重叠操作开始的时候,与之对应的那个套接字。
■ lpOverlapped 参数是一个指针,对应于在重叠操作开始时,指定的那个 WSAOVERLAPPED 结构。
■ lpcbTransfer 参数也是一个指针,对应一个DWORD(双字)变量,负责接收一次重叠发送或接收操作实际传输的字节数。
■ fWait 参数用于决定函数是否应该等待一次待决(未决)的重叠操作完成。若将 fWait设为 TRUE,那么除非操作完成,否则函数不会返回;若设为FALSE,而且操作仍然处于“待决”状态,那么WSAGetOverlappedResult 函数会返回 FALSE值,同时返回一个WSAIOINCOMPLETE(I/O操作未完成)错误。但就我们目前的情况来说,由于需要等候重叠操作的一个已传信事件完成,所以该参数无论采用什么设置,都没有任何效果。
■ 参数 lpdwFlags 对应于一个指针,指向一个DWORD(双字),负责接收结果标志(假如原先的重叠调用是用WSARecv或WSARecvFrom函数发出的)。

■ 返回值:若 WSAGetOverlappedResult 函数调用成功,返回值就是TRUE。这意味着我们的重叠 I/O 操作已成功完成,而且由 lpcbTransfer 参数指向的值已进行了更新。
若返回值是FALSE,那么可能是由下述任何一种原因造成的:
1)、重叠 I/O操 作仍处在“待决”状态。
2)、重叠操作已经完成,但含有错误。
3)、重叠操作的完成状态不可判决,因为在提供给 WSAGetOverlappedResult函数的一个或多个参数中,存在着错误。

失败后,由 lpcbTransfer 参数指向的值不会进行更新,而且我们的应用程序应调用 WSAGetLastError 函数,调查到底是何种原因造成了调用失败。如果错误码为SOCKET_ERROR/WSA_IO_INCOMPLETE(Overlapped I/O event is not in a signaledstate)或SOCKET_ERROR/WSA_IO_PENDING(Overlapped I/O operation is in progress),则表明I/O仍在进行。当然,这不是真正错误,任何其他错误码则真正表明一个实际错误。

█ 重叠 I/O 模型的编程步骤总结如下:
1) 创建一个套接字,开始在指定的端口上监听连接请求;

2) 接受一个客户端进入的连接请求;

3) 为接受的套接字新建一个 WSAOVERLAPPED 结构,并为该结构分配一个事件对象句柄,
同时将该事件对象句柄分配给一个事件数组,以便稍后由 WSAWaitForMultipleEvents 函数使用。

4) 在套接字上投递一个异步 WSARecv 请求,指定参数为 WSAOVERLAPPED 结构。
注意函数通常会以失败告终,返回 SOCKET_ERROR 错误状态 WSA_IO_PENDING(I/O操作尚未完成);

5) 使用步骤3)的事件数组,调用 WSAWaitForMultipleEvents 函数,并等待与重叠调用关联在一起的事件进入“已传信”状态(换言之,等待那个事件的“触发”);

6) WSAWaitForMultipleEvents 函数返回后,针对“已传信”状态的事件,调用 WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理;

7) 使用 WSAGetOverlappedResult 函数,判断重叠调用的返回状态是什么;

8) 在套接字上投递另一个重叠 WSARecv 请求;

9) 重复步骤5)~8)。

█ 在 Windows NT 和 Windows 2000 中,重叠 I/O 模型也允许应用程序以一种重叠方式,实现对客户端连接的接受。具体的做法是在监听套接字上调用 AcceptEx 函数。AcceptEx 是一个特殊的 Winsock1.1 扩展函数,位于 Mswsock.h 头文件以及 Mswsock.lib 库文件内。
AcceptEx 函数的定义如下:
BOOL AcceptEx(
  __in          SOCKET sListenSocket,
  __in          SOCKET sAcceptSocket,
  __in          PVOID lpOutputBuffer,
  __in          DWORD dwReceiveDataLength,
  __in          DWORD dwLocalAddressLength,
  __in          DWORD dwRemoteAddressLength,
  __out         LPDWORD lpdwBytesReceived,
  __in          LPOVERLAPPED lpOverlapped
);

● sListenSocket 参数指定的是一个监听套接字。
● sAcceptSocket 参数指定的是另一个套接字,负责对进入连接请求的“接受”。
AcceptEx 函数和 accept 函数的区别在于,我们必须提供接受的套接字,而不是让函数自动为我们创建。
正是由于要提供套接字,所以要求我们事先调用 socket 或 WSASocket 函数,创建一个套接字,以便通过 sAcceptSocket 参数,将其传递给 AcceptEx。
● lpOutputBuffer 参数指定的是一个特殊的缓冲区,因为它要负责三种数据的接收:服务器的本地地址,客户机的远程地址,以及在新建连接上发送的第一个数据块。
● dwReceiveDataLength参数以字节为单位,指定了在 lpOutputBuffer 缓冲区中,保留多大的空间,用于数据的接收。
如这个参数设为0,那么在连接的接受过程中,不会再一道接收任何数据。
● dwLocalAddressLength 和 dwRemoteAddressLength 参数也是以字节为单位,指定在 lpOutputBuffer 缓冲区中,保留多大的空间,
在一个套接字被接受的时候,用于本地和远程地址信息的保存。

要注意的是,和当前采用的传送协议允许的最大地址长度比较起来,这里指定的缓冲区大小至少应多出16字节。
举个例子来说:假定正在使用的是 TCP/IP 协议,那么这里的大小应设为“SOCKADDRIN 结构的长度+16字节”。

● lpdwBytesReceived 参数用于返回接收到的实际数据量,以字节为单位。
只有在操作以同步方式完成的前提下,才会设置这个参数。假如 AcceptEx 函数返回 ERROR_IO_PENDING,
那么这个参数永远都不会设置,我们必须利用完成事件通知机制,获知实际读取的字节量。

● lpOverlapped 参数对应的是一个 OVERLAPPED 结构,允许 AcceptEx 以一种异步方式工作。
如我们早先所述,只有在一个重叠 I/O 应用中,该函数才需要使用事件对象通知机制,这是由于此时没有一个完成例程参数可供使用。
也就是说 AcceptEx 函数只能由本节课给大家讲的“事件通知”方式获取异步 I/O 请求的结果,而“完成例程”方法无法被使用。

补充:
下面补充说明重叠IO中完成例程的使用。在WinSock 2中,WSARecv()/WSASend()最后一个参数lpCompletionROUTINE是一个可选的指针,它指向一个完成例程。若指定此参数(自定义函数地址),在重叠请求完成后,将调用完成例程处理。完成例程本质上是一种APC(Asynchronous Procedure Calls)。

WinSock 2中完成例程指针LPWSAOVERLAPPED_COMPLETION_ROUTINE定义如下:
// WINSOCK2.H
typedef void (CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
        DWORD dwError,
        DWORD cbTransferred,
        LPWSAOVERLAPPED lpOverlapped,
        DWORD dwFlags );

     前三个参数同LPOVERLAPPED_COMPLETION_ROUTINE,参数四一般不用,置0。用完成例程完成一个重叠I/O请求之后,参数中会包含下述信息:

参数一dwError表明了一个重叠操作(由lpOverlapped指定)的完成状态是什么。
参数二BytesTransferred参数指定了在重叠操作实际传输的字节量是多大
参数三lpOverlapped参数指定的是调用这个完成例程的异步I/O操作函数(ReadFileEx()/WriteFileEx()或WSARecv()/WSASend())的(WSA)OVERLAPPED结构参数。

提交带有完成例程的重叠I/O请求时,(WSA)OVERLAPPED结构的事件字段hEvent一般不再使用。使用一个含有完成例程指针参数的异步I/O函数发出一个重叠I/O请求之后,一旦重叠I/O操作完成,作为我们的调用线程,必须能够通知完成例程指针所指向的自定义函数开始执行,提供数据处理服务。这样一来,便要求将调用线程置于一种“可警告的等待状态”,在I/O操作完成后,能自动调用完成例程。WSAWaitForMultipleEvents()函数可用来将线程置于一种可警告的等待状态。这样做的代价是必须创建一个事件对象可用于WSAWaitForMultipleEvents()函数。假定应用程序只用完成例程对重叠请求进行处理,便不需要引入事件对象。作为一种变通方法,我们的应用程序可用Win32的SleepEx()函数将自己的线程置为一种可警告的等待状态。当然,亦可创建一个伪事件对象,不将它与任何东西关联在一起。
假如调用线程经常处于繁忙状态,而且并不处于一种可警告的等待状态,那么完成例程根本不会被通知执行。

如前面所述,WSAWaitForMultipleEvents()通常会等待同(WSA)OVERLAPPED结构关联在一起的事件对象。该函数也可用于将我们的线程设置为一种可警告的等待状态,为已经完成的重叠I/O请求调用完成例程进行处理(前提是将fAlertable参数设为TRUE)。使用一个含有完成例程指针的异步I/O函数提交了重叠I/O请求之后,WSAWaitForMultipleEvents()的期望返回值是WAIT_IO_COMPLETION(One or more I/O completionroutines are queued for execution),而不再是事件对象索引。从宏WAIT_IO_COMPLETION的注解可知,它的意思是有完成例程需要执行。SleepEx()函数的行为实际上和WSAWaitForMultipleEvents()差不多,只是它不需要任何事件对象。
对SleepEx函数的定义如下:
WINBASEAPI DWORD WINAPI
SleepEx(
    DWORD dwMilliseconds,
    BOOL bAlertable 
  );

其中,dwMilliseconds参数定义了SleepEx()函数的等待时间,以毫秒为单位。假如将dwMilliseconds设为INFINITE,那么SleepEx()会无休止地等待下去。bAlertable参数规定了一个完成例程的执行方式,若将它设置为FALSE,则使用一个含有完成例程指针的异步I/O函数提交了重叠I/O请求后,I/O完成例程不会被通知执行,而且SleepEx()函数不会返回,除非超过由dwMilliseconds规定的时间;若将它设置为TRUE,则完成例程会被通知执行,同时SleepEx()函数返回WAIT_IO_COMPLETION。

在完成例程处理模型中,投递重叠I/O请求的同时注册完成例程,待I/O完成时由系统回调,并克服了事件通知模型的个数限制。
利用完成例程处理重叠I/O的WinSock程序的编写步骤如下:
(1) 新建一个监听套接字,在指定端口上监听客户端的连接请求。
(2) 接受一个客户端的连接请求,并返回一个会话套接字负责与客户端通信。
(3) 为会话套接字关联一个WSAOVERLAPPED结构。
(4) 在套接字上投递一个异步WSARecv请求,方法是将WSAOVERLAPPED指定成为参数,同时提供一个完成例程。
(5) 在将fAlertable参数设为TRUE的前提下,调用WSAWaitForMultipleEvents,并等待一个重叠I/O请求完成。重叠请求完成后,完成例程会自动执行,
    而且WSAWaitForMultipleEvents会返回一个WAIT_IO_COMPLETION。在完成例程内,可随一个完成例程一道投递另一个重叠WSARecv请求。
(6) 检查WSAWaitForMultipleEvents是否返回WAIT_IO_COMPLETION。
(7) 重复步骤(5)和(6)。

当调用accept处理连接时,一般创建一个AcceptEvent伪事件,当有客户连接时,需要手动SetEvent(AcceptEvent);当调用AcceptEx处理重叠的连接时,
一般为ListenSocket创建一个ListenOverlapped结构,并为其指定一个伪事件,当有客户连接时,系统自动将其置信。这些伪事件的作用在于,
当含有完成例程指针的异步I/O操作(如WSARecv)完成时,设置了fAlertable的WSAWaitForMultipleEvents返回WAIT_IO_COMPLETION,
并调用完成例程指针指向的完成例程对数据进行处理。

重叠I/O模型的缺点是它一般要为每一个I/O请求都开一个线程,当同时有成千上万个请求发生时,系统处理线程上下文切换是非常耗时的。
所以这也就引出了更为先进的完成端口模型IOCP,用线程池来解决这个问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小米的修行之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值