套接字的I/O模型(二)
overlapped模型
- 相比于其他的I/O模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
- 重叠模型的基本设计原理是:让应用程序使用重叠的数据结构,一次投递一个或多个Winsock I/O请求。针对那些提交的请求,在它们完成之后,应用程序可以为它们提供服务。
- Windows重叠I/O机制可通过ReadFile和WriteFile两个函数,在设备上执行I/O操作。
- 要想在一个套接字上使用重叠I/O模型,首先必须创建一个设置了重叠标志的套接字。成功建好一个套接字,同时将它与一个本地接口绑定到一起后,便可开始进行重叠I/O的操作,方法是调用下列的Winsock函数,同时指定一个可选的WSAOVERLAPPED结构。
WSASend WSASendTo WSARecv WSARecvFrom WSAloctl WSARecvMsg AcceptEx ConnectEx TransmitFile TransmitPackets DisconnectEx WSANSPloctl
为了使用重叠I/O,每个函数都把WSAOVERLAPPED结构作为参数。若用一个WSAOVERLAPPED结构一起调用这些函数,函数会立即完成并返回,而不管套接字是否设为阻塞模式。这些函数依赖于WSAOVERLAPPED结构来管理一个I/O请求的完成。
- 主要有两个方法可用来管理重叠I/O请求完成情况:
- 应用程序可等待事件对象通知(event object notification)
- 可通过完成例程(completion routines),对已经完成的请求加以处理。
一:事件通知
- 重叠I/O的事件通知方法要求将Windows事件对象与WSAOVERLAPPED结构关联在一起。若使用一个WSAOVERLAPPED结构,发出像WSASend和WSARecvv这样的I/O调用,它们会立即返回。
通常,大家会发现这些I/O调用会以失败而告终,并返回SOCKET_ERROR。使用WSAGetLastError函数,便可获得与WSA_IO_PENDING错误状态有关的一个报告。这个错误状态意味着I/O操作正在进行。稍后的某个时间,应用程序需要等候与WSAOVERLAPPED结构相关联的事件对象,判断某个重叠I/O请求何时完成。WSAOVERLAPPED结构为重叠I/O请求的初始化及其后续的完成之间提供了一种通信媒介。
typedef struct _WSAOVERLAPPED { DWORD Internal; //Reserved(保留) DWORD InternalHigh; //Reserved(保留) DWORD Offset; //Reserved(保留) DWORD OffsetHigh; //Reserved(保留) WSAEVENT hEvent; //Reserved(保留) } WSAOVERLAPPED, *LPWSAOVERLAPPED; //Handle to a WSAEVENT object that represents an event
其中前四个参数均由系统在内部使用,不能由应用程序直接进行处理或使用。 hEvent允许应用程序将事件对象的句柄同操作关联起来。- 一个重叠I/O请求最终完成后,应用程序要负责获取重叠I/O操作的结果。一个重叠请求操作最终完成之后,在事件通知方法中,Winsock会更改与WSAOVERLAPPED结构关联的事件对象的事件传信状态,将其从未传信变成已传信。由于已经有一个事件对象分配给WSAOVERLAPPED结构,所以只需简单地调用WSAWaitForMultipleEvents函数,便可判断出重叠I/O调用将在什么时候完成(WSAWaitForMultipleEvents函数会等候一段指定的时间,等待一个或多个事件对象进入已传信状态。它一次最多等待64个事件对象)。
确认某个重叠请求完成之后,接着需要调用WSAGetOverlappedResult函数,判断这个重叠调用到底是成功还是失败。函数定义如下:
BOOL WSAAPI WSAGetOverlappedResult( _In_ SOCKET s, //重叠操作开始时被指定的套接字 _In_ LPWSAOVERLAPPED lpOverlapped, //被指定的WSAOVERLAPPED结构 _Out_ LPDWORD lpcbTransfer, //一次重叠发送或接收操作实际传输的字节数 _In_ BOOL fWait, //决定函数是否应该等待挂起操作完成 _Out_ LPDWORD lpdwFlags //接收结果 );
- fWait为TRUE,那么除非操作完成,否则函数不会返回;若为FALSE,而操作仍然处于挂起状态,那么WSAGetOverlappedResult函数会返回FALSE值,同时返回一个WSA_IO_INCOMPLETE错误。但就目前的情况来说,由于需要在一个已传信事件上等候重叠操作完成,所以该参数无论采用什么设置,都没有任何效果。
- lpdwFlags指向一个DWORD,负责接收结果标志(假如原先的重叠调用时用WSARecv或WSARecvFrom函数发出的)
- 如果WSAGetOverlappedResult函数调用成功,返回值就是TRUE。这意味着重叠I/O操作已成功完成,而且lpcbTransfer参数所指向的值已进行了更新。若返回值是FALSE,那么可能是由下述任何一种原因造成的:
- 重叠I/O操作仍处于挂起状态
- 重叠操作已经完成,但含有错误
- 因为在提供给WSAGetOverlappedResult函数中的一个或多个参数中,存在着错误,所以无法判断重叠操作的完成状态。
失败后,lpcbTransfer参数指向的值不会被更新,而且应用程序调用WSAGetLastError函数,查看到底是何种原因造成了调用失败。
- 代码示例:
#include<winsock2.h> #include<Ws2tcpip.h> #include<stdio.h> #pragma comment(lib,"WS2_32") #define DATA_BUFSIZE 4096 void main(void) { WSABUF DataBuf; char buffer[DATA_BUFSIZE]; DWORD EventTotal = 0, RecvBytes = 0, Flags = 0, BytesTransferred=0; WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAOVERLAPPED AcceptOverlapped; SOCKET ListenSocket, AcceptSocket; //第一步 启动Winsock,建立监听套接字 //..............................// //第二步 接受一个入站连接 AcceptSocket = accept(ListenSocket, NULL, NULL); //第三步 建立一个重叠结构 EventArray[EventTotal] = WSACreateEvent(); memset(&AcceptOverlapped, 0, sizeof(WSAOVERLAPPED)); AcceptOverlapped.hEvent = EventArray[EventTotal]; DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; EventTotal++; //第四步 投递一个WSARecv请求,以便开始在套接字上接收数据 if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { //出错....... } } //处理套接字上的重叠接收 while (TRUE) { DWORD Index; //第五步 等候重叠I/O调用结束 Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE); //索引应为0,因为EventArray中仅有一个事件 //第六步 重置已传信事件 WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]); //第七步 确定重叠请求的状态 WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags); //先检查 通信对方是否已关闭连接,如果已关闭,则关闭套接字 if (BytesTransferred == 0) { printf("Closing socket %d \n", AcceptSocket); closesocket(AcceptSocket); WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); return; } //对接收到的数据进行某种处理 DataBuf包含接收到的数据 //第八步 在套接字上投递另一个WSARecv()请求 Flags = 0; memset(&AcceptOverlapped, 0, sizeof(WSAOVERLAPPED)); AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0]; DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { //未预料到的错误 } } } }
对该程序采用的编程步骤总结如下:
- 创建一个套接字,开始在指定的端口上监听连接请求
- 接受一个入站的连接请求
- 为接受的套接字新建一个WSAOVERLAPPED结构,并为该结构分配一个事件对象句柄。也将该事件对象句柄分配给一个事件数组,以便稍后由WSAWaitForMultipleEvents函数使用
- 将WSAOVERLAPPED结构指定为参数,在套接字上投递一个异步WSARecv请求
- 使用步骤3的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入已传信状态
- 使用WSAGetOverlappedResult函数,判断重叠调用的返回状态是什么
- 函数完成后,针对事件数组,调用WSAResetEvent函数,从而重设事件对象,并对完成的重叠请求进行处理
- 在套接字上投递另一个重叠WSARecv请求
- 重复步骤5~8
如果用重叠方式(在WSAOVERLAPPED结构内部指定一个事件,或利用一个完成例程)调用Winsock函数,则调用操作有可能立即完成。例如:在数据已经被接收到并放入缓冲区之后调用WSARecv,就会导致WSARecv返回NO_ERROR。如果任何重叠函数失败并返回WSA_IO_PENDING,或立刻成功,完成事件将总会被传信,而完成例程也将随时运行(如果已做了指定)。对于带完成端口的重叠I/O而言,这意味着完成通知将被传递到完成端口,使其开始服务。
二:完成例程
完成例程是应用程序用来管理完成的重叠I/O请求的另一种方法。完成例程其实就是一些函数,我们将这些函数传递给重叠I/O请求,以供重叠I/O请求完成时由系统调用。它们的基本设计宗旨是,通过调用者的线程,为已完成的I/O请求提供服务。除此之外,应用程序可通过完成例程,继续进行重叠I/O的处理。
如果希望用完成例程为重叠I/O请求提供服务,应用程序必须为一个绑定I/O的Winsock函数指定一个完成例程,同时指定一个WSAOVERLAPPED结构。一个完成例程必须拥有下述函数原型:
void CALLBACK CompletionROUTING { DWORD dwError, //重叠操作(由lpOverlapped指定)的完成状态是什么 DWORD cbTransferred, //在重叠操作期间,实际传输的字节量 LPWSAOVERLAPPED lpOverlapped, //传递到最初的I/O调用内的一个WSAOVERLAPPED结构 DWORD dwFlags //返回操作结束时可能用的标志(如:从WSARecv结束) }
在用一个完成例程提交的重叠请求与用一个事件对象提交的重叠请求之间,存在着一个非常重要的区别。WSAOVERLAPPED结构的事件字段hEvent并未被使用;也就是说,不可以将一个事件对象同重叠请求关联到一起。用完成例程发出重叠I/O调用之后,调用线程一旦完成,最终必须为完成例程提供服务。这样,便要求我们将调用线程设置与一种警觉的等待状态(alertable wait state)。并在I/O操作完成后,对完成例程加以处理。WSAWaitForMultipleEvents函数可用来将线程置于一种警觉的等待状态。这样做的缺点在于,我们还必须有一个事件对象可用于WSAWaitForMultipleEvents函数。假定应用程序用完成例程只对重叠请求进行处理,便不大可能又什么事件对象需要处理。作为一种变通方法,应用程序可用Windows的SleepEx函数将线程置为一种警觉的等待状态。当然,亦可创建一个伪事件对象,它不与任何东西关联在一起。假如调用线程总是处于繁忙状态,而不是处于一种警觉的等待状态,那么根本不会有投递的完成例程会得到调用。
如前所见,WSAWaitForMultipleEvents通常会等待同WSAOVERLAPPED结构关联在一起的事件对象。该函数也用来将线程置入一种警觉的等待状态,并可为已经完成的重叠I/O请求进行完成例程的处理(前提是将fAlertable参数设为TRUE)。用完成例程结束重叠I/O请求之后,返回值是WSA_IO_COMPLETION,而不是事件数组中的一个事件对象的索引。SleepEx函数的行为实际上和WSAWaitForMultipleEvents差不多,只是它不需要任何事件对象。
DWORD WINAPI SleepEx( _In_ DWORD dwMilliseconds, //等待时间 以ms(毫秒)为单位 INFINITE表示一直等待 _In_ BOOL bAlertable //完成例程的执行方式 ); //加入将bAlertable设为FALSE,而且进行了一次I/O完成回调,那么I/O完成函数就不会执行,而且该函数也不会返回,除非超过由dwMilliseconds规定的时间。若设为TRUE,那么完成例程会得到执行,同时SleepEX函数返回WAIT_IO_COMPLETION
- 代码示例:(简单的服务器应用程序)
#include<winsock2.h> #include<Ws2tcpip.h> #include<stdio.h> #pragma comment(lib,"WS2_32") #define DATA_BUFSIZE 4096 SOCKET AcceptSocket, ListenSocket; WSABUF DataBuf; WSAEVENT EventArray[MAXIMUM_WAIT_OBJECTS]; DWORD Flags, RecvBytes, Index; char buffer[DATA_BUFSIZE]; void CALLBACK WorkerRoutine(DWORD Error, DWORD BytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD InFlags); int main(void) { WSAOVERLAPPED overlapped; //第一步 启动Winsock,建立监听套接字 //..............................// //第二步 接受一个新连接 AcceptSocket = accept(ListenSocket, NULL, NULL); //第三步 已经有了一个接受套接字之后,开始使用带有完全例程的重叠I/O来处理I/O //为了启动重叠I/O处理,先提交一个重叠WSARecv()请求 Flags = 0; memset(&overlapped, 0, sizeof(WSAOVERLAPPED)); DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; //第四步 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求并提供下面的 //作为完成例程的WorkerRoutine函数 if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &overlapped, WorkerRoutine) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { printf("WSARecv() failed with error %d \n", WSAGetLastError()); return; } } //因为WSAWaitForMultipleEvents() API要求在一个或多个事件对象上等待, //因此不得不创建一个伪事件对象。作为一种可选方案,也可使用SleepEx()替代 EventArray[0] = WSACreateEvent(); while (TRUE) { //第五步 : Index = WSAWaitForMultipleEvents(1, EventArray, FALSE, WSA_INFINITE, TRUE); //第六步: if (Index == WAIT_IO_COMPLETION) { //一个重叠请求完成例程结束。继续为更多的完成例程服务 continue; } else { //发生了一个严重错误:停止处理 //如果正在处理一个事件对象,那么这也就可能是事件数组的一个索引 return; } } return 1; } void CALLBACK WorkerRoutine(DWORD Error, DWORD BytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD InFlags) { DWORD SendBytes, RecvBytes; DWORD Flags; if (Error != 0 || BytesTransferred == 0) { //要么是套接字上发生了一个严重错误,要么套接字已被通信对方关闭 closesocket(AcceptSocket); return; } //此刻,一个重叠WSARecv()请求顺利完成------现在可以接受DataBuf变量中包含的已收到的数据了 //处理完接收到的数据后,需要投递另外一个重叠WSARecv()或WSASend()请求 //--------下面值投递了另外一个WSARecv()请求 Flags = 0; memset(Overlapped, 0, sizeof(WSAOVERLAPPED)); DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, Overlapped, WorkerRoutine) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { printf("WSARecv() failed with error &d \n", WSAGetLastError()); return; } } }
对该程序采用的编程步骤总结如下:
- 新建一个套接字,开始在指定端口上监听传入的连接
- 接受一个入站的连接请求
- 为接受的套接字创建一个WSAOVERLAPPED结构。
- 在套接字上投递一个异步WSARecv请求,需要将WSAOVERLAPPED指定为参数,同时提供一个完成例程
- 在将fAlertable参数设为TRUE的前提下,调用WSAWaitForMultipleEvents,并等待一个重叠I/O请求完成。重叠请求完成后,完成例程会自动执行,而且WSAWaitForMultipleEvents会返回一个WSA_IO_COMPLETION。在完成例程内,可用一个完成例程投递另一个重叠WSARecv请求
- 检查WSAWaitForMultipleEvents是否返回WAIT_IO_COMPLETION
重复步骤5和6
- 重叠模型提供高性能套接字I/O。因为使用这种模型的应用程序通知缓冲区收发系统直接使用的数据,所以它和前面的几种不同。也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已到达套接字,则该数据将直接被拷贝到投递的缓冲区。在前面的讲述的模型中,数据到达并被拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据姜葱单套接字缓冲区拷贝到应用程序的缓冲区。
- 在事件中使用重叠I/O的缺点,也是每次最多只能等待64个事件这一局限性。完成例程是一个不错的替代方式,但必须注意确保投递完成操作的线程进入警觉的等待状态,以便使完成例程能够圆满结束。同时,还要注意确保完成例程不要做过量的运算,以便在很重的负载之下,这些完成过程能够尽快开始运行
- completionport模型
声明:以上整理于Windows网络编程(第二版)