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

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_0
			currentIndex=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 int
	SOCKET 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连接的Socket
		sockConn=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模型 的有关资料


阅读更多
个人分类: C++
上一篇socket通信之五:select多路复用的客户/服务器模型
下一篇socket通信之七:Overlapped I/O 完成例程模型实现的客户/服务器模型
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭