TCP.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. 开始监听

到这里都一样

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

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsacreateevent

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
);

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

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

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaeventselect

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

等待(查询)事件

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsawaitformultipleevents

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,接continue即可。
失败:WSA_WAIT_FAILED

在编写等待(或者说查询)事件的代码之前,我们回想一下事件选择模型的流程,实际上是在select模型上进行了改进,因此我们可以借鉴select模型中FD_SET结构体的思想,为事件选择模型定义定义一个FD_SOCKEVENT_SET(SOCKET EVENT)结构体,该结构体包含SOCKET和EVENT两个数组,两个数组中下标相同表示SOCKET和EVENT是一一对应的关系。
这个结构体中最多处理64个SOCKET和EVENT,想突破这个限制有三种方法:
第一种:一次处理一个SOCKET和EVENT,然后循环多次,例如循环100次,那么就可以处理100个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、重置事件的信号
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaenumnetworkevents

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。
返回值:
成功:0
失败:SOCKET_ERROR

具体代码如下:

		//得到事件绑定的操作
		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只会产生一次,且是自动产生,且在客户端被ACCEPT后,产生RECV后产生,因此通常用来做客户端连接成功后的初始化操作。

	//按位与判断是否是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_READ

主要是读信息

//按位与判断是否是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只能判断常量,不接表达式,不带按位与操作。

//1、包含库文件
#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句柄数组
};

fd_sockevent_set sockevent_set;//全局变量不用初始化,会自动设置为0

BOOL WINAPI cls(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT :
		//释放数组中的事件和SOCKET句柄
		for(int i=0;i<sockevent_set.count;i++)
		{		
			WSACloseEvent(sockevent_set.evnetall[i]);
			closesocket(sockevent_set.sockall[i]); 
		}

		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;
	
	}

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

	//4、创建SOCKET句柄
	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");
	
	//5、绑定IP和端口号
	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");

	//6、启用监听
	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}};


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

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

		return 0;

	}

	//7.2绑定,投递事件对象
	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++;

	//7.3循环查询事件状态是否有信号
	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\n",retSignalerr);
			WSACloseEvent(eventServer);

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

		}

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

		//7.3.1获取到有信号的事件下标
		DWORD soindex = retSignal - WSA_WAIT_EVENT_0;

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

		//7.3.3按位与判断是否是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;
			}

		
		}
		
		//7.3.4按位与判断是否是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;
			}
		}

		//7.3.5按位与判断是否是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;
			}
		}

		//7.3.6按位与判断是否是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--;

		}
		
	}

	
	//8、释放数组中的事件和SOCKET句柄
	for(int i=0;i<sockevent_set.count;i++)
	{		
		WSACloseEvent(sockevent_set.evnetall[i]);
		closesocket(sockevent_set.sockall[i]); 
	}
	WSACleanup();


	system("pause");
	return 0;
}

事件选择模型的有序优化

我们开篇就提出,事件选择模型和消息队列模型的最大区别就是事件选择模型是无序的,处理的顺序由程序员决定。从上面的编程过程我们可以看到,每次处理的是有信号的事件中,数组下标最小的那个事件,假设有一个下标为0的事件被触发,然后程序在处理,处理这个事件需要2秒,处理过程中,如果用户再次触发这个事件,那么我们会发现,我们的程序就一直都在处理这个下标为0的事件,后面的事件就没有办法被轮到了,因此这里我们虽然不能左右事件放到数组中的顺序(这个层面的无序要用多线程来解决),但是我们可以使得执行事件的顺序是轮流来的。
之前的代码中while循环中查询的是一组事件,这里修改一次询问一个事件。
因此上面的while循环里面加一个for循环来逐个查询事件是否有信号:

//eventSelectoptimize.cpp
	//soindex这里是socket
		for(DWORD soindex = 0;i <sockevent_set.count; i++)
		{
			//参数1代表查询几个事件,这里每次只查询一个
			//每次按下标轮流查
			//倒数第二个参数0代表不等待,直接返回,因为还有别的事件要查询
			DWORD retSignal =  WSAWaitForMultipleEvents(1,&sockevent_set.evnetall[i],false,0,false);

			if (retSignal == WSA_WAIT_FAILED)
			{
				//报错处理
				int retSignalerr = WSAGetLastError();
				printf("循环查询单个事件状态错误码为:%d\n",retSignalerr);
				continue;

			}

			//当前事件如果没有信号就跳过循环
			if(retSignal == WSA_WAIT_TIMEOUT)
			{
				continue;
			}
		}

后面的判断信号的代码不变。
当然还有一种效率更加高的方法,上面代码里面的for循环包含了没有信号的事件,因此可以先用

DWORD retSignal =  WSAWaitForMultipleEvents(sockevent_set.count,sockevent_set.evnetall,false,WSA_INFINITE,false);

跳过没有信号的事件,从有信号的数组开始循环,具体代码略。

优化事件处理数量

在上面讲解WSAWaitForMultipleEvents的时候,有提到,在MSDN上也有说,已经规定好了,WSAWaitForMultipleEvents的第一个参数是事件处理的数量,这个数量是有限制的,就是64个,那么在实际工作中我们经常遇到要处理超过64个事件的情况,这个时候就需要对事件处理的数量就要进行优化,下面分两种思路来进行。
这里需要注意的是:限制是指我们调用一次WSAWaitForMultipleEvents只能最多处理64个事件,因此超过这个数量的事件不是不可以处理,而是要优化后才能处理,不能通过一次WSAWaitForMultipleEvents来搞定。

一次次处理

既然总的事件数量n大于WSAWaitForMultipleEvents一次调用能处理的数量,我们可以把这n个事件都放到一个大数组中,这里我们每次调用WSAWaitForMultipleEvents只处理一个事件,然后循环n次直至所有事件处理完毕,这个思路和上面的有序优化非常相似,只需要修改fd_sockevent_set代码即可:

//eventSelectSigle.cpp
struct fd_sockevent_set
{
	unsigned short count; 
	SOCKET sockall[1000]; //socket句柄数组,这里一个个处理事件,因此可以设置超过64
	WSAEVENT evnetall[1000]; //evnet句柄数组
};

当然如果能用多线程甚至是线程池来解决是最好的方案。

一组组处理

既然总的事件数量大于WSAWaitForMultipleEvents一次调用能处理的数量,但可分为m组,这里我们每次调用WSAWaitForMultipleEvents只处理一组事件,一共循环m组,每组处理多个事件。当然这里如果是单线程,那么我们就可以用一个线程逐个处理m组事件,如果是多线程就可以一次处理多组事件(每组64个)。就是定义fd_sockevent_set数组,然后申请内存初始化:

//eventSelectGroup.cpp
	fd_sockevent_set sockevent_set[10];//10组,每组可以处理64个事件
	memset(sockevent_set,0,sizeof(struct fd_sockevent_set)*10);

然后while循环里面再加一层组的循环即可。
另外还要加一段代码,使得在添加事件和SOCKET的时候,使得先装满第一组在装第二组

for(int m=0;m<10;m++)
						{
							if(sockevent_set[m].count<64)
							{
								sockevent_set[j].evnetall[sockevent_set[j].count]=wsaClientEvent;
								sockevent_set[j].sockall[sockevent_set[j].count]=socketClient; 
								sockevent_set[j].count++;
								break;//没有这个就会所有组都装
							}
						}

一组组处理的完整参考代码:

#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句柄数组
};

fd_sockevent_set sockevent_set[10];//10组,每组可以处理64个事件,变成全局变量后不用memset

BOOL WINAPI cls(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT :
		//释放数组中的事件和SOCKET句柄
		for(int j = 0; j < 10;j++)
		{
			for(int i=0;i<sockevent_set[j].count;i++)
			{		
				WSACloseEvent(sockevent_set[j].evnetall[i]);
				closesocket(sockevent_set[j].sockall[i]); 
			}
		}
		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");

	
	//memset(sockevent_set,0,sizeof(struct fd_sockevent_set)*10);



	//创建事件对象
	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里面
	//因为有多组,这里就直接把服务器的事件和SOCKET丢第0组
	sockevent_set[0].evnetall[sockevent_set[0].count]=eventServer;
	sockevent_set[0].sockall[sockevent_set[0].count]=socketServer; 
	sockevent_set[0].count++;

	//循环查询事件状态是否有信号
	while(1)
	{
		for(int j = 0; j < 10; j++)
		{
			//soindex这里是socket
			for(DWORD soindex = 0;soindex <sockevent_set[j].count; soindex++)
			{
				//该组里面没有事件则跳过
				if(sockevent_set[j].count==0)
				{
					continue;
				}
				//参数1代表查询几个事件,这里每次只查询一个
				//每次按下标轮流查
				//倒数第二个参数0代表不等待,直接返回,因为还有别的事件要查询
				DWORD retSignal =  WSAWaitForMultipleEvents(1,&sockevent_set[j].evnetall[soindex],false,0,false);

				if (retSignal == WSA_WAIT_FAILED)
				{
					//报错处理
					int retSignalerr = WSAGetLastError();
					printf("循环查询单个事件状态错误码为:%d\n",retSignalerr);
					continue;

				}

				//当前事件如果没有信号就跳过循环
				if(retSignal == WSA_WAIT_TIMEOUT)
				{
					continue;
				}

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

				//按位与判断是否是FD_ACCEPT操作码
				if(NetworkEvents.lNetworkEvents & FD_ACCEPT)
				{
					//判断FD_ACCEPT错误码对应位是否有值
					if(NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
					{
						//正常处理,创建客户端
						SOCKET socketClient=accept(sockevent_set[j].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里面
						//因为这里有10组,因此要做循环来放,放满一组才放下一组
						for(int m=0;m<10;m++)
						{
							if(sockevent_set[m].count<64)
							{
								sockevent_set[j].evnetall[sockevent_set[j].count]=wsaClientEvent;
								sockevent_set[j].sockall[sockevent_set[j].count]=socketClient; 
								sockevent_set[j].count++;
								break;//没有这个就会所有组都装
							}
						}

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

				
				}
				
				//按位与判断是否是FD_WRITE操作码
				if(NetworkEvents.lNetworkEvents & FD_WRITE)
				{
					//判断错误码对应位是否有值,没有说明SOCKET没有错误
					if(NetworkEvents.iErrorCode[FD_WRITE_BIT] == 0)
					{
						if(send(sockevent_set[j].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_READ操作码
				if(NetworkEvents.lNetworkEvents & FD_READ)
				{
					//判断错误码对应位是否有值,没有说明SOCKET没有错误
					if(NetworkEvents.iErrorCode[FD_READ_BIT] == 0)
					{
						char strRecv[1500] = {0};
						if(recv(sockevent_set[j].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操作码
				if(NetworkEvents.lNetworkEvents & FD_CLOSE)
				{
					printf("FD_CLOSE操作\n");
					printf("得到FD_CLOSE操作的错误码为:%d\n",NetworkEvents.iErrorCode[FD_CLOSE_BIT]);

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

					sockevent_set[j].count--;

				}
			}
		}


		
		
	}

	

	//释放数组中的事件和SOCKET句柄
	for(int j = 0; j < 10;j++)
	{
		for(int i=0;i<sockevent_set[j].count;i++)
		{		
			WSACloseEvent(sockevent_set[j].evnetall[i]);
			closesocket(sockevent_set[j].sockall[i]); 
		}
	}


	WSACleanup();


	system("pause");
	return 0;
}
}

释放事件和SOCKET

之前写的代码没有在最后释放数组中的事件句柄和SOCKET句柄,因此要在两个地方做释放。
一个是在程序执行完成的最后部分,加释放代码;
一个是在捕获点击关闭事件的时候,加释放代码。
就是上面程序中有WSACleanup();的前面加,加释放代码要注意几个事情:
1.服务器的事件和SOCKET不需要单独释放了,因为它也在数组里面,也会被释放;
2.要想在关闭事件里面释放,要把数组定义成全局变量,全局变量不用初始化,会自动设置为全0的初始值;
3.一组组的处理要额外加一套循环,先对组循环,在对每组中每个数组元素进行循环。
释放代码大概是:

//释放数组中的事件和SOCKET句柄
	for(int i=0;i<sockevent_set.count;i++)
	{		
		WSACloseEvent(sockevent_set.evnetall[i]);
		closesocket(sockevent_set.sockall[i]); 
	}

这里要注意,一组组的方式来优化连接数量中,释放代码有所不一样

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值