socket通信之六:Overlapped I/O 事件通知模型实现的客户/服务器模型

来源:互联网 发布:工程造价软件有什么 编辑:程序博客网 时间:2024/05/17 00:10

1.基于事件通知模型的Overlapped I/O(重叠IO模型)


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


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

  1. 事件对象通知(event object notification)
  2. 完成例程(completion routines) ,注意,这里并不是完成端口


这一篇实现基于事件对象通知的重叠I/O模型。


既然是基于事件通知,就要求将Windows事件对象与WSAOVERLAPPED结构关联在一起(WSAOVERLAPPED结构中专门有对应的参数),发送接收数据的函数参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要德数据了。


socket连接,WSAOVERLAPPED重叠IO结构,事件对象之间的关系如下图:





可以发现每建立一个socket连接时需要至少创建一个WSAOVERLAPPED结构和它关联,而每个WSAOVERLAPPED需要关联一个WSAEVENT类型的事件对象。所以为了实现多个客户端和服务器端通信,我们也需要像上一篇select模型一样建立一个类来管理多个socket和它们对应的WSAOVERLAPPED结构,WSAEVENT结构。


2.基本的函数和数据结构


2.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;    

2.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操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成

2.3.WSAWaitForMultipleEvents函数


    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 :最常见的返回值,我们需要做的就是继续Wait    WSA_WAIT_FAILED : 出现了错误,请检查cEvents和lphEvents两个参数是否有效

如果事件数组中有某一个事件被传信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值 WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。

2.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之类的也就可以关闭了。


3.基于事件通知模型的重叠IO模型


介绍完重叠I/O的一些基本知识后就开始编程了。仍然是在原来的代码框架上进行更改,并且需要修改的也是第6步,就是发送接收数据的哪一步,其它步骤不需要进行更改。


和select模型类似,在主线程中启动一个辅助线程用来处理重叠IO请求,主线程只负责和客户端建立socket连接,并将这个连接放入我们维护的集合类中,我们同样需要维护服务器端和客户端的所有socket连接,同时要需要维护和每一个socket连接相关的重叠IO结构WSAOVERLAPPED,以及事件对象WSAEVENT。这里我们把这个维护这些信息的类定义为SocketListWithIOEvent,这个类同时支持添加一个socket或者删除一个socket连接时更新相关的信息。


重叠IO的基本处理步骤如下:


1.建立并初始化重叠结构:为连入的这个套接字新建立一个WSAOVERLAPPED重叠结构,并且象前面讲到的那样,为这个重叠结构从事件句柄数组里挑出一个空闲的对象句柄“绑定”上去。这里我通过SocketListWithIOEvent类的insertSocket()函数完成。


2.以WSAOVERLAPPED结构为参数,在套接字上投递WSARecv请求:服务器在主循环中建立并初始化重叠IO后就执行这一步。


下面几步都在线程函数中完成:


3.用WSAWaitForMultipleEvents函数等待重叠操作返回的结果:我们前面已经给WSARecv关联的重叠结构赋了一个事件对象句柄,所以我们这里要等待事件对象的触发与之配合,而且需要根据WSAWaitForMultipleEvents函数的返回值来确定究竟事件数组中的哪一个事件被触发了。这里这一步被放置在线程函数workThread中处理。


4.使用WSAResetEvent函数重设当前这个用完的事件对象:事件已经被触发了之后,它对于我们来说已经没有利用价值了,所以要将它重置一下留待下一次使用


5.使用WSAGetOverlappedResult函数取得重叠调用的返回状态:并处理得到的结果。


6.同第3步一样,在套接字上继续投递WSARecv请求,重复步骤 3-5



下面是定义的和重叠IO相关的一些信息的类和用于管理所有socket的类。


#include <stdio.h>#include <stdlib.h>#include <WinSock2.h>#include <iostream>#pragma comment(lib, "ws2_32.lib")using namespace std;#define  PORT 6000//#define  IP_ADDRESS "10.11.163.113"  //表示服务器端的地址#define  IP_ADDRESS "127.0.0.1"  //直接使用本机地址#define MSGSIZE 1024//与重叠IO结构相关的一些信息,把它们封装在一个结构体中方便管理class PerSocketData{public:WSAOVERLAPPED overlap;//每一个socket连接需要关联一个WSAOVERLAPPED对象WSABUF buffer;//与WSAOVERLAPPED对象绑定的缓冲区char          szMessage[MSGSIZE];//初始化buffer的缓冲区DWORD          NumberOfBytesRecvd;//指定接收到的字符的数目DWORD          flags;};//管理所有socket连接的类class SocketListWithIOEvent{public://每建立一个socket连接,需要维护下面三个信息//1.需要保存所有socket连接SOCKET   socketArray[MAXIMUM_WAIT_OBJECTS];//2.需要保存每一个socket连接操作相关联的重叠IO结构的信息,与上面的socketArray相对应PerSocketData * overLappedData[MAXIMUM_WAIT_OBJECTS];//3.需要保存每一个socket连接操作对应的事件对象,与上面的socketArray对应WSAEVENT eventArray[MAXIMUM_WAIT_OBJECTS];//当前管理的socket连接数int totalConn;public://构造函数,初始化这个类,将它里面的成员变量都清零SocketListWithIOEvent(){totalConn=0;for (int i=0;i<MAXIMUM_WAIT_OBJECTS;i++){socketArray[i]=0;eventArray[i]=NULL;overLappedData[i]=NULL;}}//添加一个socket//需要对socketArray,overLappedData,eventArray这三个信息进行更新//返回这个连接的重叠IO结构的信息PerSocketData* insertSocket(SOCKET s){//1.保存socket连接到socketArray中socketArray[totalConn]=s;//2.建立并初始化重叠结构overLappedData[totalConn]=(PerSocketData *)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(PerSocketData));//将结构体清零overLappedData[totalConn]->buffer.len=MSGSIZE;//指定WSABUF的大小overLappedData[totalConn]->buffer.buf=overLappedData[totalConn]->szMessage;// 初始化一个WSABUF结构overLappedData[totalConn]->overlap.hEvent=WSACreateEvent();//为这个socket连接创建一个事件//3.将事件保存到eventArray中eventArray[totalConn]=overLappedData[totalConn]->overlap.hEvent;return overLappedData[totalConn++];//返回当前建立的这个socket相关联的重叠结构的信息,并将连接数加1}//如果socket断开了连接,需要将socket关闭掉,并将它//这个集合中维护的事件信息和重叠IO信息删除掉void deleteSocket(int index){closesocket(socketArray[index]);WSACloseEvent(eventArray[index]);HeapFree(GetProcessHeap(),0,overLappedData[index]);if (index<totalConn-1){//将最后一个连接的相关信息复制到当前要被删除的连接的位置上socketArray[index]=socketArray[totalConn-1];eventArray[index]=eventArray[totalConn-1];overLappedData[index]=overLappedData[totalConn-1];}overLappedData[--totalConn]=NULL;//将最后一个连接置为NULL,并将连接总数减1}};//使用这个工作线程来通过重叠IO的方式与客户端通信DWORD WINAPI workThread(LPVOID lpParam){int ret,currentIndex;DWORD cbTransferred;//SocketListWithIOEvent * sockList=(SocketListWithIOEvent *)lpParam;while(true){// 等候重叠I/O调用结束// 因为我们把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知ret=WSAWaitForMultipleEvents(sockList->totalConn,sockList->eventArray,FALSE,1000,FALSE);if (ret==WSA_WAIT_FAILED||ret==WSA_WAIT_TIMEOUT){continue;}// 注意这里返回的ret并非是事件在数组里的Index,而是需要减去WSA_WAIT_EVENT_0currentIndex=ret-WSA_WAIT_EVENT_0;//事件已经被触发了之后,它对于我们来说已经没有利用价值了,所以要将它重置一下留待下一次使用,很简单,就一步,连返回值都不用考虑WSAResetEvent(sockList->eventArray[currentIndex]);//使用WSAGetOverlappedResult函数取得重叠调用的返回状态WSAGetOverlappedResult(sockList->socketArray[currentIndex],&sockList->overLappedData[currentIndex]->overlap,&cbTransferred,TRUE,&sockList->overLappedData[sockList->totalConn]->flags);//断开连接if (cbTransferred==0){cout<<"客户端断开连接"<<endl;sockList->deleteSocket(currentIndex);}else{cout<<sockList->overLappedData[currentIndex]->szMessage<<endl;send(sockList->socketArray[currentIndex],sockList->overLappedData[currentIndex]->szMessage,cbTransferred,0);WSARecv(sockList->socketArray[currentIndex],&sockList->overLappedData[currentIndex]->buffer,1,&sockList->overLappedData[currentIndex]->NumberOfBytesRecvd,&sockList->overLappedData[currentIndex]->flags,&sockList->overLappedData[currentIndex]->overlap,NULL);}}return 0;}void main(){WSADATA wsaData;int err;//1.加载套接字库err=WSAStartup(MAKEWORD(1,1),&wsaData);if (err!=0){cout<<"Init Windows Socket Failed::"<<GetLastError()<<endl;return ;}//2.创建socket//套接字描述符,SOCKET实际上是unsigned intSOCKET serverSocket;serverSocket=socket(AF_INET,SOCK_STREAM,0);if (serverSocket==INVALID_SOCKET){cout<<"Create Socket Failed::"<<GetLastError()<<endl;return ;}//服务器端的地址和端口号struct sockaddr_in serverAddr,clientAdd;serverAddr.sin_addr.s_addr=inet_addr(IP_ADDRESS);serverAddr.sin_family=AF_INET;serverAddr.sin_port=htons(PORT);//3.绑定Socket,将Socket与某个协议的某个地址绑定err=bind(serverSocket,(struct sockaddr*)&serverAddr,sizeof(serverAddr));if (err!=0){cout<<"Bind Socket Failed::"<<GetLastError()<<endl;return ;}//4.监听,将套接字由默认的主动套接字转换成被动套接字err=listen(serverSocket,10);if (err!=0){cout<<"listen Socket Failed::"<<GetLastError()<<endl;return ;}cout<<"服务器端已启动......"<<endl;int addrLen=sizeof(clientAdd);SOCKET sockConn;SocketListWithIOEvent socketList;HANDLE hThread=CreateThread(NULL,0,workThread,&socketList,0,NULL);if (hThread==NULL){cout<<"Create Thread Failed!"<<endl;}CloseHandle(hThread);while(true){//5.接收请求,当收到请求后,会将客户端的信息存入clientAdd这个结构体中,并返回描述这个TCP连接的SocketsockConn=accept(serverSocket,(struct sockaddr*)&clientAdd,&addrLen);if (sockConn==INVALID_SOCKET){cout<<"Accpet Failed::"<<GetLastError()<<endl;return ;}cout<<"客户端连接:"<<inet_ntoa(clientAdd.sin_addr)<<":"<<clientAdd.sin_port<<endl;//将之前的第6步替换成了上面启动workThread这个线程函数和下面这一行代码//将socket放入socketList中PerSocketData * overLappedData=socketList.insertSocket(sockConn);//WSARecv不是阻塞的WSARecv(sockConn,&overLappedData->buffer,1,&overLappedData->NumberOfBytesRecvd,&overLappedData->flags,&overLappedData->overlap,NULL);}closesocket(serverSocket);//7.清理Windows Socket库WSACleanup();}

客户端的代码和前面select的代码是一样的,这里就不再列出来了。


下面是测试的例子:


可执行文件可以在这里下载,整个工程的文件可以在这里下载。



参考:

手把手教你玩转SOCKET模型之重叠I/O篇(上)

Windows Socket五种I/O模型

手把手教你玩转SOCKET模型之重叠I/O篇(下)

Windows Socket I/O模型 以及 Linux Epoll模型 的有关资料


0 0
原创粉丝点击