概念
重叠模型是一种异步IO模型。多线程结构中就考虑到采用异步的方式进行设备读写操作,即我们告诉系统对设备的读写数据,而同时应用程序的其他代码继续执行,直到获取设备操作完毕的系统通知。
为了完成异步I/O,调用的I/O函数应以非阻塞模式工作。Windows中重叠I/O的重点并非I/O本身,而是如何确认I/O完成时的状态。
使用:
1.创建套接字
WSA_FLAG_OVERLAPPED标志:要使用重叠模型。在创建套接字的时候,必须加上该标志。
SOCKET s=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
假如使用的是socket函数,而非WSASocket函数,那么会默认设置WSA_FLAG_OVERLAPPED标志。若随一个WSAFLAGOVERLAPPED结构一起调用这些以WSA开头的函数(AcceptEx和TRansmiteFile函数例外),函数会立即完成并返回,无论套接字是否设为阻塞模式
2. 传输数据
在重叠I/O 模型中,传输数据的函数是WSASend、WSARecv(TCP)和
WSASendTo, WSARecvFrom 等。
int WSASend(
SOCKET s, // 套接字句柄
LPWSABUF lpBuffers, // WSABUF 结构的数组,每个WSABUF 结构包含一个缓冲区指针和对应缓冲区的长度
DWORD dwBufferCount, // 上面WSABUF 数组的大小
LPDWORD lpNumberOfBytesSent, // 如果I/O 操作立即完成的话,此参数取得实际传输数据的字节数
DWORD dwFlags, // 标志
LPWSAOVERLAPPED lpOverlapped, // 与此I/O 操作关联的WSAOVERLAPPED 结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 指定一个完成例程
);
这些函数与Winsock1 中的send、recv 等函数相比,都多了如下两个参数。
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
I/O 操作函数都接收一个WSAOVERLAPPED 结构类型的参数。这些函数被调用之后会立即返回,它们依靠应用程序传递的WSAOVERLAPPED 结构管理I/O 请求的完成。
应用程序有两种方法可以接收到重叠I/O 请求操作完成的通知:
(1)在与WSAOVERLAPPED 结构关联的事件对象上等待,I/O 操作完成后,此事件对象受信,这是最经常使用的方法。*(事件对象通知(eventobjectnotification)使一个事件内核对象变为有信号,针对每个I/O操作绑定一个内核事件对象,并将等待事件等待函数等待该事件的受信,当I/O操作完成后系统使得与该操作绑定的事件受信,从而判断那个操作完成。该技术解决了使一个设备内核对象变为有信号技术中一个设备只能对应一个操作的不足。) 利用WSASend、WSARecv函数的第六个参数,基于事件对象
(2)使用lpCompletionRoutine 指向的完成例程。完成例程是一个自定义的函数,I/O 操作完成后,Winsock 便去调用它。这种方法很少使用,将lpCompletionRoutine 设为NULL 即可(完成例程(completionroutines)
完成端口技术多用于处理大规模的请求,通过内在的进程池技术可以达到很高的性能。) 利用WSASend、WSARecv函数的第七个参数,基于Completion Routine
具体例子:确认重叠I/O的I/O完成
该函数类似于线程中常用的WaitForMultipleObjects函数,都是在等待事件的触发。我们将WSARecv等操作上绑定的LPWSAOVERLAPPED数据结构中的事件组称事件数据,在该函数上等待。
DWORDWSAWaitForMultipleEvents(
DWORDcEvents,//等候事件的总数量
constWSAEVENT*lphEvents,//事件数组的指针
BOOLfWaitAll,//当设置为TRUE,事件数组中所有事件被传信的时候函数才会返回
//FALSE则任何一个事件被传信函数都要返回,此时设为FALSE
DWORDdwTimeout,//超时时间,如果超时,函数会返回WSA_WAIT_TIMEOUT
//如果设置为0,函数会立即返回
BOOLfAlertable);//在完成例程中会用到这个参数,这里我们先设置为FALSE
返回值:
WSA_WAIT_TIMEOUT:最常见的返回值,等待超时,我们需要做的就是继续Wait
WSA_WAIT_FAILED:出现了错误,请检查cEvents和lphEvents两个参数是否有效
如果事件数组中有某一个事件受信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。
注:WSAWaitForMultipleEvents函数只能支持由WSA_MAXIMUM_WAIT_EVENTS对象定义的一个最大值64,就是说WSAWaitForMultipleEvents只能等待64个事件,如果想同时等待多于64个事件,就要创建额外的工作者线程,就需要通过线程池管理了。
3. 接受连接
可以异步接受连接请求的函数是AcceptEx。这是一个Microsoft 扩展函数,它接受一个新的连接,返回本地和远程地址,取得客户程序发送的第一块数据。
BOOL AcceptEx(
SOCKET sListenSocket, // 监听套接字句柄
SOCKET sAcceptSocket, // 指定一个未被使用的套接字,在这个套接字上接受新的连接
PVOID lpOutputBuffer, // 指定一个缓冲区,用来取得在新连接上接收到的第一块数据,服务器的本地地址和客户端地址
DWORD dwReceiveDataLength, // 上面lpOutputBuffer 所指缓冲区的大小
DWORD dwLocalAddressLength, // 缓冲区中,为本地地址预留的长度。必须比最大地址长度多16
DWORD dwRemoteAddressLength, // 缓冲区中,为远程地址预留的长度。必须比最大地址长度多16
LPDWORD lpdwBytesReceived, // 用来取得接收到数据的长度
LPOVERLAPPED lpOverlapped // 指定用来处理本请求的OVERLAPPED 结构,不能为NULL
); // 声明在Mswsock.h 中,需要添加到Mswsock.lib 库的链接
AcceptEx 函数将几个套接字函数的功能集合在了一起。如果它投递的请求成功完成,则执行了如下3 个操作:
1) 接受了新的连接。
2) 新连接的本地地址和远程地址都会返回。
3) 接收到了远程主机发来的第一块数据。
AcceptEx 函数需要调用者提供两个套接字,一个指定了在哪个套接字上监听(sListenSocket 参数),另一个指定了在哪个套接字上接受连接(sAcceptSocket 参数)。也就是说,AcceptEx 不会像accept 函数一样为新连接创建套接字。如果提供了接收缓冲区,AcceptEx 投递的重叠操作直到接受到连接并且读到数据之后才会返回。以SO_CONNECT_TIME 为参数调用getsockopt 函数可以检查到是否接受了连接。如果接受了连接,这个调用还可以取得连接已经建立了多长时间。
AcceptEx 函数(Microsoft 扩展函数都是这样)是从Mswsock.lib 库中导出的。为了能够直接调用它,而不用链接到Mswsock.lib 库,需要使用WSAIoctl 函数将AcceptEx 函数加载到内存。WSAIoctl 函数是ioctlsocket 函数的扩展,它可以使用重叠I/O。函数的第3 个到第6个参数是输入和输出缓冲区,在这里传递AcceptEx 函数的指针。具体加载代码如下。
// 加载扩展函数AcceptEx
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes;
WSAIoctl(pListen->s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx,
sizeof(GuidAcceptEx),
&pListen->lpfnAcceptEx,
sizeof(pListen->lpfnAcceptEx),
&dwBytes,
NULL,
NULL);
4. 事件通知方式
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent; // 在此为这个操作关联一个事件对象句柄
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
前 4 个域Internal、InternalHigh、Offset 和OffsetHigh 由系统内部使用,应用程序不应该操作或者直接使用它们。hEvent 域允许应用程序为这个操作关联一个事件对象句柄。重叠I/O的事件通知方法需要将Windows 事件对象关联到上面的WSAOVERLAPPED 结构。
当使用WSAOVERLAPPED 结构进行I/O 调用时,如调用WSASend 和WSARecv,这些函数立即返回。通常情况下,这些I/O 调用会失败,返回值是SOCKET_ERROR,并且 重叠(Overlapped)I/O 模型WSAGetLastError 函数报告了WSA_IO_PENDING 出错状态。这个出错状态表示I/O 操作正在进行。在以后的一个时间,应用程序将需要通过在关联到WSAOVERLAPPED 结构的事件对象上等待以确定什么时候一个重叠I/O 请求完成。就这样,WSAOVERLAPPED 结构在重叠I/O 请求的初始化和随后的完成之间提供了交流媒介。
当重叠 I/O 请求最终完成以后,与之关联的事件对象受信,等待函数返回,应用程序可以使用WSAGetOverlappedResult 函数取得重叠操作的结果。
BOOL WSAGetOverlappedResult(
SOCKET s, // 套接字句柄
LPWSAOVERLAPPED lpOverlapped, // 重叠操作启动时指定的WSAOVERLAPPED 结构
LPDWORD lpcbTransfer, // 用来取得实际传输字节的数量
BOOL fWait, // 指定是否要等待未决的重叠操作
LPDWORD lpdwFlags // 用于取得完成状态
);
若WSAGetOverlappedResult函数调用成功,返回值就是TRUE,意味着重叠操作完成成功,而且lpcbTransfer参数所指向的值已进行了更新,若返回FALSE,那么可能是由以下原因造成的:
1.重叠I/O操作仍处于挂起状态
2.重叠操作已经完成,但含有错误
3.因为在提供给WSAGetOverlappedResult函数的一个或多个参数中存在错误,所有无法判断重叠操作的完成状态
失败后,lpcbTransfer所指向的值不会被更新,而且应用程序应调用WSAGetLastError函数查看错误原因。
实例
编程步骤:
1.创建一个套接字,开始在指定的端口上监听连接请求
2.接受一个入站的连接请求
3.为接收的套接字新建一个WSAOVERLAPPED结构,并为该结构分配一个事件对象句柄。也将该事件对象句柄分配给一个事件数组,以便稍后由WSAWaitForMultipleEvents使用。
4.将WSAOVERLAPPED指定为参数,在套接字上投递一个异步WSARecv请求
5.使用步骤3的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入已传信状态
6.使用WSAGetOverlappedResult函数,判断重叠调用的返回状态
7.函数完成后,针对重叠数组,调用WSAResetEvent函数,从而重设事件对象,并对完成的重叠请求进行处理
8.在套接字上投递另一个重叠WSARecv请求
9.重复步骤5~8
这个例子极易扩展,从而提供对多个套接字的支持。方法是将代码的重叠I/O处理部分移至一个对立的线程中,让主应用程序线程为额外的连接请求提供服务。
利用事件通知机制设计一个简单的服务器应用程序,令其在一个套接字上对重叠I/O操作进行管理:
#define DATA_BUFSIZE 2046
void main(void)
{
WSABUF DataBuf;
char buffer[DATA_BUFSIZE];
DWORD EventTotal = 0;
DWORD RecvBytes = 0;
DWORD Flags = 0;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAOVERLAPPED AcceptOverlapped;
SOCKET ListenSocket, AcceptSocket;
//第一步
//启动Winsock,建立监听套接字
...
//第二步
//接收一个入站连接
AcceptSocket = accept(ListenSocket,NULL,NULL);
//第三步
//建立一个重叠结构
EventArray[EventTotal] = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[EventTotal];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
EventTotal++;
//第四步
//接收一个WSARecv请求,以便在套接字上接收数据
if(SOCKET_ERROR ==
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL))
{
if(WSA_IO_PENDING != WSAGetLastError())
{
//出错
}
}
//处理套件子上的重叠接收
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;
ZeroMemory(&AccpetOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[Index-WSA_WAIT_EVENT_0];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
if(SOCKET_ERROR ==
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL))
{
if(WSA_IO_PENDING != WSAGetLastError())
{
//出错
}
}
}
}