Socket网络编程学习之路 第五篇 ----winsock网络IO模型(2)之 重叠IO模型

来源:互联网 发布:郑州java程序员工资 编辑:程序博客网 时间:2021/11/29 08:10
重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。

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

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

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

一.事件对象通知

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

1.重叠IO模型的基础知识

(1)WSAOVERLAPPED结构

这个结构就是重叠模型里的核心了。

typedef struct _WSAOVERLAPPED

{

   DWORD internal ;

   DWORD internalHigh ;

   DWORD offset ;

   DWORD offsetHigh ;

   WSAEVENT hEvent ;

} WSAOVERLAPPED , *LPWSAOVERLAPPED ;

我们需要把WSARecv投递到一个WSAOVERLAPPED结构上,我们也需要一个与这个结构绑定在一起的事件爱你来通知我们IO操作已经完成,下面介绍如何绑定事件:

WSAEVENT   event ;   // 定义事件

WSAOVERLAPPED AcceptOverlapped ;   // 定义重叠结构

event = WSACreateEvent() ;  // 建立一个事件对象句柄

ZeroMemory(&AcceptOverlapped , sizeof(WSAOVERLAPPED ) ) ;

AcceptOverlapped.hEvent = event ;


(2)WSARecv函数

在重叠模型中,这个函数是用来接收数据的函数,比recv函数多了一个重叠结构的参数外还有几个其他的参数,这个参数就是用来实现异步的关键

int WSARecv(

     SOCKET s ,

     LPWSABUF  lpBuffers ,   // 接收缓冲区,与recv函数不同,这里是一个WSABUF类型数组的首地址

    DWORD         dwBufferCount ,   // 接收缓冲区中数组的大小,即缓冲区的个数

    LPDWORD   lpNumOfBytesRecvd , // 返回函数调用所接收到的字节数

    LPDWORD   lpFlags ,

    LPWSAOVERLAPPED lpOverlapped ,  // 绑定的重叠结构

    LPWSAOVERLAPPED_Competition_Routine  lpCompetitionRoutine // 完成例程中的参数

);

WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了,但是I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成 。

例子:

SOCKET s ;

WSABuf DataBuf ;

#define DATA_BUFFERSIZE  4096

char buf[DATA_BUFFERSIZE] ;

ZeroMemory( buf , DATA_BUFFERSIZE ) ;

DataBuf.len= DATA_BUFFERSIZE ;

DataBuf.buffer = buf ;

DWORD dwBufferCount = 1 , dwRecvCount = 0 , Flags = 0 ;

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

WSAEVENT event ;   // 如果处理多个操作,这里需要一个WSAEVENT的数组

event = WSACreateEvent() ;  // 建立一个事件对象句柄

ZeroMemory(&AcceptOverlapped , sizeof(WSAOVERLAPPED ) ) ;

AcceptOverlapped.hEvent = event ;  // 把事件句柄绑定到重叠结构上

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


(3)WaitForMultipleEvents

int WaitForMutipleEvents(

DWORD dwCount ,  // 等待事件的个数

const WSAEVENT * lpEvent , // 事件数组的指针

BOOL bWait ,//如果设置为 TRUE,则事件数组中所有事件被传信的时候都会返回,FALSE则任何一个事件被传信函数都要返回

DWORD dwTimeOut  // 超时事件,如果超时会返回WSA_WAIT_TIMEOUT                                          DWORD dwAlertable // 这个在完成例程中会用到                                                                                ) ;                                                                                                    返回值:                                                                                               WSA_WAIT_TIMEOUT:在这个时候我们需要继续等待                                                          WSA_WAIT_FAILED :出现错误                                                                             
WSAWaitForMultipleEvents函数只能支持由WSA_MAXIMUM_WAIT_EVENTS对象定义的一个最大值,是 64,就是说WSAWaitForMultipleEvents只能等待64个事件,如果想同时等待多于64个事件,就要 创建额外的工作者线程,就不得不去管理一个线程池,这一点就不如下一篇要讲到的完成例程模型了。


(4)GetOverlappedResult

既然我们使用函数WaitForMultipleEvents函数来等待IO的结果,那么我们也需要一个函数来检查等待的结果。

BOOL GetOverlappedResult(

SOCKET s ,

LPWSAOVERLAPPED lpOverlapped ,要查询那个重叠操作的结构的指针

LPWORD lpTransfer ,  // 实际接收到的字节数

BOOLdwWait

LPDOWD lpdwFlags   // 接收结果的标志

) ;


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


二.基于完成例程通知的重叠IO模型

基于事件通知的重叠IO模型,在使用WSARecv投递一个请求后,系统在完成后是通过事件来通知的,而完成例程是系统在完成以后是自己调用回调函函数,这就是唯一的区别。


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

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

完成例程中的回调函数的原型:

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

);

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

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

举个例子:
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);


关于例子,后续博文会给出相关的Demo。






















0 0