0
点赞
收藏
分享

微信扫一扫

5种Windows网络模型之事件选择模型

眸晓 2022-04-17 阅读 43
网络

1. 事件选择模型的意义

  上一篇博客5种Windows网络模型之select模型介绍了select模型克服了CS模型的几种缺点,但是select模型也存在缺陷,比如当select函数将所有socket投递给操作系统后,系统系统帮我们把有信号的socket装进fd_set结构体,直至返回有事件的socket集合这一过程程序是阻塞等待的,为了克服这个缺点人们提出了事件选择模型。

2. Windows处理用户行为的两种方式

2.1 事件机制

核心:事件集合
处理过程:根据需要,我们为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少就创建多少,将事件投递给系统,让系统帮忙监管。
特点:所有的时间都是用户自己定义的,系统只是帮用户置成有无信号,事件是没有顺序的(我们介绍的事件选择模型就是应用该机制)。

2.2消息机制

核心:消息对列
处理过程:所有的用户操作,比如点击鼠标、键盘等,所有的操作均依次按顺序被记录,装进一个队列。
特点:消息对列由操作系统维护,咱们做的操作被操作系统取出来分类处理(有先后顺序)。

3. 事件选择模型

3.1 事件选择模型的整体逻辑

  1. 创建一个事件对象(WSACreateEvent);
  2. 为每一个事件对象绑定一个socket以及操作(accept、recv等)并投递给操作系统,程序员无需其它操作(注:这里select函数是循环检测是否有socket响应,而事件选择模型使主动件有响应的socket投递给操作系统)。
  3. 查看事件是否有信号(WSAWaitForMultipleEvents);
  4. 有信号的话就分类处理(WSAEnumNetWorkEvents)

3.2 函数详解

  1. 创建一个事件对象
    WSAEVENT eventSever = WSACreateEvent();
    参数:无
    返回值:如果没有错误则返回事件对象的句柄;
        如果出错则返回 WSA_INVALID_EVENT。
  2. 绑定并投递
    函数原型:
int WSAAPI WSAEventSelect(
  [in] SOCKET   s,
  [in] WSAEVENT hEventObject,
  [in] long     lNetworkEvents
);

功能:给事件绑上socket码与事件
参数1:被绑定的socket
参数2:事件对象,就是将参数1与参数2绑定在一起
参数3:具体事件(一般有四种:FD_ACCEPT、FD_READ、FD_WRITE和FD_CLOSE)。
返回值:如果成功返回0,如果失败则返回SOCKET_ERROR。

  1. 询问事件
    函数原型:
DWORD WSAAPI WSAWaitForMultipleEvents(
  [in] DWORD          cEvents,
  [in] const WSAEVENT *lphEvents,
  [in] BOOL           fWaitAll,
  [in] DWORD          dwTimeout,
  [in] BOOL           fAlertable
);

功能:获取发生信号的事件。
参数1:事件个数
参数2:事件列表(我们定义一个结构体,存储所有的socket、事件,socket与事件一一对应,在结构体中定义一个变量存储socket与事件个数,参数1和参数2直接从该结构体取出即可)。
参数3:事情等待的方式,一般填TRUE或FALSE
  TRUE:所有的事件均产生信号才返回
  FALSE: (1)任何一个事件产生信号则立即返回;
         (2)返回值减去WSA_WAIT_EVENT_0表示事件对象的索引,其状态导致函数返回。
参数4:超时间隔,以毫秒为单位,跟select函数参数5含义一样
    n,等待n秒,超时返回WSA_WAIT_TIMEOUT
    0,检查事件对象的状态并立即返回(不管有没有信号)
    WSA_INFINTE,等待直到有事件发生
参数5:重叠IO模型中填TRUE,在事件选择模型中用FALSE即可。
返回值: 1. 我们要获得有信号事件发生的索引,若参数3为TRUE,则所有事件均有信号发生,无需特殊方式获得索引;如果参数3为FALSE,则返回值减去WSA_WAIT_EVENT_0就是数组中事件的下标。
     2. 如果返回值为WSA_WAIT_TIMEOUT,则代表超时了,continue即可。
     3. 如果返回值为WSA_WAIT_FAILED,则代表函数执行失败。

  1. 列举事件
    函数原型:
int WSAAPI WSAEnumNetworkEvents(
  [in]  SOCKET             s,
  [in]  WSAEVENT           hEventObject,
  [out] LPWSANETWORKEVENTS lpNetworkEvents
);

功能:获取事件类型(accept、recv、close等),并将事件上的信号量重置。
参数1:对应的socket;
参数2:对应的事件;
参数3:触发事件的类型在这里装着;它是一个结构体指针,系统已经为我们定义好了:

typedef struct _WSANETWORKEVENTS {
       long lNetworkEvents;
       int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
  • 成员1(lNetworkEvents):具体操作,一个信号可能包含两个操作,以按位或的形式存在。
  • 成员2(iErrorCode):错误码数组,FD_ACCEPT事件错误码存在该数组中,对应下标为FD_ACCEPT_BIT,如果没有对应就是0;

返回值:如果成功返回0;如果失败返回SOCKET_ERROR。

3.3 程序

  说明:本节给出服务端的程序,客户端的程序和上一篇博文中的客户端程序一样,在此不重复给出。运行时仍需要先开启服务端,再开启多个服务器。上一篇中的select模型和本篇中的事件选择模型对于我们普通用户感觉差不多,但是对于系统来说事件选择模型效率确实是提高了。
  具体的程序如下所示:

//服务端
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<WinSock2.h>
#include<stdbool.h>
#pragma comment(lib,"ws2_32.lib")


struct fd_es_set {
	unsigned short count;
	SOCKET sockall[WSA_MAXIMUM_WAIT_EVENTS];//WSA_MAXIMUM_WAIT_EVENTS是64
	WSAEVENT eventall[WSA_MAXIMUM_WAIT_EVENTS];
};
struct fd_es_set esSet;//全局变量不需要初始化,变量自动被置成0

BOOL WINAPI over(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		//释放所有socket
		for (int i = 0; i < esSet.count; i++)
		{
			closesocket(esSet.sockall[i]);
			WSACloseEvent(esSet.eventall[i]);
		}
		break;
	}
	WSACleanup();
	return 0;
}

int main()
{
	SetConsoleCtrlHandler(over, TRUE);//这个函数的作用是当点击运行框右上角叉号关闭时,执行上面的over函数,系统调用函数

	WORD wdVersion = MAKEWORD(2, 2);		//使用网络库的版本
	WSADATA wdSockMsg;						//系统通过调用这个参数给我们一些配置信息
	int nRes = WSAStartup(wdVersion, &wdSockMsg);//打开网络库
	if (0 != nRes)	//如果打开网络库错误
	{
		switch (nRes)
		{
		case WSASYSNOTREADY:
			printf("可以重启电脑,或检查网络库");
			break;
		case WSAVERNOTSUPPORTED:
			printf("请更新网络库");
			break;
		case WSAEINPROGRESS:
			printf("Please reboot this software");
			break;
		case WSAEPROCLIM:
			printf("请关闭不必要的软件,以为当前网络提供充足资源");
			break;
		case WSAEFAULT:
			printf("参数错误");
			break;
		}
		return 0;
	}

	//版本校验
	if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
	{
		//如果版本错误
		WSACleanup();				//清理网络库
		return 0;
	}

	SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);		//创建服务器socket
	if (INVALID_SOCKET == socketSever)
	{
		//socket创建失败
		int a = WSAGetLastError();	//返回错误码
		WSACleanup();				//清理网络库
		return 0;
	}

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	si.sin_port = htons(12345);
	int bind_a = bind(socketSever, (struct sockaddr*)&si, sizeof(si));
	if (SOCKET_ERROR == bind_a)
	{
		//如果bind函数出错
		int a = WSAGetLastError();	//返回错误码
		closesocket(socketSever);	//关闭socket
		WSACleanup();				//清理网络库
		return 0;
	}
	//开始监听
	int listen_r = listen(socketSever, SOMAXCONN);
	if (SOCKET_ERROR == listen_r)
	{
		//如果listen函数出错
		int a = WSAGetLastError();	//返回错误码
		closesocket(socketSever);	//关闭socket
		WSACleanup();				//清理网络库
		return 0;
	}	


	//struct fd_es_set esSet = { 0,{0},{NULL} };

	//创建事件
	WSAEVENT eventSever = WSACreateEvent();
	if (WSA_INVALID_EVENT == eventSever)
	{
		//如果创建事件失败
		int a = WSAGetLastError();	//返回错误码
		closesocket(socketSever);	//关闭socket
		WSACleanup();				//清理网络库
		return 0; 
	}
	//绑定并投递
	int wsa_r = WSAEventSelect(socketSever, eventSever, FD_ACCEPT);
	if (wsa_r == SOCKET_ERROR)
	{
		//如果绑定失败
		int a = WSAGetLastError();	//返回错误码
		//释放事件
		WSACloseEvent(eventSever);
		//释放所有socket
		closesocket(socketSever);
		//清理网络库
		WSACleanup();
		return 0;
	}
	//装进结构体
	esSet.eventall[esSet.count] = eventSever;
	esSet.sockall[esSet.count] = socketSever;
	esSet.count++;
	//printf("write event\n");
	while (1)
	{
		//询问
		for (int nIndex = 0 ; nIndex < esSet.count; nIndex++)
		{//由于事件机制中的事件是无序的,这样可能会造成一些事件在某些情况下等待时间过长,因此在这里添加一个for循环使得事件有顺序
			DWORD nRes = WSAWaitForMultipleEvents(esSet.count, esSet.eventall, FALSE, 100, FALSE);
			if (WSA_WAIT_FAILED == nRes)
			{
				//如果是WSAWaitForMultipleEvents函数出错
				printf("错误码:%d\n", WSAGetLastError());
				break;
			}
			//如果WSAWaitForMultipleEvents参数4是一个具体的数,则需要使用下面的超时处理函数
			if (WSA_WAIT_TIMEOUT == nRes)
			{
				continue;
			}
		
			//DWORD nIndex = nRes - WSA_WAIT_EVENT_0;//返回值减WSA_WAIT_EVENT_0得到数组中事件的下标

			WSANETWORKEVENTS NetworkEvents;

			//得到下标后对应的具体操作
			if (SOCKET_ERROR == WSAEnumNetworkEvents(esSet.sockall[nIndex], esSet.eventall[nIndex], &NetworkEvents))
				//参数1:对应的socket,参数2:对应的事件,参数3:触发事件的类型
			{
				//如果WSAEnumNetworkEvents执行出错
				printf("错误码:%d\n", WSAGetLastError());
				break;
			}

			if (NetworkEvents.lNetworkEvents & FD_ACCEPT)
			{
				if (0 == NetworkEvents.iErrorCode[FD_ACCEPT_BIT])
				{
					//正常处理
					SOCKET socketClient = accept(socketSever, NULL, NULL);//socketSever也可替换为esSet.sockall[nIndex]
					if (INVALID_SOCKET == socketClient)
					{
						continue;
					}
					//创建事件对象
					WSAEVENT wsaClientEvent = WSACreateEvent();
					if (WSA_INVALID_EVENT == wsaClientEvent)
					{
						//事件对象创建失败
						closesocket(socketClient);
						continue;
					}
					//将刚创建的事件与socket绑定后投递给系统
					if (SOCKET_ERROR == WSAEventSelect(socketClient, wsaClientEvent, FD_READ | FD_WRITE | FD_CLOSE))
					{
						//如果绑定失败
						int a = WSAGetLastError();	//返回错误码
						//释放事件
						WSACloseEvent(eventSever);
						//释放所有socket
						closesocket(socketSever);
						continue;
					}
					//将刚accept返回的客户端socket与刚创建的事件装进结构体
					esSet.sockall[esSet.count] = socketClient;
					esSet.eventall[esSet.count] = wsaClientEvent;
					esSet.count++;
					printf("accept event\n");
				}
				else
				{
					continue;
				}
			}
			if (NetworkEvents.lNetworkEvents & FD_WRITE)
			{
				if (0 == NetworkEvents.iErrorCode[FD_WRITE_BIT])
				{
					if (SOCKET_ERROR == send(esSet.sockall[nIndex], "connect success", strlen("connect success"), 0))
					{
						printf("connect fail, error code:%d\n", WSAGetLastError());
						continue;
					}
					printf("write event\n");
				}
				else
				{
					printf("socket error code:%d\n", NetworkEvents.iErrorCode[FD_WRITE_BIT]);
					continue;
				}
			}
			if (NetworkEvents.lNetworkEvents & FD_READ)
			{
				if (0 == NetworkEvents.iErrorCode[FD_READ_BIT])
				{
					char str[1500] = { 0 };
					if (SOCKET_ERROR == recv(esSet.sockall[nIndex], str, 1499, 0))
					{
						printf("recv error code:%d\n", WSAGetLastError());
						continue;
					}
					printf("%s\n", str);
				}
				else
				{
					printf("socket error code:%d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
					continue;
				}
			}
			if (NetworkEvents.lNetworkEvents & FD_CLOSE)
			{
				if (0 == NetworkEvents.iErrorCode[FD_CLOSE_BIT])
				{

				}
				else
				{
					printf("Client offline:%d\n", NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
				}
				//如果某个socket出现错误,则需要将该socket对应的事件连同出错的socket本身一并从结构体中删除,并释放相关的资源
				//套接字
				closesocket(esSet.sockall[nIndex]);
				esSet.sockall[nIndex] = esSet.sockall[esSet.count - 1];
				//事件
				WSACloseEvent(esSet.eventall[nIndex]);
				esSet.eventall[nIndex] = esSet.eventall[esSet.count - 1];
				esSet.count--;
			}
		}
	}
	//释放事件数组和socket数组
	for (int i = 0; i < esSet.count; i++)
	{
		closesocket(esSet.sockall[i]);
		WSACloseEvent(esSet.eventall[i]);
	}
	//清理网络库
	WSACleanup();
	return 0;
}

事件选择模型对于select模型来说明显提高了程序的执行效率,但是程序在执行到recv、send函数在向收发缓冲区读取或写入数据时仍然是阻塞的,要想从根本上克服这个缺点,利用多线程机制是一种行之有效的方法。

举报

相关推荐

0 条评论