WINSOCK.03.事件选择模型

Posted oldmao_2001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WINSOCK.03.事件选择模型相关的知识,希望对你有一定的参考价值。


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于TCP/IP的网络编程有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型

这次先讲第二种。

基础知识

windows处理用户行为有两种方式:

消息机制

其核心是消息队列,就是将要处理的操作放到队列(FIFO)中进行处理。
其特点是消息队列由操作系统维护,处理过程遵循队列特点,处理过程中,操作可以同时进行入队。
基于这个消息机制的异步选择模型下一篇讲。

事件机制

其核心是事件集合,同上面一样也是操作,但是这里没有先后顺序,是一个集合,处理的顺序由程序员决定。
根据需求,我们为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少创建多少。
当有对应的操作发生,例如单击鼠标左键,那么事件就会出发信号,程序员可以获取到这个信号,然后对信号进行处理。
其特点是所有事件都是自定义的,系统只管检测是否有信号。由于事件集合的无序性,当事件定义过多,会挤兑一些事件的执行效率(有人插队,轮不到)。
基于事件机制,本节来学习事件选择模型。

事件选择模型步骤

第一步:使用WSACreateEvent创建一个事件对象(变量)
第二步:使用WSAEventSelect为每一个事件对象绑定个SOCKET句柄,以及操作accept.read.close等,并投递给系统(两个事情:绑定,投递)
第三步:使用WSAWaitForMultipleEvents查看事件是否有信号
第四步:有信号的话就使用WSAEnumNetworkEvents分类处理

事件选择模型相关函数

同样的,这个模型的前面步骤和基本模型是一样的

  1. 打开网络库

  2. 校验版本

  3. 创建SOCKET

  4. 绑定地址与端口

  5. 开始监听

到这里都一样

创建、销毁、重置事件对象

WSAEVENT WSAAPI WSACreateEvent();

成功返回事件对象句柄
不成功则返回WSA_INVALID_EVENT
这里补充一下内核对象知识
之前在写SOCKET的时候我通常都写SOCKET句柄,这个玩意可以理解为一个指针,但是这个指针不是我们自己用的指针,是系统用的指针,因此它由系统在内核申请,由系统来访问,用户不能定位或修改它的内容(这里涉及到黑客攻击方面的知识,不展开,其类型是void *,类型是运行时要转换的),从而保护内核,防止恶意的访问、篡改。句柄需要专门的函数来创建和释放,类似程序自己申请的内存需要malloc和free,所以这里的SOCKET和WSAEVENT都是句柄,都是Windows内核对象

销毁事件对象句柄:

BOOL WSAAPI WSACloseEvent(
  WSAEVENT hEvent
);

重置事件对象句柄,将本来产生信号的事件重置为无信号状态:

BOOL WSAAPI WSAResetEvent(
  WSAEVENT hEvent
);

同样的有:

BOOL WSAAPI WSASetEvent(
  WSAEVENT hEvent
);

这个是将本来无信号的事件重置为有信号状态(但不能指定具体信号状态)。

绑定,投递事件对象(重点)

int WSAAPI WSAEventSelect(
  SOCKET   s,
  WSAEVENT hEventObject,
  long     lNetworkEvents
);

给事件绑上SOCKET句柄与操作码,并投递给操作系统。
参数1:要绑定的SOCKET句柄
参数2:事件对象
参数3:具体事件,根据MSDN,常见的事件有:

操作码(信号)发生原因绑定操作
FD_READ有客户端消息绑定客户端SOCKET句柄
FD_WRITE可以可客户端发送消息绑定客户端SOCKET句柄,FD_ACCEPT成功后会自动产生这个信号
FD_OOB有带外数据一般不使用
FD_ACCEPT有客户端连接请求绑定客户端SOCKET句柄
FD_CONNECT在客户端编写,绑定服务器端SOCKET句柄
FD_CLOSE客户端下线(正常、强制均可)绑定客户端SOCKET句柄
FD_QOS套接字服务质量状态发生变化网络发生拥堵时发生该事件,获取服务质量状态可用WSAloctl
FD_GROUP_QOS保留操作码
FD_ROUTING_ INTERFACE_CHANGE路由接口改变(动态路由?)重叠I/O模型专用,要先WSAloctl注册才能生效
FD_ADDRESS_ LIST_CHANGE地址列表改变同上
0取消操作码绑定

当多个事件码同时绑定可以用【|】来连接多个事件码。
返回值:
成功:0
失败:SOCKET_ERROR

等待(查询)事件

DWORD WSAAPI WSAWaitForMultipleEvents(
  DWORD          cEvents,
  const WSAEVENT *lphEvents,
  BOOL           fWaitAll,
  DWORD          dwTimeout,
  BOOL           fAlertable
);

参数1:通过转定义可以看到:

typedef unsigned long       DWORD;

DWORD是无符号长整型,代表当前绑定事件数量(最大是64,这里指已经绑定的数量,不是最大数量)
参数2:多个事件对象数组的指针入口;
参数3:TRUE代表要等多个事件对象都产生信号后才返回,然后将事件对象数组按数组索引依次进行处理,这种方式不常用,会产生由于等待造成较大的延时;
FALSE代表只要多个事件对象中有一个产生信号后才返回,返回后用返回值减去宏WSA_WAIT_EVENT_0得到事件对象数组中有信号的事件对象的数组索引(下标),由于事件数组和SOCKET数组下标是一一对应关系(下面有讲),这个时候也获得了SOCKET数组下标。
需要注意的是,如果同时有多个事件对象产生信号,那么这个时候经过宏运算后得到是事件数组中下标最小的那个。
参数4:等待时长,当查询完毕后,系统等待的时间长度,单位是毫秒。如果在等待过程中有事件信号产生则立刻返回。当超过设置的等待时长则返回WSA_WAIT_TIMEOUT,此时应该继续循环(continue;),相当于每次查询后会停顿一下,再根据if对WSA_WAIT_TIMEOUT的判断进行相应的处理;
当等待时长设置为0时,表示程序查询完时间状态后不等待,直接返回,进行下一轮查询;
当等待时长设置为WSA_INFINITE时,表示查询查询完会一直等待,知道有事件信号产生才返回,反正没信号也没事干,等着也行。
参数5:TRUE,在重叠I/O模型中使用;
FALSE,在事件选择模型中使用。

返回值:
有信号的数组下标:这里分两种情况:参数3如果是TRUE,那么是整个数组,如果参数3是FALSE那么只返回一个值;
当参数5为TRUE的时候,返回值为:WSA_WAIT_IO_COMPLETION;
当参数4设置了等待时长,超过这个设置的时长没有信号就会返回WSA_WAIT_TIMEOUT。

在编写等待(或者说查询)事件的代码之前,我们回想一下事件选择模型的流程,实际上是在select模型上进行了改进,因此我们可以借鉴select模型中FD_SET结构体的思想,为事件选择模型定义定义一个FD_SOCKEVENT_SET(SOCKET EVENT)结构体,该结构体包含SOCKET和EVENT两个数组,两个数组中下标相同表示SOCKET和EVENT是一一对应的关系。
这个结构体中最多处理64个SOCKET和EVENT,想突破这个限制可以多定义几个这个结构体。

struct fd_sockevent_set
{
	unsigned short count; 
	SOCKET sockall[WSA_MAXIMUM_WAIT_EVENTS]; //socket句柄数组
	WSAEVENT evnetall[WSA_MAXIMUM_WAIT_EVENTS]; //evnet句柄数组
};

然后在创建事件前面添加代码,定义socket+event结构体

fd_sockevent_set sockevent_set = {0,{0},{0}};

然后在绑定、投递事件对象代码后面加上:

//将事件和SOCKET放到sockevent_set里面
	sockevent_set.evnetall[sockevent_set.count]=eventServer;
	sockevent_set.sockall[sockevent_set.count]=socketServer; 
	sockevent_set.count++;

然后要使用循环来不断的查询事件对象状态是否有signal。

//循环查询事件状态是否有信号
	while(1)
	{
		DWORD retSignal =  WSAWaitForMultipleEvents(sockevent_set.count,sockevent_set.evnetall,false,WSA_INFINITE,false);
		if (retSignal == WSA_WAIT_FAILED)
		{
			//报错处理
			int retSignalerr = WSAGetLastError();
			printf("循环查询事件状态错误码为:d%",retSignalerr);
			WSACloseEvent(eventServer);

			closesocket(socketServer);
			WSACleanup();
			return 0;

		}

		//如果WSAWaitForMultipleEvents参数5指定了等待时长,则要对超时进行判断,WSA_INFINITE可以省略不写
		if(retSignal == WSA_WAIT_TIMEOUT)
		{
			continue;
		}
		DWORD soindex = retSignal - WSA_WAIT_EVENT_0;
	}

列举事件

上面查询到了有信号的事件以及对应的SOCKET,这里要对结果进行处理,该函数有两个功能:
1、获取事件的类型
2、重置事件的信号

int WSAAPI WSAEnumNetworkEvents(
  SOCKET             s,
  WSAEVENT           hEventObject,
  LPWSANETWORKEVENTS lpNetworkEvents
);

参数1:SOCKET句柄
参数2:事件句柄
参数3:通过这个结构体指针(lp开头)将事件类型返回回来,定义代码如下:

typedef struct _WSANETWORKEVENTS {
  long lNetworkEvents;
  int  iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

这里要理解这个事情,当我们绑定事件的时候,上面说过,可以绑定多个操作码,用竖线隔开即可,也就是意味着一个事件对应多个操作码,那么参数1:lNetworkEvents也就包含多个操作码,但是会按位排列。参数2:iErrorCode是一个错误码数组。当有多个操作码则要按操作码FD_XXX_BIT对应具体数组的下标,对应关系:

/*
 * WinSock 2 extension -- bit values and indices for FD_XXX network events
 */
#define FD_READ_BIT      0
#define FD_READ          (1 << FD_READ_BIT)

#define FD_WRITE_BIT     1
#define FD_WRITE         (1 << FD_WRITE_BIT)

#define FD_OOB_BIT       2
#define FD_OOB           (1 << FD_OOB_BIT)

#define FD_ACCEPT_BIT    3
#define FD_ACCEPT        (1 << FD_ACCEPT_BIT)

#define FD_CONNECT_BIT   4
#define FD_CONNECT       (1 << FD_CONNECT_BIT)

#define FD_CLOSE_BIT     5
#define FD_CLOSE         (1 << FD_CLOSE_BIT)

#define FD_QOS_BIT       6
#define FD_QOS           (1 << FD_QOS_BIT)

#define FD_GROUP_QOS_BIT 7
#define FD_GROUP_QOS     (1 << FD_GROUP_QOS_BIT)

如果某个操作码没有错误,那么它对应的数组下标里面的存储数值为0,例如FD_READ没有问题,那么在数组中第0位是0;FD_ACCEPT没有问题,那么在数组中第3位是0。

具体代码如下:

		//得到事件绑定的操作
		WSANETWORKEVENTS NetworkEvents; 
		if(SOCKET_ERROR==WSAEnumNetworkEvents(sockevent_set.sockall[soindex], sockevent_set.evnetall[soindex],&NetworkEvents))
		{
			int NetworkEventserr = WSAGetLastError();
			printf("得到事件绑定的操作错误码为:d%",NetworkEventserr);
		}

处理accept

对于accept是几个处理里面最最复杂的,思路是这样:
用WSANETWORKEVENTS来按位与操作,如果是accept操作,如果是
判断是否有错误码,如果没有
那么连接并创建出客户端SOCKET
如果创建成功,则创建客户端事件对象
如果创建成功,绑定并投递客户端事件对象,绑定这里对于accept来说,有三种客户端事件对象:
FD_READ|FD_WRITE|FD_CLOSE
如果绑定成功,则将客户端事件和SOCKET放到sockevent_set里面,否则报错

//按位与判断是否是FD_ACCEPT操作码
		if(NetworkEvents.lNetworkEvents & FD_ACCEPT)
		{
			//判断FD_ACCEPT错误码对应位是否有值
			if(NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
			{
				//正常处理,创建客户端
				SOCKET socketClient=accept(sockevent_set.sockall[soindex], NULL, NULL); 
				//创建失败则跳过
				if(socketClient==INVALID_SOCKET)
				{
					continue;
				}
				//创建成功则为该SOCKET创建事件对象
				WSAEVENT wsaClientEvent =WSACreateEvent(); 
				//失败则关闭SOCKET句柄
				if(wsaClientEvent == WSA_INVALID_EVENT)
				{
					closesocket(socketClient);
					continue;
				}

				//绑定,投递客户端事件对象
				//客户端事件对象通常有三种
				if (WSAEventSelect(socketClient,wsaClientEvent,FD_READ|FD_WRITE|FD_CLOSE)==SOCKET_ERROR)
				{
					//出错关闭句柄,关闭事件对象
					closesocket(socketClient);
					WSACloseEvent(wsaClientEvent);
					//获取错误码略
					continue;
				}

				//绑定投递成功后将客户端事件和SOCKET放到sockevent_set里面
				sockevent_set.evnetall[sockevent_set.count]=wsaClientEvent;
				sockevent_set.sockall[sockevent_set.count]=socketClient; 
				sockevent_set.count++;

			}
			else
			{
				//出现异常不影响其他处理
				continue;
			}

		
		}

处理FD_READ|FD_WRITE|FD_CLOSE

用WSANETWORKEVENTS来按位与操作,分别对三种操作码进行判断,然后进行处理

FD_WRITE

这里需要注意的是,当一个客户端连接到服务器,就会触发FD_ACCEPT,然后立即触发一次FD_WRITE。因此FD_WRITE与普通SELECT模型不一样,SELECT模型中send是连接成功后随时都可以调用的,这里的FD_WRITE只会产生一次,因此通常用来做客户端连接成功后的初始化操作。

	//按位与判断是否是FD_WRITE操作码
		if(NetworkEvents.lNetworkEvents & FD_WRITE)
		{
			//判断错误码对应位是否有值,没有说明SOCKET没有错误
			if(NetworkEvents.iErrorCode[FD_WRITE_BIT] == 0)
			{
				if(send(sockevent_set.sockall[soindex],"连接成功~",sizeof("连接成功~"),0)==SOCKET_ERROR)
				{
					int FD_WRITEsenderr = WSAGetLastError();
					printf("得到FD_WRITE操作send函数执行的错误码为:d%\\n",FD_WRITEsenderr);
					continue;
				}
			}
			else
			{
				printf("得到FD_WRITE操作的错误码为:d%\\n",NetworkEvents.iErrorCode[FD_WRITE_BIT]);
				continue;
			}
		}

FD_WRITE

主要是读信息

//按位与判断是否是FD_READ操作码
		if(NetworkEvents.lNetworkEvents & FD_READ)
		{
			//判断错误码对应位是否有值,没有说明SOCKET没有错误
			if(NetworkEvents.iErrorCode[FD_READ_BIT] == 0)
			{
				char strRecv[1500] = {0};
				if(recv(sockevent_set.sockall[soindex],strRecv,sizeof(strRecv),0)==SOCKET_ERROR)
				{
					int FD_READrecverr = WSAGetLastError();
					printf("得到FD_READ操作recv函数执行的错误码为:d%\\n",FD_READrecverr);
					continue;
				}
				//打印接收的信息
				printf("接收的消息为:s%\\n",strRecv);
			}
			else
			{
				printf("得到FD_READ操作的错误码为:d%\\n",NetworkEvents.iErrorCode[FD_READ_BIT]);
				continue;
			}
		}

FD_CLOSE

主要处理关闭时候要清理的对象

这里用了一个小trick,就是删除一个数组中的一个元素,不用逐个将后面的元素逐个往前移动,直接将数组最后一个元素(位置是sockevent_set.count-1)填补到当前删除元素的位置即可。
另外还要注意,删除操作和close操作不可调换位置。
由于FD_CLOSE错误码都是不为0的,因此不用对等于0的情况做判断,直接打印即可。
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaenumnetworkevents

//按位与判断是否是FD_CLOSE操作码
		if(NetworkEvents.lNetworkEvents & FD_CLOSE)
		{
			printf("FD_CLOSE操作\\n");
			printf("得到FD_CLOSE操作的错误码为:d%\\n",NetworkEvents.iErrorCode[FD_CLOSE_BIT]);

			//清理下线的客户端套接字
			closesocket(sockevent_set.sockall[soindex]);
			sockevent_set.sockall[soindex] = sockevent_set.sockall[sockevent_set.count-1];
			//清理下线的客户端事件
			WSACloseEvent(sockevent_set.evnetall[soindex]);
			sockevent_set.evnetall[soindex] = sockevent_set.evnetall[sockevent_set.count-1];

			sockevent_set.count--;

		}

处理事件的注意事项

这里每个操作码的判断不能使用swich或者if elseif结构,其原因是一个信号里面可能包含多个操作码,例如:【11】,其实是【10】和【01】两个操作码的组合,如果用if elseif结构,就会只执行一个条件,漏掉一个;如果用swich结构,此时就会一个都不执行,因为switch只能判断常量,不接表达式,不带按位与操作。

#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")

struct fd_sockevent_set
{
	unsigned short count; 
	SOCKET sockall[WSA_MAXIMUM_WAIT_EVENTS]; //socket句柄数组
	WSAEVENT evnetall[WSA_MAXIMUM_WAIT_EVENTS]; //evnet句柄数组
};

BOOL WINAPI cls(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT :
		
		WSACleanup();

	}

	return TRUE;
}


int main(void)
{
	SetConsoleCtrlHandler(cls,TRUE);

	/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
	WORD wdVersion=MAKEWORD(2,2);
	int a=*((char*)&wdVersion); 
	int b=*((char*)&wdVersion+1);

	WSADATA wdScokMsg;
	int nRes = WSAStartup(wdVersion,&wdScokMsg);


	if (0 != nRes)
	{
		switch(nRes)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。\\n");
				break; 
			case WSAVERNOTSUPPORTED: 
				break; 
			case WSAEINPROGRESS: 
				break; 
			case WSAEPROCLIM: 
				break; 
			case WSAEFAULT:
				break;
		}
		return 0;
	
	}

	//校验版本	
	if (2!=HIBYTE(wdScokMsg.wVersion)|| 2!=LOBYTE(wdScokMsg.wVersion))
	{
			printf("版本有问题!\\n");
			WSACleanup();
			return 0;
	}

	SOCKET socketServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);


	if(INVALID_SOCKET == socketServer)
	{
		int err=WSAGetLastError();
		
		
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(12345);//用htons宏将整型转为端口号的无符号整型

	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
	
	if(SOCKET_ERROR==bind(socketServer,(const struct sockaddr *)&si,sizeof(si)))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	printf("服务器端bind成功!\\n");

	if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器监听失败错误码为:%d\\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	
	printf("服务器端监听成功!\\n");

	fd_sockevent_set sockevent_set = {0,{0},{0}};


	//创建事件对象
	WSAEVENT eventServer = WSACreateEvent();

	if(eventServer == WSA_INVALID_EVENT)
	{
		int createerr = WSAGetLastError();
		closesocket(socketServer);
		WSACleanup();

		return 0;

	}

	//绑定,投递事件对象
	if (WSAEventSelect(socketServer,eventServer,FD_ACCEPT)==SOCKET_ERROR)
	{
		int selecterr = WSAGetLastError();
		WSACloseEvent(eventServer);

		closesocket(socketServer);
		WSACleanup();
		return 0;
	}
       
	//将事件和SOCKET放到sockevent_set里面
	sockevent_set.evnetall[sockevent_set.count]=eventServer;
	sockevent_set.sockall[sockevent_set.count]=socketServer; 
	sockevent_set.count++;

	//循环查询事件状态是否有信号
	while(1)
	{
		DWORD retSignal =  WSAWaitForMultipleEvents(sockevent_set.count,sockevent_set.evnetall,关于片段生命周期

python常用代码

在同一个片段中实现多个事件监听器 - Android

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

三.Windows I/O模型之事件选择(WSAEventSelect )模型

windows下的IO模型之事件选择(WSAEventSelect)模型