WinSock重叠I/O模型

本文详细介绍了Windows下的重叠I/O模型,包括概念、使用方法及如何处理完成通知。探讨了重叠I/O在文件和网络通信中的应用,如ReadFile、WriteFile、WSARecv和WSASend等API的使用,以及获取I/O操作结果的方法。

一.重叠I/O模型的概念

当调用ReadFile()WriteFile()时,如果最后一个参数lpOverlapped设置为NULL,那么线程就阻塞在这里,直到读写完指定的数据后,它们才返回。这样在读写大文件的时候,很多时间都浪费在等待ReadFile()WriteFile()的返回上面。如果ReadFile()WriteFile()是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。

为了解决这个问题,Windows引进了重叠I/O的概念,它能够同时以多个线程处理多个I/O。其实你自己开多个线程也可以处理多个I/O,但是系统内部对I/O的处理在性能上有很大的优化。它是Windows下实现异步I/O最常用的方式。

Windows为几乎全部类型的文件提供这个工具:磁盘文件、通信端口、命名管道和套接字。通常,使用ReadFile()WriteFile()就可以很好地执行重叠I/O

重叠模型的核心是一个重叠数据结构。若想以重叠方式使用文件,必须用 FILE_FLAG_OVERLAPPED标志打开它,例如:

HANDLE hFile = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

如果没有规定该标志,则针对这个文件(句柄),重叠I/O是不可用的。如果设置了该标志,当调用ReadFile()WriteFile()操作这个文件(句柄)时,必须为最后一个参数提供OVERLAPPED结构:

// WINBASE.H

typedef struct _OVERLAPPED{

DWORD Internal;

DWORD InternalHigh;

DWORD Offset;

DWORD OffsetHigh;

HANDLE hEvent; //关键的一个参数

}OVERLAPPED, *LPOVERLAPPED;

头两个32位的结构字InternalInternalHigh由系统内部使用;其次两个32位结构字OffsetOffsetHigh使得可以设置64位的偏移量,该偏移量是要文件中读或写的地方。

因为I/O异步发生,就不能确定操作是否按顺序完成。因此,这里没有当前位置的概念。对于文件的操作,总是规定该偏移量。在数据流下(如COM端口或socket),没有寻找精确偏移量的方法,所以在这些情况中,系统忽略偏移量。这四个字段不应由应用程序直接进行处理或使用,OVERLAPPED结构的最后一个参数是可选的事件句柄,当I/O完成时,该事件对象受信(signaled)。程序通过等待该对事件对象受信来做善后处理。

设置了OVERLAPPED参数后,ReadFile()/WriteFile()的调用会立即返回,这时候你可以去做其他的事(所谓异步),系统会自动替你完成ReadFile()/WriteFile()相关的I/O操作。你也可以同时发出几个ReadFile()/WriteFile()的调用(所谓重叠)。当系统完成I/O操作时,会将OVERLAPPED.hEvent置信,我们可以通过调用WaitForSingleObject/WaitForMultipleObjects来等待这个I/O完成通知,在得到通知信号后,就可以调用GetOverlappedResult来查询I/O操作的结果,并进行相关处理。由此可以看出,OVERLAPPED结构在一个重叠I/O请求的初始化及其后续的完成之间,提供了一种沟通或通信机制。注意OVERLAPPED结构的生存周期,一般动态分配,待I/O完成后,回收重叠结构。

Win32重叠I/O机制为基础,自WinSock 2发布开始,重叠I/O便已集成到新的WinSock API中,比如WSARecv()/WSASend()。这样一来,重叠I/O模型便能适用于安装了WinSock 2的所有Windows平台。可以一次投递一个或多个WinSock I/O请求。针对那些提交的请求,在它们完成之后,应用程序可为它们提供服务(对I/O的数据进行处理)。

相应的,要想在一个套接字上使用重叠I/O模型来处理网络数据通信,首先必须使用 WSA_FLAG_OVERLAPPED这个标志来创建一个套接字。如下所示:

SOCKET s = WSASocket(AF_INET, SOCK_STEAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

创建套接字的时候,假如使用的是socket()函数,而非WSASocket()函数,那么会默认设置WSA_FLAG_OVERLAPPED标志。成功创建好了一个套接字,将其与一个本地接口绑定到一起后,便可开始进行这个套接字上的重叠I/O操作,方法是调用下述的WinSock 2函数,同时为它们指定一个WSAOVERLAPPED结构参数(#define WSAOVERLAPPED OVERLAPPED// WINSOCK2.H):

1WSASend()

2WSASendTo()

3WSARecv()

4WSARecvFrom()

5WSAIoctl()

6AcceptEx()

7TransmitFile()

若随一个WSAOVERLAPPED结构一起调用这些函数,函数会立即返回,无论套接字是否设为锁定模式。它们依赖于WSAOVERLAPPED结构来返回一个I/O请求操作的结果

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

实际编程时,可以投递一个0字节缓冲区的WSARecv/WSASend操作,这样就没有用户缓冲区与I/O操作相关联,避免了用户缓冲区的锁定(过多的锁定可能导致非分页内存池耗尽,即WSAENOBUFS),应用程序绕开单套接字缓冲区而直接与TCP Stack进行数据交互,从而避免了内存拷贝。当然,只要投递了足够多的重叠发送/接收操作,就能避免额外的内存拷贝,这时将单套接字缓冲区设置为0并不能提升性能。因为应用程序的发送缓冲区将始终被锁定直到可以下传给TCP,所以停用套接字的发送缓冲区对性能的影响比停用接收缓冲区小。然而,如果接收缓冲区被设置为0,而又未投递重叠接收操作,则进来的数据都只能停留在TCP Stack中,而TCP驱动程序的缓冲区最多只能接收窗口大小。TCP缓冲区被定位在非分页内存池中,假如很多连接发数据过来,但我们根本没有投递接收操作,则将消耗大量的非分页内存池。非分页内存池是一种有限的资源,过多的锁定可能导致非分页内存池耗尽,即WSAENOBUFS

Windows NTWindows 2000中,重叠I/O模型也允许应用程序以一种重叠方式实现对套接字连接的处理。具体的做法是在监听套接字上调用AcceptEx函数。AcceptEx是一个特殊的WinSock扩展函数,由mswsock.dll实现,使用时需包含Mswsock.h头文件,链接Mswsock.lib库文件。该函数最初的设计宗旨是在Windows NTWindows 2000操作系统上使用Win 32的重叠I/O机制。但事实上,它也适用于WinSock 2中的重叠I/OAcceptEx的定义如下:

// MSWSOCK.H

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协议,那么这里的大小应设为SOCKADDR_IN结构的长度+16字节

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

最后一个参数是lpOverlapped,它对应的是一个OVERLAPPED结构,允许AcceptEx()以一种异步方式工作。如我们早先所述,只有在一个重叠I/O应用中,该函数才需要使用事件对象通知机制hEvent字段,这是由于此时没有一个完成例程参数可供使用。

二.获取重叠I/O操作完成结果

当异步I/O请求挂起后,最终要知道I/O操作是否完成。一个重叠I/O请求最终完成后,应用程序要负责读取重叠I/O操作的结果。对于读,直到I/O完成,接收缓冲器才有效(参考IRP缓冲区管理)。对于写,要知道写是否成功,有几种方法可以做到这点,最直接的方法是调用(WSA)GetOverlappedResult,其函数原型如下:

WINBASEAPI BOOL WINAPI

GetOverlappedResult(

HANDLE hFile,

LPOVERLAPPED lpOverlapped,

LPDWORD lpNumberOfBytesTransferred,

BOOL bWait);

BOOL WSAAPI WSAGetOverlappedResult(

SOCKET s,

LPWSAOVERLAPPED lpOverlapped,

LPDWORD lpcbTransfer,

BOOL fWait,

LPDWORD lpdwFlags);

参数一为的文件/套接字句柄。

参数二为参数一关联的(WSA) OVERLAPPED结构,在调用CreateFile()WSASocket()AcceptEx()时指定。

参数三指向字节计数指针,负责接收一次重叠发送或接收操作实际传输的字节数。

参数四是确定命令是否等待的标志。Wait参数用于决定函数是否应该等待一次重叠操作完成。若将Wait设为TRUE,那么直到操作完成函数才返回;若设为FALSE,而且操作仍然处于未完成状态,那么(WSA)GetOverlappedResult()函数会返回FALSE值。

(WSA)GetOverlappedResult()函数调用成功,返回值就是TRUE。这意味着我们的重叠I/O操作已成功完成,而且由参数三BytesTransfered参数指向的值已进行了更新。若返回值是FALSE,那么可能是由下述任何一种原因造成的:

重叠I/O操作仍处在待决状态。

重叠操作已经完成,但含有错误。

重叠操作的完成状态不可判决,因为在提供给 WSAGetOverlappedResult函数的一个或多个参数中,存在着错误。

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

下面介绍两种常用重叠I/O完成通知的方法。

1.使用事件通知

使用(WSA)GetOverlappedResult()是直截了当的,它吻合重叠I/O的概念。毕竟,如果要等待I/O,也许使用常规I/O命令更好。对于大多数程序,反复检查I/O是否完成,并非最佳。解决方案之一是使用(WSA)OVERLAPPED结构中的hEvent字段,使应用程序将一个事件对象句柄同一个文件/套接字关联起来。

当指定OVERLAPPED参数给ReadFile()/WriteFile()WSARecv()/WSASend()后,可以再为(WSA)OVERLAPPED最后一个参数提供自定义的事件对象(通过(WSA)CreateEvent()创建)。

I/O完成时,系统更改(WSA)OVERLAPPED结构对应的事件对象的传信状态,使其从未传信unsignaled)变成已传信signaled)。由于我们之前将事件对象分配给了(WSA)OVERLAPPED结构,所以只需简单地调用WaitForSingleObject/WaitForMultipleObjectsWSAWaitForMultipleEvents函数,从而判断出一个(一些)重叠I/O在什么时候完成。通过WaitForSingleObject/WaitForMultipleObjectsWSAWaitForMultipleEvents函数返回的索引可以知道这个重叠I/O完成事件是在哪个HANDLEFileSocket)上发生的。

然后调用(WSA)GetOverlappedResult()函数,将发生事件的HANDLEFILESOCKET)传给参数一,将这个HANDLE对应的(WSA)OVERLAPPED结构传给参数二,这样判断重叠调用到底是成功还是失败。如果返回FALSE值,则重叠操作已经完成但含有错误。或者重叠操作的完成状态不可判决,因为在提供给(WSA)GetOverlappedResult()函数的一个或多个参数中存在着错误。失败后,由BytesTransfered参数指向的值不会进行更新,应用程序应调用(WSA)GetLastError()函数,调查到底是何种原因造成了调用失败。

(WSA)GetOverlappedResult()函数返回TRUE,则根据先前调用异步I/O函数时设置的缓冲区(ReadFile/WriteFileWSARecv/WSASendlpBuffer字段)BytesTransfered,使用指针偏移定位就可以准确操作接受到的数据了。

利用事件对象来完成同步通知的方法比重复调用(WSA)GetOverlappedResult()浪费处理器时间的方案要高效得多。但WaitForMultipleObjects/WSAaitForMultipleEvent支持的事件对象个数的上限为MAXIMUM_WAIT_OBJECTS/WSA_MAXIMUM_WAIT_EVENTS=64

2.使用完成例程

对于文件重叠I/O操作,等待I/O操作结束的另外方法是使用ReadFileEx()WriteFileEx()。这些命令只用于重叠I/O,当为它们的最后一个参数lpCompletionRoutine传递了一个完成例程指针(回调函数地址)时,I/O操作结束时将调用此函数进行处理。

完成例程指针LPOVERLAPPED_COMPLETION_ROUTINE定义如下:

// WINBASE.H

typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(

DWORD dwErrorCode,

DWORD dwNumberOfBytesTransfered,

LPOVERLAPPED lpOverlapped );

相应在WinSock 2中,WSARecv()/WSASend()最后一个参数lpCompletionROUTINE是一个可选的指针,它指向一个完成例程。若指定此参数(自定义函数地址),在重叠请求完成后,将调用完成例程处理。完成例程本质上是一种APCAsynchronous 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()函数。假定应用程序只用完成例程对重叠请求进行处理,便不需要引入事件对象。作为一种变通方法,我们的应用程序可用Win32SleepEx()函数将自己的线程置为一种可警告的等待状态。当然,亦可创建一个伪事件对象,不将它与任何东西关联在一起。假如调用线程经常处于繁忙状态,而且并不处于一种可警告的等待状态,那么完成例程根本不会被通知执行。

如前面所述,WSAWaitForMultipleEvents()通常会等待同(WSA)OVERLAPPED结构关联在一起的事件对象。该函数也可用于将我们的线程设置为一种可警告的等待状态,为已经完成的重叠I/O请求调用完成例程进行处理(前提是将fAlertable参数设为TRUE)。使用一个含有完成例程指针的异步I/O函数提交了重叠I/O请求之后, WSAWaitForMultipleEvents()的期望返回值是WAIT_IO_COMPLETIONOne or more I/O completion routines 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/OWinSock程序的编写步骤如下:

(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)完成时,设置了fAlertableWSAWaitForMultipleEvents返回WAIT_IO_COMPLETION,并调用完成例程指针指向的完成例程对数据进行处理。

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

参考

Windows 2000 Systems Programming Black Book Al Williams

Network Programming for Microsoft Windows Anthony Jones,Jim Ohlund

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值