介绍
重叠I/O是Win32文件操作的一项技术,其基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步I/O请求。
重叠I/O的概念
在传统文件操作中,当对文件进行读写时,线程会阻塞在读写操作上,直到读写完指定的数据后它们才返回。这样在读写大文件的时候,很多时间都浪费在等待上面,如果读写操作是对管道读写数据,那么有可能阻塞得更久,导致程序性能下降。
为了解决这个问题,我们首先想到可以用多个线程处理多个I/O,但这种方式显然不如从系统层面实现效果更好。Windows引进了重叠I/O的概念,它能够同时以多个线程处理多个I/O,而且系统内部对I/O的处理在性能上有很大的优化。
重叠I/O是Windows环境下实现异步I/O最常用的方式。Windows为几乎全部类型的文件操作(如磁盘文件、命名管道和套接字等)都提供了这种方法。
以Win32重叠I/O机制为基础,自WinSock2发布开始,重叠I/O便已集成到新的WinSock函数中,这样一来,重叠I/O模型便能适用于安装了WinSock2的所有Windows平台,可以一次投递一个或多个WinSock I/O请求。针对那些提交的请求,在它们完成之后,应用程序可为它们提供服务(对I/O的数据进行处理)。
重叠模型的核心是一个重叠数据结构WSAOVERLAPPED,该结构与OVERLAPPED结构兼容,其定义如下:
typedef struct _WSAOVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
};
PVOID Pointer;
};
HANDLE hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
其中:
● Internal:由重叠I/O实现的实体内部使用的字段。在使用Socket的情况下,该字段被底层操作系统使用。
● InternalHigh:由重叠I/O实现的实体内部使用的字段。在使用Socket的情况下,该字段被底层操作系统使用。
● Offset:在使用套接字的情况下该参数被忽略。
● OffsetHigh:在使用套接字的情况下该参数被忽略。
● Pointer:在使用套接字的情况下该参数被忽略。
● hEvent:允许应用程序为这个操作关联一个事件对象句柄。重叠I/O的事件通知方法需要将Windows事件对象关联到WSAOVERLAPPED结构。
在重叠I/O模式下,对套接字的读写调用会立即返回,这时候程序可以去做其他的工作,系统会自动完成具体的I/O操作。另外,应用程序也可以同时发出多个读写调用。当系统完成I/O操作时,会将WSAOVERLAPPED中的hEvents置为授信状态,可以通过调用WSAWaitForMultipleEvents()函数来等待这个I/O完成通知,在得到通知信号后,就可以调用WSAGetOverlappedResult()函数来查询I/O操作的结果,并进行相关处理。由此可以看出,WSAOVERLAPPED结构在一个重叠I/O请求的初始化及其后续的完成之间,提供了一种沟通或通信机制。
重叠I/O模型的相关函数
(1)套接字创建函数:WSASocket()
若想以重叠方式使用套接字,必须用重叠方式(标志为WSA_FLAG_OVERLAPPED)打开套接字。WSASocket()函数用于创建绑定到指定的传输服务提供程序的套接字,该函数的原型定义如下:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
其中:
● af:指定地址族;
● type:指定套接字的类型;
● protocol:指定套接字使用的协议;
● lpProtocolInfo,指向WSAPROTOCOL_INFO结构,指定新建套接字的特性;
● g:预留字段;
● dwFlags:指定套接字属性的标志,在重叠I/O模型中,dwFlags参数需要被置为WSA_FLAG_OVERLAPPED,这样就可以创建一个重叠套接字,在后续的操作中执行重叠I/O操作,同时初始化和处理多个操作。
如果函数执行成功,则返回新建套接字的句柄,否则返回INVALID_SOCKET。可以通过WSAGetLastError()函数获得错误号了解具本错误信息。
(2)数据发送函数:WSASend()和WSASendTo()
在WinSock环境下,WSASend()函数和WSASendTo()函数提供了在重叠套接字上进行数据发送的能力,并在以下两个方面有所增强:
1)用于重叠socket上,完成重叠发送操作;
2)一次发送多个缓冲区中的数据,完成集中写入操作。
函数WSASend()覆盖标准的send()函数,该函数的定义如下:
int WSASend(
__in SOCKET s,
__in LPWSABUF lpBuffers,
__in DWORD dwBufferCount,
__out LPDWORD lpNumberOfBytesSent,
__in DWORD dwFlags,
__in LPWSAOVERLAPPED lpOverlapped,
__in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
其中:
● s:标识一个已连接套接字的描述符;
● lpBuffers:一个指向WSABUF结构数组的指针,每个WSABUF结构包含缓冲区的指针和缓冲区的大小;
● dwBufferCount:记录lpBuffers数组中WSABUF结构的数目;
● lpNumberOfBytesSent:是一个返回值,如果发送操作立即完成,则为一个指向所发送数据字节数的指针;
● dwFlags:标志位,与send()调用的flag域类似;
● lpOverlapped:指向WSAOVERLAPPED结构的指针,该参数对于非重叠套接字无效;
● lpCompletionRoutine:指向完成例程,是一个指向发送操作完成后调用的完成例程的指针,该参数对于
非重叠套接字无效。
如果重叠操作立即完成,则WSASend()函数返回0,并且参数lpNumberOfBytesSent被更新为发送数据的字节数;如果重叠操作被成功初始化,并且将在稍后完成,则WSASend()函数返回SOCKET_ERROR,错误代码为WSA_IO_PENDING。
当重叠操作完成后,可以通过下面两种方式获取传输数据的数量:
1)如果指定了完成例程,则通过完成例程的cbTransferred参数获取;
2)通过WSAGetOverlappedResult()函数的lpcbTransfer参数获取。
另外,WSASendTo()函数提供了在非连接模式下使用重叠I/O进行数据发送的能力,该函数覆盖标准的sendto()函数,其原型定义如下:
int WSASendTo(
__in SOCKET s,
__in LPWSABUF lpBuffers,
__in DWORD dwBufferCount,
__out LPDWORD lpNumberOfBytesSent,
__in DWORD dwFlags,
__in const struct sockaddr* lpTo,
__in int iToLen,
__in LPWSAOVERLAPPED lpOverlapped,
__in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
该函数与WSASend()函数的主要差别类似于sendto()和send()的差别,增加了对目标地址的输入参数,其中:
● lpTo:指向以sockaddr结构存储的目的地址的指针;
● iToLen:指向目的地址长度的指针。
(3)数据接收函数:WSARecv()和WSARecvFrom()
在WinSock环境下,WSARecv()函数和WSARecvFrom()函数提供了在重叠套接字上进行数据接收的能力,并在以下两个方面有所增强:
1)用于重叠socket,完成重叠接收的操作;
2)一次将数据接收到多个缓冲区中,完成集中读出操作。
函数WSARecv()覆盖标准的recv()函数,该函数的定义如下:
int WSARecv(
__in SOCKET s,
___inout LPWSABUF lpBuffers,
__in DWORD dwBufferCount,
__out LPDWORD lpNumberOfBytesRecvd,
___inout LPDWORD lpFlags,
__in LPWSAOVERLAPPED lpOverlapped,
__in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
其中:
● s:标识一个已连接套接字的描述符;
● lpBuffers:一个指向WSABUF结构数组的指针,每个WSABUF结构包含缓冲区的指针和缓冲区的大小;
● dwBufferCount:记录lpBuffers数组中WSABUF结构的数目;
● lpNumberOfBytesRecvd:是一个返回值,如果I/O操作立即完成,则该参数指定接收数据的字节数;
● dwFlags:标志位,与recv()调用的flag域类似;
● lpOverlapped:指向WSAOVERLAPPED结构的指针,该参数对于非重叠套接字无效;
● lpCompletionRoutine:指向完成例程,是一个指向接收操作完成后调用的完成例程的指针,该参数对于非重叠套接字无效。
如果重叠操作立即完成,则WSARecv()函数返回0,并且参数lpNumberOfBytesRecvd被更新为接收数据的字节数;如果重叠操作被成功初始化,并且将在稍后完成,则WSARecv()函数返回SOCKET_ERROR,错误代码为WSA_IO_PENDING。
另外,WSARecvFrom()函数提供了在非连接模式下使用重叠I/O进行数据接收的能力,该函数覆盖标准的recvfrom()函数,其原型定义如下:
int WSARecvFrom(
__in SOCKET s,
___inout LPWSABUF lpBuffers,
__in DWORD dwBufferCount,
__out LPDWORD lpNumberOfBytesRecvd,
___inout LPDWORD lpFlags,
__out struct sockaddr* lpFrom,
___inout LPINT lpFromlen,
__in LPWSAOVERLAPPED lpOverlapped,
__in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
该函数与WSARecv()函数的主要差别类似于recvfrom()和recv()的差别,增加了对来源地址的输入参数,其中:
● lpFrom:是一个返回值,指向以sockaddr结构存储的来源地址的指针;
● iFromLen:指向来源地址长度的指针。
(4)重叠操作结果获取函数:GetOverlappedResult()
当异步I/O请求挂起后,最终要知道I/O操作是否完成。一个重叠I/O请求最终完成后,应用程序要负责取回重叠I/O操作的结果。对于读操作,直到I/O完成,输入缓冲区才有效。对于写操作,要知道写是否成功。为了获得指定文件、命名管道或通信设备上重叠操作的结果,最直接的方法是调用WSAGetOverlappedResult(),其函数原型如下:
BOOL WSAAPI WSAGetOverlappedResult(
__in SOCKET s,
__in LPWSAOVERLAPPED lpOverlapped,
__out LPDWORD lpcbTransfer,
__in BOOL fWait,
__out LPDWORD lpdwFlags
);
其中:
● s:标识进行重叠操作的描述符;
● hFile:指向文件、命名管道或通信设备的句柄;
● lpOverlapped:指向重叠操作开始时指定的WSAOVERLAPPED结构;
● lpcbTransfer:本次重叠操作实际接收(或发送)的字节数;
● fWait:指定函数是否等待挂起的重叠操作结束,若为TRUE则函数在操作完成后才返回,若为FALSE且函数挂起,则函数返回FALSE,WSAGetLastError()函数返回WSA_IO_INCOMPLETE;
● lpdwFlags:指向DWORD的指针,该变量存放完成状态的附加标志位,如果重叠操作为WSARecv()或WSARecvFrom(),则本参数包含lpFlags参数所需的结果。
如果函数成功,则返回值为TRUE。它意味着重叠操作已经完成,lpcbTransfer所指向的值已经被刷新。应用程序可调用WSAGetLastError()来获取重叠操作的错误信息。
如果函数失败,则返回值为FALSE。它意味着要么重叠操作未完成,要么由于一个或多个参数的错误导致无法决定完成状态。失败时lpcbTransfer指向的值不会被刷新。应用程序可用WSAGetLastError()来获取失败的原因。
重叠I/O模型的编程框架
Windows Sockets可以使用事件通知和完成例程两种方式来管理重叠I/O操作。
使用事件通知方式进行重叠I/O的编程框架
对于大多数程序,反复检查I/O是否完成并非最佳方案。事件通知是一种较好的通知方式,此时需要使用WSAOVERLAPPED结构中的hEvent字段,使应用程序将一个事件对象句柄(通过WSACreateEvent()函数创建)同一个套接字关联起来。
当I/O完成时,系统更改WSAOVERLAPPED结构对应的事件对象的授信状态,使其从“未授信”变成“已授信”。由于之前已将事件对象分配给了WSAOVERLAPPED结构,所以只需简单地调用WSAWaitForMultipleEvents()函数,从而判断出一个(或一些)重叠I/O在什么时候完成。通过WSAWaitForMultipleEvents()函数返回的索引可以知道这个重叠I/O完成事件是在哪个Socket上发生的。
以面向连接的数据接收为例,在使用事件通知方式的重叠I/O模型下,套接字的编程框架如图8-8所示。
整体来看,使用事件通知方式进行重叠I/O的网络应用程序的基本流程如下
1)套接字初始化,设置为重叠I/O模式;
2)创建套接字网络事件对应的用户事件对象;
3)初始化重叠结构,为套接字关联事件对象;
4)异步接收数据,无论能否接收到数据,都会直接返回;
5)调用WSAWaitForMultiEvents()函数在所有事件对象上等待,只要有一个事件对象变为已授信状态,则函数返回;
6)调用WSAGetOverlappedResult()函数获取套接字上的重叠操作的状态,并保存到重叠结构中;
7)根据重置事件的状态进行处理;
8)重置已授信的事件对象、重叠结构、标志位和缓冲区;
9)回到步骤4。
代码
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <Windows.h>
#include <stdio.h>
#include <conio.h>
#pragma comment(lib,"ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015
int main(int argc,TCHAR *argv[]) {
WSABUF DataBuf;
char buffer[DEFAULT_BUFLEN];
DWORD EventTotal = 0, RecvBytes = 0, Flags = 0, BytesTransferred = 0;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAOVERLAPPED AcceptOverlapped;
WSADATA wsaData;
int iResult;
SOCKET ServerSocket;
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult!=0)
{
printf("wsatartup failed error code %d", iResult);
return 1;
}
ServerSocket = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);
if (ServerSocket==INVALID_SOCKET)
{
printf("WSASOCKET failed with error code %d\n",WSAGetLastError());
WSACleanup();
return 1;
}
int iOptval = 1;
iResult = setsockopt(ServerSocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,(char*)&iOptval, sizeof(iOptval));
if (iResult == SOCKET_ERROR) {
wprintf(L"setsockopt for SO_EXCLUSIVEADDRUSE failed with error: %ld\n",
WSAGetLastError());
}
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(DEFAULT_PORT);
addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
iResult = bind(ServerSocket,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
if (iResult==SOCKET_ERROR)
{
printf("bind failed with error code %d",WSAGetLastError());
closesocket(ServerSocket);
WSACleanup();
return 1;
}
iResult = listen(ServerSocket,SOMAXCONN);
if (iResult == SOCKET_ERROR)
{
printf("LISTEN FAILED\n");
closesocket(ServerSocket);
WSACleanup();
return -1;
}
printf("TCP Server starting...\n");
EventArray[EventTotal] = WSACreateEvent();
ZeroMemory(buffer, DEFAULT_BUFLEN);
ZeroMemory(&AcceptOverlapped,sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[EventTotal];
DataBuf.len = DEFAULT_BUFLEN;
DataBuf.buf = buffer;
EventTotal++;
sockaddr_in addrClient;
int addrClientlen = sizeof(sockaddr_in);
SOCKET AcceptSocket;
while (true)
{
AcceptSocket = accept(ServerSocket, (sockaddr FAR*) & addrClient, &addrClientlen);
if (AcceptSocket == INVALID_SOCKET)
{
printf("accept failed\n");
closesocket(ServerSocket);
WSACleanup();
return 1;
}
printf("new conn...\n");
//处理数据
while (true)
{
DWORD Index;
iResult = WSARecv(AcceptSocket, &DataBuf,1, &RecvBytes,&Flags, &AcceptOverlapped, NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at %d\n", WSAGetLastError());
}
}
Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags);
if (BytesTransferred == 0)
{
printf("CLOSING SOCKET .....\n");
closesocket(AcceptSocket);
break;
}
printf("bYTE RECVED %d\n", BytesTransferred);
WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
// 重置Flags变量和重叠结构
Flags = 0;
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
ZeroMemory(buffer, DEFAULT_BUFLEN);
AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0];
// 重置缓冲区
DataBuf.len = DEFAULT_BUFLEN;
DataBuf.buf = buffer;
}
}
return 0;
}
使用完成例程方式进行重叠I/O的编程框架
对于网络重叠I/O操作,等待I/O操作结束的另一种方法是使用完成例程,WSARecv()、WSARecvFrom()、WSASend()、WSASendTo()中最后一个参数lpCompletionROUTINE是一个可选的指针,它指向一个完成例程。若指定此参数(自定义函数地址),则hEvent参数将会被忽略,上下文信息将传送给完成例程函数,然后调用WSAGetOverlappedResult()函数查询重叠操作的结果。
完成例程的函数原型如下
void CALLBACK CompletionROUTINE(
IN DWORD dwError,
IN DWORD cbTransferred,
IN LPWSAOVERLAPPED lpOverlapped,
IN DWORD dwFlags
);
● dwError:指定lpOverlaped参数中表示的重叠操作的完成状态;
● cbTransferred:指定传送的字节数;
● lpOverlapped:指定重叠操作的结构;
● dwFlags:指定操作结束时的标志,通常可以设置为0。
以面向连接的数据接收为例,在使用完成例程方式的重叠I/O模型下,套接字的编程框架如图8-9所示。
整体来看,使用完成例程方式进行重叠I/O的网络应用程序的基本流程如下:
1)套接字初始化,设置为重叠I/O模式;
2)初始化重叠结构;
3)异步传输数据,将重叠结构作为输入参数,并指定一个完成例程对应于数据传输后的处理;
4)调用WSAWaitForMultiEvents()函数或SleepEx()函数将自己的线程置为一种可警告等待状态,等待一个重叠I/O请求完成,重叠请求完成后,完成例程会自动执行,在完成例程内,可随一个完成例程一起投递另一个重叠I/O操作;
5)回到步骤3。