重叠(Overlapped) I/O模型
与介绍过的其他模型相比,重叠I/O模型提供了更好的系统性能。这个模型的基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步I/O请求(即所谓的重叠I/O)。提交的I/O请求完成之后,与之关联的重叠数据中事件对象触发,应用程序便可使用WSAGetOverlappedResult函数获取重叠操作的结果。这和使用重叠结构调用ReadFile和WriteFile函数操作文件类似。
具体编程流程:
1、创建一个事件句柄表和缓冲区对象表
2、每创建一个套接字,就创建一个缓冲区对象和一个事件对象,将事件对象与缓冲区对象中ol.hEvent成员相关联,并将套接字和事件对象的句柄分别放入上面的两个表中。
3、创建一个监听套接字和监听事件对象,先投递几个异步的Accept的请求。(引子,必须先投递几个,否则不能引发后续的异步I/O请求)
4、调用WSAWaitForMultipleEvents在所有事件对象上等待,此函数返回后,我们对事件句柄表中每个事件调用WSAWaitForMultipleEvents函数和HandleIO函数,以便确认在哪些套接字上发生了网络事件。
5、在HandleIO函数中处理发生的网络事件,并继续投递异步的I/O请求。(继续投递异步I/O请求作为引子)
书上源代码的设计思路:
先声明一个套接字对象。
//套接字对象
typedef struct_SOCKET_OBJ
{
SOCKET s ; //套接字句柄
intnOutstandingOps ; //记录此套接字上的重叠I/O数量,要等待这个成员为0,说明套接字上面的I/O操作全部完成才可以释放套接字对象
LPFN_ACCEPTEXlpfnAcceptEx ; //扩展函数AcceptEx的指针(仅对监听套接字而言)
} SOCKET_OBJ , *PSOCKET_OBJ ;
然后声明一个缓冲区对象
//缓冲区对象
typedef struct _BUFFER_OBJ
{
OVERLAPPEDol ; //重叠结构
char *buff; //send/recv/AcceptEx所使用的缓冲区
int nLen ; //buff的长度
PSOCKET_OBJpSocket ; //此I/O所属的套接字对象
intnOperation ; //提交的操作类型
#define OP_ACCEPT 1 //操作类型
#define OP_READ 2
#define OP_WRITE 3
SOCKETsAccept ; //用来保存AccetpEx接受的客户端套接字(仅对监听套接字而言)
_BUFFER_OBJ*pNext ;
} BUFFER_OBJ, *PBUFFER_OBJ ;
套接字对象与缓冲区对象的关系:
具体流程:
程序只有单线程。
线程一开始先创建一个监听套接字和监听事件对象、监听事件缓冲区,用于先行投递几个异步的Accept I/O请求作为引发后续操作的引子。
然后调用WSAWaitForMultipleEvents函数等待相应的事件触发。
事件触发后,调用HandleIO函数处理相应的网络事件,并再次投递相应的异步I/O请求作为引子。
需要注意的是,程序中所有异步的Accept I/O请求中的s成员均共享程序一开始创建监听套接字sListen。且如果客户端套接字不关闭的情况下,每完成一个异步的Accept I/O请求,就会继续投递一个新的异步的Accpet I/O请求和一个异步的Send请求,每完成一个异步的Read I/O请求就会投递一个新的异步Write I/O请求,每完成异步Write I/O请求就会投递一个新的Read I/O请求。如果客户端套接字关闭,则除了保留程序一开始投递N个的异步Accept I/O请求所需的资源外,其余的套接字对象和缓冲区对象都会被释放,程序回到初始化状态。
书上源代码:
#define _WIN32_WINNT 0x0400
#include<windows.h>
#include<cstdio>
#include"InitSocket.h"
#define BUFFER_SIZE 2048
CInitSock InitSock ; //进入main函数前已经进行了初始化
//套接字对象
typedef struct _SOCKET_OBJ
{
SOCKET s ; //套接字句柄
int nOutstandingOps ; //记录此套接字上的重叠I/O数量
LPFN_ACCEPTEX lpfnAcceptEx ; //扩展函数AcceptEx的指针(仅对监听套接字而言)
} SOCKET_OBJ , *PSOCKET_OBJ ;
//缓冲区对象
//ol成员变量必须处于第一个位置
typedef struct _BUFFER_OBJ
{
OVERLAPPED ol ; //重叠结构
char *buff ; //send/recv/AcceptEx所使用的缓冲区
int nLen ; //buff的长度
PSOCKET_OBJ pSocket ; //此I/O所属的套接字对象
int nOperation ; //提交的操作类型
#define OP_ACCEPT 1 //操作类型
#define OP_READ 2
#define OP_WRITE 3
SOCKET sAccept ; //用来保存AccetpEx接受的客户端套接字(仅对监听套接字而言)
_BUFFER_OBJ *pNext ;
} BUFFER_OBJ, *PBUFFER_OBJ ;
//申请套接字对象
PSOCKET_OBJ GetSocketObj(SOCKET s) ;
//释放套接字对象
void FreeSocketObj(PSOCKET_OBJ pSocket) ;
//申请缓冲区对象
PBUFFER_OBJ GetBufferObj(PSOCKET_OBJ pSocket ,ULONG nLen) ;
//释放缓冲区对象
void FreeBufferObj(PBUFFER_OBJ pBuffer) ;
//寻找已触发的对象
PBUFFER_OBJ FindBufferObj(HANDLE hEvent) ;
//因为是数组与链表,所以需要重建对齐
void RebuildArray() ;
//提交接受连接
BOOL PostAccept(PBUFFER_OBJ pBuffer) ;
//提交接收
BOOL PostRecv(PBUFFER_OBJ pBuffer) ;
//投递发送的请求
BOOL PostSend(PBUFFER_OBJ pBuffer) ;
//I/O处理函数
BOOL HandleIO(PBUFFER_OBJ pBuffer) ;
//全局变量
HANDLE g_events[WSA_MAXIMUM_WAIT_EVENTS] ; //I/O事件句柄数组
int g_nBufferCount ; //上数组中有效句柄数量
PBUFFER_OBJ g_pBufferHead ,g_pBufferTail ; //记录缓冲区对象组成的表的地址
//主函数
int main(void)
{
//创建监听套接字,绑定到本地端口,进入监听模式
int nPort = 4567 ;
SOCKET sListen = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED) ;
SOCKADDR_IN si ;
si.sin_family = AF_INET ;
si.sin_port = ntohs(nPort) ;
si.sin_addr.s_addr = INADDR_ANY ;
bind(sListen,(sockaddr*)&si,sizeof(si)) ;
listen(sListen ,200) ;
//为监听套接字创建一个SOCKET_OBJ对象
PSOCKET_OBJ pListen = GetSocketObj(sListen) ;
//加载扩展函数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) ;
//创建用来重新建立g_events数组的事件对象
g_events[0] = WSACreateEvent() ;
//在此可以投递多个接受I/O请求,程序始终保持5个异步的Accept I/O
for(int i = 0 ; i< 5 ; ++i)
{
PostAccept(GetBufferObj(pListen,BUFFER_SIZE)) ; //GetBufferObj里面有增加事件对象的计数
}
while(TRUE)
{
// int nIndex = WSAWaitForMultipleEvents(g_nBufferCount+1,g_events,FALSE,WSA_INFINITE,FALSE) ;
int nIndex = WSAWaitForMultipleEvents(g_nBufferCount+1,g_events,FALSE,5000,FALSE) ; //等待5秒,用于打印连接数
if(nIndex == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents() failed\n") ;
break ;
}
else if(nIndex == WSA_WAIT_TIMEOUT) //超时,我自己添加的,测试所用
{
printf("现在连接数: %d\n",g_nBufferCount ) ;
continue ;
}
nIndex = nIndex - WSA_WAIT_EVENT_0 ;
for(int i = 0 ; i <= nIndex ; ++i)
{
int nRet = WSAWaitForMultipleEvents(1,&g_events[i],TRUE,0,FALSE) ; //逐个查看触发的对象
if(nRet == WSA_WAIT_TIMEOUT)
{
continue ;
}
else
{
WSAResetEvent(g_events[i]) ;
//重新建立g_events数组
if(0 == i)
{
RebuildArray() ;
continue ;
}
//处理这个I/O
PBUFFER_OBJ pBuffer = FindBufferObj(g_events[i]) ;
if(pBuffer != NULL)
{
if(!HandleIO(pBuffer))
{
RebuildArray() ;
}
}
}
}
}
return 0 ;
}
//申请缓冲区对象
PBUFFER_OBJ GetBufferObj(PSOCKET_OBJ pSocket ,ULONG nLen)
{
if(g_nBufferCount > WSA_MAXIMUM_WAIT_EVENTS-1)
{
return NULL ;
}
PBUFFER_OBJ pBuffer = (PBUFFER_OBJ)GlobalAlloc(GPTR,sizeof(BUFFER_OBJ)) ;
if(pBuffer != NULL)
{
pBuffer->buff = (char*)GlobalAlloc(GPTR,nLen) ;
pBuffer->ol.hEvent = WSACreateEvent() ;
pBuffer->pSocket = pSocket ; //共享一个监听套接字
pBuffer->sAccept = INVALID_SOCKET ;
//将新的BUFFER_OBJ添加到列表中,单线程无需要同步
if(NULL == g_pBufferHead)
{
g_pBufferHead = g_pBufferTail = pBuffer ;
}
else
{
g_pBufferTail->pNext = pBuffer ;
g_pBufferTail = pBuffer ;
}
g_events[++g_nBufferCount] = pBuffer->ol.hEvent ; //这里有增加计数
}
return pBuffer ;
}
//释放缓冲区对象
void FreeBufferObj(PBUFFER_OBJ pBuffer)
{
//从列表中移除BUFFER_OBJ对象
PBUFFER_OBJ pTest = g_pBufferHead ;
BOOL bFind = FALSE ;
if(pTest == pBuffer) //释放的是头结点
{
g_pBufferHead = g_pBufferTail = NULL ;
bFind = TRUE ;
}
else
{
while(pTest != NULL && pTest->pNext != pBuffer)
{
pTest = pTest->pNext ;
}
if(pTest != NULL) //pTest为被删结点的前一个结点
{
pTest->pNext = pBuffer->pNext ;
if(NULL == pTest->pNext) //删除的是尾结点
{
g_pBufferTail = pTest ;
}
bFind = TRUE ;
}
}
//释放它占用的内存空间
if(bFind)
{
g_nBufferCount-- ;
CloseHandle(pBuffer->ol.hEvent) ;
GlobalFree(pBuffer->buff) ;
GlobalFree(pBuffer) ;
}
}
//申请套接字对象
PSOCKET_OBJ GetSocketObj(SOCKET s)
{
PSOCKET_OBJ pSocket = (PSOCKET_OBJ)GlobalAlloc(GPTR,sizeof(SOCKET_OBJ)) ;
if(pSocket != NULL)
{
pSocket->s = s ;
}
return pSocket ;
}
//释放套接字对象
void FreeSocketObj(PSOCKET_OBJ pSocket)
{
if(pSocket->s != INVALID_SOCKET)
{
closesocket(pSocket->s) ;
}
GlobalFree(pSocket) ;
}
//寻找已经触发的缓冲区对象
PBUFFER_OBJ FindBufferObj(HANDLE hEvent)
{
PBUFFER_OBJ pBuffer = g_pBufferHead ;
while(pBuffer != NULL)
{
if(pBuffer->ol.hEvent == hEvent)
{
break ;
}
pBuffer = pBuffer->pNext ;
}
return pBuffer ;
}
//因为是数组与链表,所以需要重建对齐
void RebuildArray()
{
PBUFFER_OBJ pBuffer = g_pBufferHead ;
int i = 1 ;
while(pBuffer != NULL)
{
g_events[i++] = pBuffer->ol.hEvent ;
pBuffer = pBuffer->pNext ;
}
}
//提交接受连接
BOOL PostAccept(PBUFFER_OBJ pBuffer)
{
PSOCKET_OBJ pSocket = pBuffer->pSocket ;
if(pSocket->lpfnAcceptEx != NULL)
{
//设置I/O类型,增加套接字上的重叠I/O计数
pBuffer->nOperation = OP_ACCEPT ;
pSocket->nOutstandingOps++ ;
//投递此重叠I/O
DWORD dwBytes ;
pBuffer->sAccept = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
BOOL b = pSocket->lpfnAcceptEx(
pSocket->s, //已经调用过listen的监听套接字
pBuffer->sAccept, //新连接将在这个套接字上面产生,一定为不能用的`
pBuffer->buff, //第一块数据的接收缓冲区,如果为0则,新连接则不等待第一块数据
BUFFER_SIZE-((sizeof(sockaddr_in)+16)*2), //第一块数据缓冲区的大小,不计入两个地址的大小。
sizeof(sockaddr_in)+16, //本地地址结构的大小,必须比16还要大
sizeof(sockaddr_in)+16, //远端地址结构的大小,必须比16还要大
&dwBytes, //实际接收数据的大小,
&pBuffer->ol) ; //Overlapped结构
if(!b)
{
if(WSAGetLastError() != WSA_IO_PENDING)
{
return FALSE ;
}
return TRUE ;
}
}
return FALSE ;
}
//提交接受
BOOL PostRecv(PBUFFER_OBJ pBuffer)
{
//设置I/O类型,增加套接字上的重叠I/O计数
pBuffer->nOperation = OP_READ ;
pBuffer->pSocket->nOutstandingOps++ ;
//投递此重叠I/O
DWORD dwBytes ;
DWORD dwFlags = 0 ;
WSABUF buf ;
buf.buf = pBuffer->buff ; //方便引用而已
buf.len = pBuffer->nLen ;
if(WSARecv(pBuffer->pSocket->s,&buf,1,&dwBytes,&dwFlags,&pBuffer->ol,NULL) != NO_ERROR)
{
if(WSAGetLastError() != WSA_IO_PENDING)
{
return FALSE ;
}
}
return TRUE ;
}
//投递发送的请求
BOOL PostSend(PBUFFER_OBJ pBuffer)
{
//设置I/O类型,增加套接字上的重叠I/O计数
pBuffer->nOperation = OP_WRITE ;
pBuffer->pSocket->nOutstandingOps++ ;
//投递此重叠I/O
DWORD dwBytes ;
DWORD dwFlags = 0 ;
WSABUF buf ;
buf.buf = pBuffer->buff ;
buf.len = pBuffer->nLen ;
if(WSASend(pBuffer->pSocket->s,&buf,1,&dwBytes,dwFlags,&pBuffer->ol,NULL) != NO_ERROR)
{
if(WSAGetLastError() != WSA_IO_PENDING)
{
return FALSE ;
}
}
return TRUE ;
}
//I/O处理函数
BOOL HandleIO(PBUFFER_OBJ pBuffer)
{
PSOCKET_OBJ pSocket = pBuffer->pSocket ; //从BUFFER_OBJ对象中提取SOCKET_OBJ对象指针,为的是方便引用 ;
pSocket->nOutstandingOps-- ;
//获取重叠操作结果
DWORD dwTrans ;
DWORD dwFlags ;
BOOL bRet = WSAGetOverlappedResult(pSocket->s,&pBuffer->ol,&dwTrans,FALSE,&dwFlags) ;
if(!bRet)
{
//在此套接字上有错误发生,因此,关闭套接字,移除此缓冲区对象。
//如果没有其它抛出的I/O请求了,释放此缓冲区对象,否则,等待此套接字上的其它I/O也完成
if(pSocket->s != INVALID_SOCKET)
{
closesocket(pSocket->s) ;
pSocket->s = INVALID_SOCKET ;
}
if(0 == pSocket->nOutstandingOps)
{
FreeSocketObj(pSocket) ;
}
FreeBufferObj(pBuffer) ;
return FALSE ;
}
//没有错误发生,处理已完成的I/O
switch(pBuffer->nOperation)
{
case OP_ACCEPT : //接收到一个新的连接,并接收到了对方发来的第一个封包
{
//为新客户创建一个SOCKET_OBJ对象
PSOCKET_OBJ pClient = GetSocketObj(pBuffer->sAccept) ;
//为发送数据创建一个BUFFER_OBJ对象,这个对象在套接字出错或者关闭时释放
PBUFFER_OBJ pSend = GetBufferObj(pClient,BUFFER_SIZE) ;
if(NULL == pSend)
{
printf("Too much connections!\n") ;
FreeSocketObj(pClient) ;
return FALSE ;
}
RebuildArray() ;
//将数据复制到发送缓冲区
pSend->nLen = dwTrans ;
memcpy(pSend->buff,pBuffer->buff,dwTrans) ;
//打印接收到的数据,自己添加的
printf("接收到的数据:%s\n",pSend->buff) ;
//投递此发送I/O(将数据回显给客户)
if(!PostSend(pSend))
{
//万一出错的话,释放上面刚申请的两个对象
FreeSocketObj(pSocket) ;
FreeBufferObj(pSend) ;
return FALSE ;
}
//继续投递接受I/O
PostAccept(pBuffer);
}
break ;
case OP_READ : // 接收数据完成
{
if(dwTrans > 0)
{
//创建一个缓冲区,以发送数据。这里就使用原来的缓冲区
PBUFFER_OBJ pSend = pBuffer ;
pSend->nLen = dwTrans ;
//打印接收到的数据,自己添加的
printf("接收到的数据:%s\n",pSend->buff) ;
//投递发送I/O(将数据回显给客户)
PostSend(pSend) ;
}
else //套接字关闭
{
//关闭一个连接
printf("关闭一个连接\n") ;
//必须先关闭套接字,以便在此套接字上投递的其它I/O也返回
if(pSocket->s != INVALID_SOCKET)
{
closesocket(pSocket->s) ;
pSocket->s = INVALID_SOCKET ;
}
if(pSocket->nOutstandingOps == 0)
{
FreeSocketObj(pSocket) ;
}
FreeBufferObj(pBuffer) ;
return FALSE ;
}
}
break ;
case OP_WRITE : //发送数据完成
{
if(dwTrans > 0)
{
//打印发送数据消息
printf("发送数据完成\n") ;
//继续使用这个缓冲区投递接收数据的请求
pBuffer->nLen = BUFFER_SIZE ;
PostRecv(pBuffer) ;
}
else //套接字关闭
{
//同样,要先关闭套接字
if(pSocket->s != INVALID_SOCKET)
{
closesocket(pSocket->s ) ;
pSocket->s = INVALID_SOCKET ;
}
if(pSocket->nOutstandingOps == 0)
{
FreeSocketObj(pSocket) ;
}
FreeBufferObj(pBuffer) ;
return FALSE ;
}
}
break ;
}
return TRUE ;
}
总结:
基于重叠(Overlapped)I/O模型的服务器,虽然能够一次投递多个重叠的I/O请求,但是程序的伸缩性感觉和WSAEventSelect等模型差不多。因为当发出异步的I/O请求之后,程序依然需要在某个时间点上面调用WSAWaitForMultipleEvents来等待异步I/O的完成,这里又会出现了WSAEventSelect模型上的问题。