重叠IO模型

比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据

http://blog.163.com/window2008s/blog/static/5440295920087151593603/

1. 重叠模型的优点

2. 重叠模型的基本原理

3. 关于重叠模型的基础知识

4. 重叠模型的实现步骤

5. 多客户端情况的注意事项

一.重叠模型的优点

1.可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。

2.比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。

         因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。

  而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。

3. 从《windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右 ―― 非常好的性能,已经直逼完成端口。

二.重叠模型的基本原理

      说了这么多的好处,你一定也跃跃欲试了吧,不过我们还是要先提一下重叠模型的基本原理。

      概括一点说,重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。

      需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):

1. 事件对象通知(event object notification)

2. 完成例程(completion routines) ,注意,这里并不是完成端口

 

---------------------------------------事件对象通知(event object notification)篇------------------------------------------

 

  而本文只是讲述如何来使用事件通知的的方法实现重叠IO模型,完成例程的方法准备放到下一篇讲 :) (内容太多了,一篇写不完啊) ,如没有特殊说明,本文的重叠模型默认就是指的基于事件通知的重叠模型。

   既然是基于事件通知,就要求将Windows事件对象与WSAOVERLAPPED结构关联在一起(WSAOVERLAPPED结构中专门有对应的参 数),通俗一点讲,就是。。。。对了,忘了说了,既然要使用重叠结构,我们常用的send, sendto, recv, recvfrom也都要被WSASend, WSASendto, WSARecv, WSARecvFrom替换掉了, 它们的用法我后面会讲到,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑 定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完 WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们 想要德数据了。

      也许说了半天你还是不大明白,那就继续往后面看吧。。。。。。。-_-b,语言表达能力有限啊~~~

三.关于重叠模型的基础知识

      下面来介绍并举例说明一下编写重叠模型的程序中将会使用到的几个关键函数。

1. WSAOVERLAPPED结构

这个结构自然是重叠模型里的核心,它是这么定义的

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;    // Done !!

2. WSARecv系列函数

在重叠模型中,接收数据就要靠它了,它的参数也比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操作成功了,但是

I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成

举个例子:(变量的定义顺序和上面的说明的顺序是对应的,下同)

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 AcceptOverlapped ;// 如果要处理多个操作,这里当然需要一个

// WSAOVERLAPPED数组

WSAEVENT event;     // 如果要多个事件,这里当然也需要一个WSAEVENT数组

// 需要注意的是可能一个SOCKET同时会有一个以上的重叠请求,

//  也就会对应一个以上的WSAEVENT

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,因为。。。后面再讲吧..-_-b

BOOL fAlertable       // 在完成例程中会用到这个参数,这里我们先设置为FALSE

);

返回值:

WSA_WAIT_TIMEOUT :最常见的返回值,我们需要做的就是继续Wait

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,                   // SOCKET,不用说了

LPWSAOVERLAPPED lpOverlapped,  // 这里是我们想要查询结果的那个重叠结构的指针

LPDWORD lpcbTransfer,     // 本次重叠操作的实际接收(或发送)的字节数

BOOL fWait,               // 设置为TRUE,除非重叠操作完成,否则函数不会返回

// 设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE

// 错误为WSA_IO_INCOMPLETE

//不过因为我们是等待事件传信来通知我们操作完成,所以我们这里设置成什么都没有作用…

LPDWORD lpdwFlags       // 指向DWORD的指针,负责接收结果标志

);

这个函数没什么难的,这里我们也不需要去关注它的返回值,直接把参数填好调用就可以了,这里就先不举例了

唯一需要注意一下的就是如果WSAGetOverlappedResult完成以后,第三个参数返回是 0 ,则说明通信对方已经关闭连接,我们这边的SOCKET, Event之类的也就可以关闭了

四。     实现重叠模型的步骤

   作了这么多的准备工作,费了这么多的笔墨,我们终于可以开始着手编码了。其实慢慢的你就会明白,要想透析重叠结构的内部原理也许是要费点功夫,但是只是 学会如何来使用它,却是真的不难,唯一需要理清思路的地方就是和大量的客户端交互的情况下,我们得到事件通知以后,如何得知是哪一个重叠操作完成了,继而 知道究竟该对哪一个套接字进行处理,应该去哪个缓冲区中的取得数据,everything will be OK^_^。

  下面我们配合代码,来一步步的讲解如何亲手完成一个重叠模型。

【第一步】定义变量…………

#define DATA_BUFSIZE     4096          // 接收缓冲区大小

SOCKET         ListenSocket,             // 监听套接字

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 请求,我们只用等待事件的触发就OK了。

if(WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,

& AcceptOverlapped, NULL) == SOCKET_ERROR)

{

// 返回WSA_IO_PENDING是正常情况,表示IO操作正在进行,不能立即完成

// 如果不是WSA_IO_PENDING错误,就大事不好了~~~~~~!!!

if(WSAGetLastError() != WSA_IO_PENDING)   

{

// 那就只能关闭大吉了

closesocket(AcceptSocket);

WSACloseEvent(EventArray[dwEventTotal]);

}

}

【第六步】 用WSAWaitForMultipleEvents函数等待重叠操作返回的结果

  我们前面已经给WSARecv关联的重叠结构赋了一个事件对象句柄,所以我们这里要等待事件对象的触发与之配合,而且需要根据 WSAWaitForMultipleEvents函数的返回值来确定究竟事件数组中的哪一个事件被触发了,这个函数的用法及返回值请参考前面的基础知识 部分。

DWORD dwIndex;

// 等候重叠I/O调用结束

// 因为我们把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知

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);

// 先检查通信对方是否已经关闭连接

// 如果==0则表示连接已经,则关闭套接字

if(dwBytesTransferred == 0)

{

closesocket(AcceptSocket);

WSACloseEvent(EventArray[dwIndex]);    // 关闭事件

return;

}

【第九步】“享受”接收到的数据

如果程序执行到了这里,那么就说明一切正常,WSABUF结构里面就存有我们WSARecv来的数据了,终于到了尽情享用成果的时候了!喝杯茶,休息一下吧~~~^_^

DataBuf.buf就是一个char*字符串指针,听凭你的处理吧,我就不多说了

【第十步】同第五步一样,在套接字上继续投递WSARecv请求,重复步骤 6 ~ 9

  这样一路作下来,我们终于可以从客户端接收到数据了,但是回想起来,呀~~~~~,这样岂不是只能收到一次数据,然后程序不就Over了?…….-_-b   所以我们接下来不得不重复一遍第四步和第五步的工作,再次在这个套接字上投递另一个WSARecv请求,并且使整个过程循环起来,are u clear??

     大家可以参考我的代码,在这里就先不写了,因为各位都一定比我smart,领悟了关键所在以后,稍作思考就可以灵活变通了。

五. 客户端情况的注意事项

      完成了上面的循环以后,重叠模型就已经基本上搭建好了80%了,为什么不是100%呢?因为仔细一回想起来,呀~~~~~~~,这样岂不是只能连接一个客 户端??是的,如果只处理一个客户端,那重叠模型就半点优势也没有了,我们正是要使用重叠模型来处理多个客户端。

   所以我们不得不再对结构作一些改动。

1. 首先,肯定是需要一个SOCKET数组 ,分别用来和每一个SOCKET通信

其 次,因为重叠模型中每一个SOCKET操作都是要“绑定”一个重叠结构的,所以需要为每一个SOCKET操作搭配一个WSAOVERLAPPED结构,但 是这样说并不严格,因为如果每一个SOCKET同时只有一个操作,比如WSARecv,那么一个SOCKET就可以对应一个WSAOVERLAPPED结 构,但是如果一个SOCKET上会有WSARecv 和WSASend两个操作,那么一个SOCKET肯定就要对应两个WSAOVERLAPPED结构,所以有多少个SOCKET操作就会有多少个 WSAOVERLAPPED结构。

然后,同样是为每一个WSAOVERLAPPED结构都要搭配一个WSAEVENT事件,所以说 有多少个SOCKET操作就应该有多少个WSAOVERLAPPED结构,有多少个WSAOVERLAPPED结构就应该有多少个WSAEVENT事件, 最好把SOCKET – WSAOVERLAPPED – WSAEVENT三者的关联起来,到了关键时刻才会临危不乱:)

2. 不得不分作两个线程:

一个用来循环监听端口,接收请求的连接,然后给在这个套接字上配合一个WSAOVERLAPPED结构投递第一个WSARecv请求,然后进入第二个线程中等待操作完成。

第二个线程用来不停的对WSAEVENT数组WSAWaitForMultipleEvents,等待任何一个重叠操作的完成,然后根据返回的索引值进行处理,处理完毕以后再继续投递另一个WSARecv请求。

这里需要注意一点的是,前面我是把WSAWaitForMultipleEvents函数的参数设置为WSA_

INFINITE 的,但是在多客户端的时候这样就不OK了,需要设定一个超时时间,如果等待超时了再重新WSAWaitForMultipleEvents,因为 WSAWaitForMultipleEvents函数在没有触发的时候是阻塞在那里的,我们可以设想一下,这时如果监听线程忠接入了新的连接,自然也会 为这个连接增加一个Event,但是WSAWaitForMultipleEvents还是阻塞在那里就不会处理这个新连接的Event了。也不知道说明 白了没有。。。。。。-_-b 可能在这里你也体会不到,真正编码的时候就会明白了。

其他还有不明白的地方可以参考我的代码,代码里也有比较详尽的注释,  Enjoy~~~

不过可惜是为了照顾大多数人,使用的是MFC的代码,显得代码有些杂乱。

六.已知问题

     这个已知问题是说我的代码中的已知问题,可不是重叠结构的已知问题:)

   这个示例代码已经写好了很久了,这两天做最后测试的时候才发现竟然有两个Bug,而且还不是每次都会出现,5555,我最近是实在没有精力去改了,如果 有心的朋友能修改掉这两个Bug,那真是造福大家了,这篇文章都险些流产,我更没有经历去修改都快要淡忘了的代码的Bug了,我写在这里提醒一下大家了, 反正这个代码也仅仅是抛砖引玉而已,而且我觉得比起代码来还是文字比较珍贵^_^,因为重叠模型的代码网上也还是有不少的。两个Bug是这样的:

1.  多个客户端在连续退出的时候,有时会出现异常;

2.  有时多个客户端的接收缓冲区竟然会重叠到一起,就是说A客户端发送的数据后面会根有B客户端上次发来的数据。。。。。-_-b

   改进算法:其实代码中的算法还有很多可以改进的地方,limin朋友就向我提及过几个非常好的改进算法,比如如何在socket数组中寻找空闲的 socket用来通信,但是我并没有加到这份代码里面来,因为本来重叠模型的代码就比较杂,再加上这些东西恐怕反而会给初学者带来困难。但是非常欢迎各位 和我讨论重叠模型的改进算法以及我代码中存在问题!^_^

 

-----------------------------------------完成例程(Completion Routine)篇---------------------------------------------------

 http://softbbs.pconline.com.cn/9821027.html

本文配套的示例源码下载地址(在我的下载空间里)
http://download.csdn.net/user/PiggyXP

记得写这个系列的上一篇文章的时候已经是四年前了,准确的说是四年半以前了,翻开我尘封已久的blog,感觉到上面已经落了厚厚的一层尘土,突然又来了感觉,于是我翻箱倒柜的找出以前的资料,上传到了我的空间里,而且,顺便又为在网络编程苦海中苦苦寻觅的朋友带来一份礼物,这次为大家带来的是重叠IO模型里面的“完成例程”的实现方式及示例代码。

本文凝聚着笔者心血,如要转载,请指明原作者及出处,谢谢!不过代码写得不好,欢迎改进,而且没有版权,请随便散播、使用。^_^

OK, Let’s go !

Have fun!! 

 

本文配套的示例源码下载地址(在我的下载空间里)

http://download.csdn.net/user/PiggyXP

(VC++ 2008编写的多客户端MFC代码,配有非常非常详尽的注释,功能只是简单的显示一下各个客户端发来的字符,作为教学代码,为了使得代码结构清晰明了,简化了很多地方,用于产品开发的话还需要做很多改进,有错误或者不足的地方,非常欢迎大家不吝指出。)

代码界面示意图:

重叠IO模型 - ywx209100@126 - ywx209100@126的博客

本文假设你已经对重叠I/O的机制已有了解,否则请先参考本系列的前一篇《手把手教你玩转重叠IO模型》

 

目录:

1.

完成例程的优点

2.

完成例程的基本原理

3.

关于完成例程的函数介绍

4.

完成例程的实现步骤

5.

实际应用中应该进一步完善的地方

 

 

一.

完成例程的优点

1.

首先需要指明的是,这里的“完成例程”(Completion Routine)并非是大家所常听到的

“完成端口”(Completion Port),而是另外一种管理重叠I/O请求的方式,而至于什么是重叠I/O,简单来讲就是Windows系统内部管理I/O的一种方式,核心就是调用的ReadFile和WriteFile函数,在制定设备上执行I/O操作,不光是可用于网络通信,也可以用于其他需要的地方。

在Windows系统中,管理重叠I/O可以有三种方式:

(1)

上一篇中提到的基于事件通知的重叠I/O模型

(2)

本篇中将要讲述的基于“完成例程”的重叠I/O模型

(3)

下一篇中将要讲到的“完成端口”模型

虽然都是基于重叠I/O,但是因为前两种模型都是需要自己来管理任务的分派

,所以性能上没有区别,而完成端口是创建完成端口对象使操作系统亲自来管理任务的分派,所以完成端口肯定是能获得最好的性能。

2.

如果你想要使用重叠I/O机制带来的高性能模型,又懊恼于基于事件通知的重叠模型要收到64个等待事件的限制,还有点畏惧完成端口稍显复杂的初始化过程,那么“完成例程”无疑是你最好的选择!^_^ 因为完成例程摆脱了事件通知的限制,可以连入任意数量客户端而不用另开线程,也就是说只用很简单的一些代码就可以利用Windows内部的I/O机制来获得网络服务器的高性能,是不是心动了呢?那就一起往下看。。。。。。。。。。

3.

而且个人感觉“完成例程”的方式比重叠I/O更好理解,因为就和我们传统的“回调函数”是一样的,也更容易使用一些,推荐!

 

二.

完成例程的基本原理

概括一点说,上一篇拙作中提到的那个基于事件通知的重叠I/O模型,在你投递了一个请求以后(比如WSARecv),系统在完成以后是用事件来通知你的,而在完成例程中,系统在网络操作完成以后会自动调用你提供的回调函数,区别仅此而已,是不是很简单呢?

首先这里统一几个名词,包括“重叠操作”、“重叠请求”、“投递请求”等等,这是为了配合这的重叠I/O才这么讲的,说的直白一些,也就是你在代码中发出的WSARecv()、WSASend()等等网络函数调用。

上篇文章中偷懒没画图,这次还是画个流程图来说明吧,采用完成例程的服务器端,通信流程简单的来讲是这样的:

 重叠IO模型 - ywx209100@126 - ywx209100@126的博客

从图中可以看到,服务器端存在一个明显的异步过程,也就是说我们把客户端连入的SOCKET与一个重叠结构绑定之后,便可以将通讯过程全权交给系统内部自己去帮我们调度处理了,我们在主线程中就可以去做其他的事情,边等候系统完成的通知就OK,这也就是完成例程高性能的原因所在。

如果还没有看明白,我们打个通俗易懂的比方,完成例程的处理过程,也就像我们告诉系统,说“我想要在网络上接收网络数据,你去帮我办一下”(投递WSARecv操作),“不过我并不知道网络数据合适到达,总之在接收到网络数据之后,你直接就调用我给你的这个函数(比如_CompletionProess),把他们保存到内存中或是显示到界面中等等,全权交给你处理了”,于是乎,系统在接收到网络数据之后,一方面系统会给我们一个通知,另外同时系统也会自动调用我们事先准备好的回调函数,就不需要我们自己操心了。

看到这里,各位应该已经对完成例程的体系结构有了比价清晰的了解了吧,下面各位喝点咖啡转转脖子休息休息,然后就进入到下面的具体实现部分了。

 

一.

完成例程的函数介绍

这个部分将要介绍在完成例程模型中会使用到的关键函数,内容比较枯燥,大家要做好心理准备。不过在实际应用以前,很多东西肯定也不会理解得太深刻,可以先泛泛的了解一下,以后再回头复习这里的知识就可以了。

厄。。。。。。仔细审查了一下代码,发现其实这里也没有什么新函数好介绍了,大部分都是使用重叠模型那一章里介绍的一样的函数,需要查看的朋友请看这里《手把手教你玩转重叠IO模型》

,这里就不再重复了:

这里只补充一个知识点,就是咱们完成例程方式和前面的事件通知方式最大的不同之处就在于,我们需要提供一个回调函数供系统收到网络数据后自动调用,回调函数的参数定义应该遵照如下的函数原型:

 

1.

完成例程回调函数原型及传递方式

函数应该是这样定义的,函数名字随便起,但是参数类型不能错

 view plaincopy to clipboardprint?

  • Void CALLBACK _CompletionRoutineFunc(   
  • DWORD dwError, // 标志咱们投递的重叠操作,比如WSARecv,完成的状态是什么 

  • DWORD cbTransferred, // 指明了在重叠操作期间,实际传输的字节量是多大 

  •   LPWSAOVERLAPPED lpOverlapped, // 参数指明传递到最初的IO调用内的一个重叠  结构 

  • DWORD dwFlags  // 返回操作结束时可能用的标志(一般没用));

Void CALLBACK _CompletionRoutineFunc(  DWORD dwError, // 标志咱们投递的重叠操作,比如WSARecv,完成的状态是什么  DWORD cbTransferred, // 指明了在重叠操作期间,实际传输的字节量是多大  LPWSAOVERLAPPED lpOverlapped, // 参数指明传递到最初的IO调用内的一个重叠  结构  DWORD dwFlags  // 返回操作结束时可能用的标志(一般没用)); 

还有一点需要重点提一下的是,因为我们需要给系统提供一个如上面定义的那样的回调函数,以便系统在完成了网络操作后自动调用,这里就需要提一下究竟是如何把这个函数与系统内部绑定的呢?如下所示,在WSARecv函数中是这样绑定的

view plaincopy to clipboardprint?

  • int WSARecv(   
  •             SOCKET s,                      // 当然是投递这个操作的套接字 

  •             LPWSABUF lpBuffers,          // 接收缓冲区,与Recv函数不同 

  • // 这里需要一个由WSABUF结构构成的数组 

  • DWORD dwBufferCount,        // 数组中WSABUF结构的数量,设置为1即可 

  • LPDWORD lpNumberOfBytesRecvd,  // 如果接收操作立即完成,这里会返回函数调用 

  • // 所接收到的字节数 

  • LPDWORD lpFlags,             // 说来话长了,我们这里设置为0 即可 

  •   LPWSAOVERLAPPED lpOverlapped,  // “绑定”的重叠结构 

  •   LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine   
  • // 我们的完成例程函数的指针 

  • );  

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

其他参数我们可以先不用先细看,只看最后一个,看到了吗?直接在WSARecv的最后一个参数上,传递一下我们回调函数的指针就行了,这里注意一下,咱们这次多次提到的这个“完成例程”,其实就是指的咱们提供的这个回调函数。

view plaincopy to clipboardprint?

  • 举个例子:(变量的定义顺序和上面的说明的顺序是对应的,下同)   
  • SOCKET s;   
  • WSABUF DataBuf;           // 定义WSABUF结构的缓冲区 

  • // 初始化一下DataBuf 

  • #define DATA_BUFSIZE 4096 

  • char buffer[DATA_BUFSIZE];   

  • ZeroMemory(buffer, DATA_BUFSIZE);   
  • DataBuf.len = DATA_BUFSIZE;   
  • DataBuf.buf = buffer;   
  • DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;   

  • // 建立需要的重叠结构,每个连入的SOCKET上的每一个重叠操作都得绑定一个 

  • WSAOVERLAPPED AcceptOverlapped ;// 如果要处理多个操作,这里当然需要一个 

  • // WSAOVERLAPPED数组 

  • ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));   
  • // 作了这么多工作,终于可以使用WSARecv来把我们的完成例程函数绑定上了 

  • // 当然,假设我们的_CompletionRoutine函数已经定义好了 

  • WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes,    
  • &Flags, &AcceptOverlapped, _CompletionRoutine);  

举个例子:(变量的定义顺序和上面的说明的顺序是对应的,下同)SOCKET s;WSABUF DataBuf;           // 定义WSABUF结构的缓冲区// 初始化一下DataBuf#define DATA_BUFSIZE 4096char buffer[DATA_BUFSIZE];ZeroMemory(buffer, DATA_BUFSIZE);DataBuf.len = DATA_BUFSIZE;DataBuf.buf = buffer;DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;// 建立需要的重叠结构,每个连入的SOCKET上的每一个重叠操作都得绑定一个WSAOVERLAPPED AcceptOverlapped ;// 如果要处理多个操作,这里当然需要一个// WSAOVERLAPPED数组ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));// 作了这么多工作,终于可以使用WSARecv来把我们的完成例程函数绑定上了// 当然,假设我们的_CompletionRoutine函数已经定义好了WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes, &Flags, &AcceptOverlapped, _CompletionRoutine); 

其他的函数我这里就不一一介绍了,因为我们毕竟还有MSDN这么个好帮手,而且在讲后面的完成例程和完成端口的时候我还会讲到一些 ^_^

四.         完成例程的实现步骤

基础知识方面需要知道的就是这么多,下面我们配合代码,来一步步的讲解如何亲手实现一个完成例程模型(前面几步的步骤和基于事件通知的重叠I/O方法是一样的)。

【第一步】创建一个套接字,开始在指定的端口上监听连接请求

和其他的SOCKET初始化全无二致,直接照搬即可,在此也不多费唇舌了,需要注意的是为了一目了然,我去掉了错误处理,平常可不要这样啊,尽管这里出错的几率比较小。

 

view plaincopy to clipboardprint?

  1. WSADATA wsaData;   
  2. WSAStartup(MAKEWORD(2,2),&wsaData);   
  3.   
  4. ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  //创建TCP套接字   
  5.   
  6. SOCKADDR_IN ServerAddr;                           //分配端口及协议族并绑定   
  7. ServerAddr.sin_family=AF_INET;                                   
  8. ServerAddr.sin_addr.S_un.S_addr  =htonl(INADDR_ANY);             
  9. ServerAddr.sin_port=htons(11111);        // 在11111端口监听   
  10.                                     // 端口号可以随意更改,但最好不要少于1024   
  11.   
  12. bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字   
  13.   
  14. listen(ListenSocket, 5);                                   //开始监听  

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); // 在11111端口监听 // 端口号可以随意更改,但最好不要少于1024bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字listen(ListenSocket, 5); //开始监听

【第二步】接受一个入站的连接请求

  一个accept就完了,都是一样一样一样一样的啊~~~~~~~~~~

 至于AcceptEx的使用,在完成端口中我会讲到,这里就先不一次灌输这么多了,不消化啊^_^

 

view plaincopy to clipboardprint?

  1. AcceptSocket = accept (ListenSocket, NULL,NULL) ;   

AcceptSocket = accept (ListenSocket, NULL,NULL) ;

 

当然,这里是我偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样

 

 

view plaincopy to clipboardprint?

  1. SOCKADDR_IN ClientAddr;                   // 定义一个客户端得地址结构作为参数   
  2. int addr_length=sizeof(ClientAddr);   
  3. AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);   
  4. // 于是乎,我们就可以轻松得知连入客户端的信息了   
  5. LPCTSTR lpIP =  inet_ntoa(ClientAddr.sin_addr);      // 连入客户端的 IP   
  6. UINT nPort = ClientAddr.sin_port;                      // 连入客户端的Port  

SOCKADDR_IN ClientAddr; // 定义一个客户端得地址结构作为参数int addr_length=sizeof(ClientAddr);AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);// 于是乎,我们就可以轻松得知连入客户端的信息了LPCTSTR lpIP = inet_ntoa(ClientAddr.sin_addr); // 连入客户端的 IPUINT nPort = ClientAddr.sin_port; // 连入客户端的Port

【第三步】准备好我们的重叠结构

有新的套接字连入以后,新建立一个WSAOVERLAPPED重叠结构(当然也可以提前建立好),准备绑定到我们的重叠操作上去。这里也可以看到和上一篇中的明显区别,就是不用再为WSAOVERLAPPED结构绑定一个hEvent了。

view plaincopy to clipboardprint?

  1. // 这里只定义一个,实际上是每一个SOCKET的每一个操作都需要绑定一个重叠结构的,所以在实际使用面对多个客户端的时候要定义为数组,详见示例代码;   
  2. WSAOVERLAPPED AcceptOverlapped;    
  3. ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));      // 置零  

// 这里只定义一个,实际上是每一个SOCKET的每一个操作都需要绑定一个重叠结构的,所以在实际使用面对多个客户端的时候要定义为数组,详见示例代码;WSAOVERLAPPED AcceptOverlapped; ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置零

    

【第四步】开始在套接字上投递WSARecv请求,需要将第三步准备的WSAOVERLAPPED结构和我们定义的完成例程函数为参数

各个变量都已经初始化OK以后,我们就可以开始进行具体的Socket通信函数调用了,然后让系统内部的重叠结构来替我们管理I/O请求,我们只用等待网络通信完成后调用咱们的回调函数就OK了。

这个步骤的重点就是 绑定一个Overlapped变量和一个完成例程函数

view plaincopy to clipboardprint?

  1. // 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求   
  2. // 并提供下面的作为完成例程的_CompletionRoutine回调函数(函数名字)   
  3. if(WSARecv(   
  4.     AcceptSocket,   
  5.     &DataBuf,   
  6.     1,   
  7.     &dwRecvBytes,   
  8.     &Flags,   
  9.     &AcceptOverlapped,   
  10.     _CompletionRoutine) == SOCKET_ERROR)  // 注意我们传入的回调函数指针   
  11.     {   
  12.         if(WSAGetLastError() != WSA_IO_PENDING)   
  13.         {   
  14.             ReleaseSocket(nSockIndex);   
  15.             continue;   
  16.             }   
  17.         }   
  18. }  

// 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求// 并提供下面的作为完成例程的_CompletionRoutine回调函数(函数名字)if(WSARecv( AcceptSocket, &DataBuf, 1, &dwRecvBytes, &Flags, &AcceptOverlapped, _CompletionRoutine) == SOCKET_ERROR) // 注意我们传入的回调函数指针 { if(WSAGetLastError() != WSA_IO_PENDING) { ReleaseSocket(nSockIndex); continue; } }}

  

【第五步】 调用WSAWaitForMultipleEvents函数或者SleepEx函数等待重叠操作返回的结果

  我们在前面提到过,投递完WSARecv操作,并绑定了Overlapped结构和完成例程函数之后,我们基本就是完事大吉了,等了系统自己去完成网络通信,并在接收到数据的时候,会自动调用我们的完成例程函数。

  而我们在主线程中需要做的事情只有:做别的事情,并且等待系统完成了完成例程调用后的返回结果。

就是说在WSARecv调用发起完毕之后,我们不得不在后面再紧跟上一些等待完成结果的代码。有两种办法可以实现:

1)    和上一篇重叠I/O中讲到的一样,我们可以使用WSAWaitForMultipleEvent来等待重叠操作的事件通知, 方法如下:

 

view plaincopy to clipboardprint?

  1. // 因为WSAWaitForMultipleEvents() API要求   
  2. // 在一个或多个事件对象上等待, 但是这个事件数组已经不是和SOCKET相关联的了   
  3. // 因此不得不创建一个伪事件对象.    
  4. WSAEVENT EventArray[1];        
  5. EventArray[0] = WSACreateEvent();                        // 建立一个事件   
  6.            
  7. // 然后就等待重叠请求完成就可以了,注意保存返回值,这个很重要   
  8. DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);  

// 因为WSAWaitForMultipleEvents() API要求// 在一个或多个事件对象上等待, 但是这个事件数组已经不是和SOCKET相关联的了// 因此不得不创建一个伪事件对象. WSAEVENT EventArray[1]; EventArray[0] = WSACreateEvent(); // 建立一个事件 // 然后就等待重叠请求完成就可以了,注意保存返回值,这个很重要DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);

这里参数的含义我就不细说了,MSDN上一看就明白,调用这个函数以后,线程就会置于一个警觉的等待状态,注意 fAlertable 参数一定要设置为 TRUE。

2)    可以直接使用SleepEx函数来完成等待,效果都是一样的。

SleepEx函数调用起来就简单得多,它的函数原型定义是这样的

    

view plaincopy to clipboardprint?

  1. DWORD SleepEx(   
  2.              DWORD dwMilliseconds,  // 等待的超时时间,如果设置为INFINITE就会一直等待下去   
  3.              BOOL   bAlertable   // 是否置于警觉状态,如果为FALSE,则一定要等待超时时间完毕之后才会返回,这里我们是希望重叠操作一完成就能返回,所以同(1)一样,我们一定要设置为TRUE   
  4.  );  

DWORD SleepEx( DWORD dwMilliseconds, // 等待的超时时间,如果设置为INFINITE就会一直等待下去 BOOL bAlertable // 是否置于警觉状态,如果为FALSE,则一定要等待超时时间完毕之后才会返回,这里我们是希望重叠操作一完成就能返回,所以同(1)一样,我们一定要设置为TRUE );

    调用这个函数的时候,同样注意用一个DWORD类型变量来保存它的返回值,后面会派上用场。

【第六步】通过等待函数的返回值取得重叠操作的完成结果

这是我们最关心的事情,费了那么大劲投递的这个重叠操作究竟是个什么结果呢?就是通过上一步中我们调用的等待函数的DWORD类型的返回值,正常情况下,在操作完成之后,应该是返回WAIT_IO_COMPLETION,如果返回的是 WAIT_TIMEOUT,则表示等待设置的超时时间到了,但是重叠操作依旧没有完成,应该通过循环再继续等待。如果是其他返回值,那就坏事了,说明网络通信出现了其他异常,程序就可以报错退出了……

判断返回值的代码大致如下:

view plaincopy to clipboardprint?

  1. ///   
  2. // 返回WAIT_IO_COMPLETION表示一个重叠请求完成例程代码的结束。继续为更多的完成例程服务   
  3. if(dwIndex == WAIT_IO_COMPLETION)   
  4. {   
  5. TRACE("重叠操作完成...\n");   
  6. }   
  7. else if( dwIndex==WAIT_TIMEOUT )   
  8. {   
  9.      TRACE(“超时了,继续调用等待函数”);   
  10. }   
  11. else  
  12. {   
  13.     TRACE(“废了…”);   
  14. }  

/ 返回WAIT_IO_COMPLETION表示一个重叠请求完成例程代码的结束。继续为更多的完成例程服务if(dwIndex == WAIT_IO_COMPLETION){TRACE("重叠操作完成...\n");}else if( dwIndex==WAIT_TIMEOUT ){ TRACE(“超时了,继续调用等待函数”);}else{ TRACE(“废了…”);}

 

操作完成了之后,就说明我们上一个操作已经成功了,成功了之后做什么?当然是继续投递下一个重叠操作了啊…..继续上面的循环。

 

【第七步】继续回到第四步,在套接字上继续投递WSARecv请求,重复步骤4-7

 大家可以参考我的代码,在这里就先不写了,因为各位都一定比我smart,领悟了关键所在以后,稍作思考就可以灵活变通了。

 

【第八步】“享受”接收到的数据

朋友们看到这里一定会问,我忙活了这么久,那客户端传来的数据在哪里接收啊?怎么一点都没有提到呢……

这个问题问得好,我们写了这么多代码图个什么呢?

其实想要读取客户端的数据很简单,因为我们在WSARecv调用的时候,是传递了一个WSABUF的变量的,用于保存网络数据,而在我们写的完成例程回调函数里面,就可以取到客户端传送来的网络数据了。

因为系统在调用我们完成例程函数的时候,其实网络操作已经完成了,WSABUF里面已经有我们需要的数据了,只是通过完成例程来进行后期的处理。具体可以参考示例代码。 而DataBuf.buf就是一个char*字符串指针,听凭你的处理吧,我就不多说了。

 

一.         实际应用中应该完善的地方

其实我一直都很想把我以前做的工程中的代码贴出来给大家分享,但是代码实在是太繁杂了,仅仅把网络通信的部分剥离出来,不经过测试的话,肯定还会有其他的很多问题,反而误导了初学者,不过我的计划是在写下一个“完成端口”部分的时候,直接把项目中的一部分代码拿出来试试看吧……

总之网络服务器端程序,在实际应用的时候,关键的几点就是:

1)    要考虑到客户端很多、通信量很大的时候,如何去处理,如何尽可能的减小开销,提高效率;

2)    多个线程之间共用一些变量的时候,一定要注意到同步问题;

3)    作为一个网络程序,出现异常是家常便饭,一定要把代码写得尽可能的健壮,要尽量全面的考虑处理各种各样的错误;

4)    尽量不要出现各种字符缓冲区的问题,写安全的代码,防止被黑客利用……(这点似乎扯远了,但是确实是一个很现实的问题)。

     其他的问题,还希望各位这方面的网络专家使劲批评指正,因为代码是很多年前的了,一定存在着很多的问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值