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分类处理
事件选择模型相关函数
同样的,这个模型的前面步骤和基本模型是一样的
-
打开网络库
-
校验版本
-
创建SOCKET
-
绑定地址与端口
-
开始监听
到这里都一样
创建、销毁、重置事件对象
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,关于片段生命周期
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段