分享

网络重叠IO

 embedsoft 2013-04-14

  

记得写这个系列的上一篇文章的时候已经是四年前了,准确的说是四年半以前了,翻开我尘封已久的IO模型里面的“完成例程”的实现方式及示例代码。

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

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

http://piggyxp.download.csdn.net/

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

 

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

 

目录:

1.完成例程的优点

2.完成例程的基本原理

3.关于完成例程的函数介绍

4.完成例程的实现步骤

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

 

 

一.        完成例程的优点

1.   首先需要指明的是,这里的“完成例程”(Completion Routine)并非是大家所常听到的“完成端口”(Completion Port),而是另外一种管理重叠I/O请求的方式,而至于什么是重叠I/O,简单来讲就是Windows系统内部管理I/O的一种方式,核心就是调用的ReadFileWriteFile函数,在制定设备上执行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()等等网络函数调用。

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

 

 完成例程流程图

 

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

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

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

 

一.        完成例程的函数介绍

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

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

,这里就不再重复了:

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

 

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

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

 

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

      

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

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

其他参数我们可以先不用先细看,只看最后一个,看到了吗?直接在

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

MSDN这么个好帮手,而且在讲后面的完成例程和完成端口的时候我还会讲到一些

四。     实现重叠模型的步骤

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

 

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

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

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

 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用来通信,但是我并没有加到这份代码里面来,因为本来重叠模型的代码就比较杂,再加上这些东西恐怕反而会给初学者带来困难。但是非常欢迎各位和我讨论重叠模型的改进算法以及我代码中存在问题!^_^

 

就说这么多吧,但愿你能通过这篇文章熟练的玩转重叠IO模型,就没有枉费我这番功夫了。^_^

敬请期待本系列下一篇拙作《手把手教你玩转SOCKET模型之完成例程篇》

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多