重叠I/O,Overlapped I/O。

一、 重叠I/O的优点
 
1. 可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。
2. 比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O使应用程序能达到更佳的系统性能。因为它和这4种模型不同的是,使用重叠I/O的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
3. 从《windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右 ―― 非常好的性能,已经直逼完成端口了。

二、重叠I/O的基本原理
 
概括一点说,重叠I/O是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
 
需要注意的是,有两个方法可以用来管理重叠I/O请求的完成情况(就是说接到重叠操作完成的通知):
 
1. 事件对象通知(Event Object Notification) 。
2. 完成例程(Completion Routines)(注意,这里并不是完成端口)
 
本文只是讲述如何来使用事件通知的的方法实现重叠I/O,如没有特殊说明,本文的重叠I/O默认就是指基于事件通知的重叠I/O。
 
既然是基于事件通知,就要求将Windows事件对象与WSAOVERLAPPED结构关联在一起(WSAOVERLAPPED结构中专门有对应的参数),既然要使用重叠结构,我们常用的send, sendto, recv, recvfrom也都要被WSASend,WSASendto, WSARecv, WSARecvFrom替换掉了,它们的用法我后面会讲到,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要德数据了。
 
三、关于重叠I/O的基础知识
 
下面来介绍并举例说明一下编写重叠I/O的程序中将会使用到的几个关键数据结构和函数。
 
1. WSAOVERLAPPED结构
 
这个结构自然是重叠I/O里的核心,它是这么定义的:
 
typedef struct _WSAOVERLAPPED

{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent; // 唯一需要关注的参数,用来关联WSAEvent对象
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
我们需要把WSARecv等操作投递到一个重叠结构上,而我们又需要一个与重叠结构“绑定”在一起的事件对象来通知我们操作的完成,看到了和hEvent参数,不用我说你们也该知道如何来来把事件对象绑定到重叠结构上吧?大致如下:
WSAEVENT event; // 定义事件
WSAOVERLAPPED AcceptOverlapped ; // 定义重叠结构
event = WSACreateEvent(); // 建立一个事件对象句柄
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 初始化重叠结构
AcceptOverlapped.hEvent = event;
 
2. WSARecv系列函数
 
在重叠I/O中,接收数据就要靠它了,它的参数也比recv要多,因为要用刀重叠结构嘛,它是这样定义的:
 
int WSARecv

(
SOCKET s, // 当然是投递这个操作的套接字
LPWSABUF lpBuffers, // 接收缓冲区,与Recv函数不同这里需要一个由WSABUF结构构成的数组
DWORD dwBufferCount, // 数组中WSABUF结构的数量
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回所接收到的字节数
LPDWORD lpFlags, // 说来话长了,我们这里设置为0 即可
LPWSAOVERLAPPED lpOverlapped, // “绑定”的重叠结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程中将会用到的参数,我们这里设置为 NULL
);

返回值:
WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了。
 
举个例子:(变量的定义顺序和上面的说明的顺序是对应的,下同)
SOCKET s;
WSABUF DataBuf; // 定义WSABUF结构的缓冲区
// 初始化一下DataBuf
#define DATA_BUFSIZE 5096
char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;
// 重叠结构,如果要处理多个操作,这里当然需要一个WSAOVERLAPPED数组。
WSAOVERLAPPED AcceptOverlapped ;

// 事件,如果要多个事件,这里当然也需要一个WSAEVENT数组。需要注意的是,可能一个SOCKET同时会有一个以上的重叠请求,也就会对应一个以上的WSAEVENT。
WSAEVENT event;

Event = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = event; // 关键的一步,把事件句柄“绑定”到重叠结构上。

作了这么多工作,终于可以使用WSARecv来把我们的请求投递到重叠结构上了,呼…
WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes, &Flags, &AcceptOverlapped, NULL);
其他的函数我这里就不一一介绍了,因为我们毕竟还有MSDN这么个好帮手,而且在讲后面的完成例程和完成端口的时候我还会讲到一些 ^_^
 
3. WSAWaitForMultipleEvents函数
 
熟悉WSAEventSelect模型的朋友对这个函数肯定不会陌生,不对,其实大家都不应该陌生,这个函数与线程中常用的WaitForMultipleObjects函数有些地方还是比较像的,因为都是在等待某个事件的触发嘛。
 
因为我们需要事件来通知我们重叠操作的完成,所以自然需要这个等待事件的函数与之配套。
 
DWORD WSAWaitForMultipleEvents(
DWORD cEvents, // 等候事件的总数量
const WSAEVENT* lphEvents, // 事件数组的指针
BOOL fWaitAll, // 这个要多说两句:如果设置为 TRUE,则事件数组中所有事件被传信的时候函数才会返回;FALSE则任何一个事件被传信函数都要返回。我们这里肯定是要设置为FALSE的。
DWORD dwTimeout, // 超时时间,如果超时,函数会返回。
WSA_WAIT_TIMEOUT  // 如果设置为0,函数会立即返回;如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回。在这里不建议设置为WSA_INFINITE,因为…后面再讲吧。
 
BOOL fAlertable // 在完成例程中会用到这个参数,这里我们先设置为FALSE。
);

返回值:
WSA_WAIT_TIMEOUT :最常见的返回值,我们需要做的就是继续等待。
WSA_WAIT_FAILED : 出现了错误,请检查cEvents和lphEvents两个参数是否有效如果事件数组中有某一个事件被传信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值 WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。具体的例子就先不在这里举了,后面还会讲到。
注意:WSAWaitForMultipleEvents函数只能支持由WSA_MAXIMUM_WAIT_EVENTS对象定义的一个最大值,是 64,就是说WSAWaitForMultipleEvents只能等待64个事件,如果想同时等待多于64个事件,就要创建额外的工作者线程,就不得不去管理一个线程池,这一点就不如下一篇要讲到的完成例程模型了。
 
4. WSAGetOverlappedResult函数
 
既然我们可以通过WSAWaitForMultipleEvents函数来得到重叠操作完成的通知,那么我们自然也需要一个函数来查询一下重叠操作的结果,定义如下:
 
BOOL WSAGetOverlappedResult(
SOCKET s, // 套接字,不用说了
LPWSAOVERLAPPED lpOverlapped, // 要查询的那个重叠结构的指针
LPDWORD lpcbTransfer, // 本次重叠操作的实际接收(或发送)的字节数
… //其他参数暂略,以后补充。
);
如果WSAGetOverlappedResult完成以后,第三个参数返回是 0 ,则说明通信对方已经关闭连接,我们这边的SOCKET, Event之类的也就可以关闭了。
 
四、实现重叠I/O的步骤
 
下面我们配合代码,来一步步的讲解如何亲手完成一个重叠I/O。
 
第一步:定义变量
 
#define DATA_BUFSIZE 4096 // 接收缓冲区大小
SOCKET ListenSocket, // 监听套接字
SOCKET AcceptSocket; // 与客户端通信的套接字
WSAOVERLAPPED AcceptOverlapped; // 重叠结构一个
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 用来通知重叠操作完成的事件句柄数组
WSABUF DataBuf[DATA_BUFSIZE] ; 
DWORD dwEventTotal = 0, // 程序中事件的总数
dwRecvBytes = 0, // 接收到的字符长度
Flags = 0; // WSARecv的参数
 
 
第二步:创建一个套接字,开始在指定的端口上监听连接请求。
 
和其他SOCKET初始化一样,直接照搬即可。
 
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);
 
ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); //创建TCP套接字
SOCKADDR_IN ServerAddr; //分配端口及协议族并绑定
ServerAddr.sin_family=AF_INET; 
ServerAddr.sin_addr.S_un.S_addr =htonl(INADDR_ANY); 
ServerAddr.sin_port=htons(11111);
bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字
listen(ListenSocket, 5); //开始监听
 
第三步:接受一个入站的连接请求
 
AcceptSocket = accept (ListenSocket, NULL,NULL) ; 
当然,这里是偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样:
 
SOCKADDR_IN ClientAddr; // 定义一个客户端得地址结构作为参数
int addr_length=sizeof(ClientAddr);
AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);
// 于是乎,我们就可以轻松得知连入客户端的信息了
LPCTSTR lpIP = inet_ntoa(ClientAddr.sin_addr); // IP
UINT nPort = ClientAddr.sin_port; // Port
 
第四步:建立并初始化重叠结构
 
为连入的这个套接字新建立一个WSAOVERLAPPED重叠结构,并且象前面讲到的那样,为这个重叠结构从事件句柄数组里挑出一个空闲的对象句柄“绑定”上去。
 
// 创建一个事件,dwEventTotal可以暂时先作为Event数组的索引。
EventArray[dwEventTotal] = WSACreateEvent(); 
 
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置零
AcceptOverlapped.hEvent = EventArray[dwEventTotal]; // 关联事件
 
char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer; // 初始化一个WSABUF结构
dwEventTotal ++; // 总数加一
 
第五步:以WSAOVERLAPPED结构为参数,在套接字上投递WSARecv请求。
 
各个变量都已经初始化OK以后,我们就可以开始Socket操作了,然后让WSAOVERLAPPED结构来替我们管理I/O 请求,我们只须等待事件的触发就可以了。
 
if(SOCKET_ERROR==WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,&AcceptOverlapped, NULL))

// 返回WSA_IO_PENDING是正常情况,表示IO操作正在进行,不能立即完成。如果不是WSA_IO_PENDING错误,就大事不好了!
if(WSAGetLastError() != WSA_IO_PENDING) 
{
// 那就只能关闭大吉了
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwEventTotal]);
}
}
 
第六步:用WSAWaitForMultipleEvents函数等待重叠操作返回的结果

因为前面已经把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知。
DWORD dwIndex;

// 等候重叠I/O调用结束
dwIndex=WSAWaitForMultipleEvents(dwEventTotal,EventArray ,FALSE ,WSA_INFINITE,FALSE);
// 注意这里返回的Index并非是事件在数组里的Index,而是需要减去WSA_WAIT_EVENT_0
dwIndex = dwIndex – WSA_WAIT_EVENT_0;
 
第七步:使用WSAResetEvent函数重设当前这个用完的事件对象
 
事件已经被触发了之后,它对于我们来说已经没有利用价值了,所以要将它重置一下留待下一次使用,很简单,就一步,连返回值都不用考虑。
WSAResetEvent(EventArray[dwIndex]);
 
第八步:使用WSAGetOverlappedResult函数取得重叠调用的返回状态
 
这是我们最关心的事情,费了那么大劲投递的这个重叠操作究竟是个什么结果呢?其实对于本模型来说,唯一需要检查一下的就是对方的Socket连接是否已经关闭了。
 
DWORD dwBytesTransferred;
WSAGetOverlappedResult( AcceptSocket,AcceptOverlapped,&dwBytesTransferred, FALSE, &Flags);
// 先检查通信对方是否已经关闭连接,如果dwBytesTransferred等于0则表示连接已经关闭,就关闭套接字。
if(dwBytesTransferred == 0)
{
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwIndex]); // 关闭事件
return;
}
 
 
第九步:“享受”接收到的数据
 
如果程序执行到了这里,那么就说明一切正常,WSABUF结构里面就存有我们WSARecv来的数据了,终于到了尽情享用成果的时候了!喝杯茶,休息一下吧~~~^_^
 
DataBuf.buf就是一个char*字符串指针,听凭你的处理吧,这里就不多说了。
 
 
第十步:同第五步一样,在套接字上继续投递WSARecv请求,然后重复步骤 6 ~ 9。
 
这样一路下来,我们终于可以从客户端接收到数据了,但是回想起来,呀~~~~~,这岂不是只能收到一次数据,然后程序不就Over了?所以我们接下来不得不重复一遍第五步到第九步的工作,再次在这个套接字上投递另一个WSARecv请求,并且使整个过程循环起来,are u clear?
 
五、多客户端情况的注意事项
 
完成了上面的循环以后,重叠I/O就已经基本上搭建好了80%了,为什么不是100%呢?因为仔细一回想起来,呀~~~~~~~,这样岂不是只能连接一个客户端?是的,如果只处理一个客户端,那重叠I/O就半点优势也没有了,我们正是要使用重叠I/O来处理多个客户端。
 
所以我们不得不再对结构作一些改动。
 
首先需要一个SOCKET数组 ,分别用来和每一个SOCKET通信。其次,因为重叠I/O中每一个SOCKET操作都是要“绑定”一个重叠结构的,所以需要为每一个SOCKET操作搭配一个WSAOVERLAPPED结构,但是这样说并不严格,因为如果每一个SOCKET同时只有一个操作,比如WSARecv,那么一个SOCKET就可以对应一个WSAOVERLAPPED结构,但是如果一个SOCKET上会有WSARecv 和WSASend两个操作,那么一个SOCKET肯定就要对应两个WSAOVERLAPPED结构,所以有多少个SOCKET操作就会有多少个WSAOVERLAPPED结构。
 
然后,同样是为每一个WSAOVERLAPPED结构都要搭配一个WSAEVENT事件,所以说有多少个SOCKET操作就应该有多少个WSAOVERLAPPED结构,有多少个WSAOVERLAPPED结构就应该有多少个WSAEVENT事件,最好把SOCKET、WSAOVERLAPPED 、WSAEVENT三者的关联起来,到了关键时刻才会临危不乱。
 
须要开两个线程。一个线程用来循环监听端口,接收请求的连接,然后给在这个套接字上配合一个WSAOVERLAPPED结构投递第一个WSARecv请求。另一个个线程用来不停的对WSAEVENT数组WSAWaitForMultipleEvents,等待任何一个重叠操作的完成,然后根据返回的索引值进行处理,处理完毕以后再继续投递下一个WSARecv请求。
 
这里需要注意一点的是,前面是把WSAWaitForMultipleEvents函数的参数设置为WSA_INFINITE的,但是在多客户端的时候这样就不好了,需要设定一个超时时间,如果等待超时了再重新WSAWaitForMultipleEvents。因为WSAWaitForMultipleEvents函数在没有触发的时候是阻塞在那里的,我们可以设想一下,这时如果监听线程中接入了新的连接,自然也会为这个连接增加一个Event,但是WSAWaitForMultipleEvents还是阻塞在那里就不会处理这个新连接的Event了。

重叠I/O之事件对象通知相关推荐

  1. IO模型之重叠Overlapped IO基于事件通知

    文章目录 1. 重叠Overlapped IO模型 2. 重叠Overlapped IO模型的优点 3. 重叠IO请求的通知的方式 4. 重叠I0基于事件通知的流程 5. WSAOVERLAPPED结 ...

  2. 从零开始学习jQuery (五) 事件与事件对象【转】

    一.摘要 事件是脚本编程的灵魂. 所以本章内容也是jQuery学习的重点. 本文将对jQuery中的事件处理以及事件对象进行详细的讲解. 二.前言 本篇文章是至今为止本系列内容最多的一篇, 足以可见其 ...

  3. 从零开始学习jQuery (五) 事件与事件对象

    本系列文章导航 从零开始学习jQuery (一) 开天辟地入门篇 从零开始学习jQuery (二) 万能的选择器 从零开始学习jQuery (三) 管理jQuery包装集 从零开始学习jQuery ( ...

  4. jQuery框架学习第五天:事件与事件对象

    jQuery框架学习第一天:开始认识jQuery jQuery框架学习第二天:jQuery中万能的选择器 jQuery框架学习第三天:如何管理jQuery包装集 jQuery框架学习第四天:使用jQu ...

  5. 【js】event(事件对象)详解

    1.事件对象 Event 对象代表事件的状态,比如事件在其中发生的元素.键盘按键的状态.鼠标的位置.鼠标按钮的状态. 什么时候会产生Event 对象呢? 例如: 当用户单击某个元素的时候,我们给这个元 ...

  6. windows C/C++ 内核对象、事件对象、信号量

    承接上一篇:https://blog.csdn.net/uVarAndMethod/article/details/90360838 1.内核对象: 进程.线程.文件.文件映射.事件.互斥体等等 2. ...

  7. js事件Event对象(自定义事件对象 CustomEvent)

    文章目录 一.参考 二.Event 接口介绍 2.1 事件分类 三. 创建过时Event不推荐 3.1 document.createEvent 3.1.1 语法`var event = docume ...

  8. js-event(事件对象)详解

    1.事件对象 Event 对象代表事件的状态,比如事件在其中发生的元素.键盘按键的状态.鼠标的位置.鼠标按钮的状态. 什么时候会产生Event 对象呢?  例如:当用户单击某个元素的时候,我们给这个元 ...

  9. 后处理程序文件大小的变量_【每日一题】(17题)面试官问:JS中事件流,事件处理程序,事件对象的理解?...

    关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 2020,实「鼠」不易 2021,「牛」转乾坤 风劲潮涌当扬帆,任重道远须奋蹄! 一.前言 2020.12.23 立 ...

最新文章

  1. gensim中word2vec使用
  2. 文巾解题 50. Pow(x, n)
  3. U3D 扩展方法 Dotween tolua
  4. 国外JAVA与IT技术网站地址
  5. 导致集群重启_干货丨如何水平扩展和垂直扩展DolphinDB集群?
  6. 求n的阶乘的算法框图_单片机常用的14个C语言算法
  7. C#委托的异步调用[转]
  8. oracle死锁优化,Oracle性能优化之LockContention(转)
  9. Hive的数据模型-外部表
  10. spring-boot-admin 2.0小试牛刀
  11. [******] 堆排序
  12. 第一个hadoop 程序
  13. Unity3D基础16:网格过滤器和渲染器
  14. Microsoft.ACE.OLEDB.12.0 读取混合列
  15. 项目管理工具maven的使用
  16. Centos 7环境MySql8.0.28源码安装
  17. 手机java应用安装失败_解决OPPO手机在Android studio 环境下安装失败问题
  18. 天然产物数据库综述:2020年从哪里找天然产物数据
  19. java钟表动画_js实现一个简单钟表动画(javascript+html5 canvas)
  20. 8个微信实用技巧,你知道多少?

热门文章

  1. ES6新特性之解构表达式
  2. 异常注意事项_finally有return语句
  3. Spring Session快速入门
  4. kafka常用命令及问题解决
  5. verilog设计简易正弦波信号发生器_电子设计竞赛教程-信号源类
  6. Java学习路线详解
  7. 编写自适应高度的 textarea
  8. open-falcon的邮件报警
  9. 使用ping命令查看网络延迟--用Enki学Linux系列(7)
  10. 数据库索引的实现原理及查询优化