https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于UDP的网络编程还有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这节讲基于UDP的事件选择模型。
关于基础知识,前面有写过,这里贴过来:
基础知识
windows处理用户行为有两种方式:
消息机制
其核心是消息队列,就是将要处理的操作放到队列(FIFO)中进行处理。 其特点是消息队列由操作系统维护,处理过程遵循队列特点,处理过程中,操作可以同时进行入队。 消息机制的处理是有序的(按队列的顺序),全面的(无论事件是否注册,都会进入到队列中,我们可以选择我们想要处理的事件来进行处理) 基于这个消息机制的异步选择模型下一篇讲。
事件机制
其核心是事件集合,同上面一样也是操作,但是这里没有先后顺序,是一个集合,处理的顺序由程序员决定。 根据需求,我们为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少创建多少。 当有对应的操作发生,例如单击鼠标左键,那么事件就会出发信号,程序员可以获取到这个信号,然后对信号进行处理。 其特点是所有事件都是自定义的,系统只管检测是否有信号。由于事件集合的无序性,当事件定义过多,会挤兑一些事件的执行效率(有人插队,轮不到)。 事件机制是无序的(集合的特点),不全面的(只有绑定的事件才会获取信号,没有绑定的事件就忽略) 本节来学习事件选择模型。
事件选择模型步骤
第一步:使用WSACreateEvent创建一个事件对象(变量) 第二步:使用WSAEventSelect为每一个事件对象绑定个SOCKET句柄,以及操作recvfrom,sendto等,并投递给系统(两个事情:绑定,投递) 第三步:循环使用WSAWaitForMultipleEvents查看事件是否有信号(这里要注意,该函数在等待过程中线程是处于挂起状态,不占用CPU时间片,这也是和SELECT模型的根本区别) 第四步:有信号的话就使用WSAEnumNetworkEvents分类处理
事件选择模型逻辑
同样的,这个模型的逻辑和基本模型是一样的
-
打开网络库 -
校验版本 -
创建SOCKET -
绑定地址与端口
5. 开始监听 这个UDP没有
5、事件选择
6、有序处理
7、增加事件数量
8、释放
事件选择
创建事件对象
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsacreateevent
WSAEVENT WSAAPI WSACreateEvent();
成功返回事件对象句柄 不成功则返回WSA_INVALID_EVENT
由于事件对象是一个句柄(内核对象),因此有相关的配套操作函数: 销毁事件对象句柄:
BOOL WSAAPI WSACloseEvent(
WSAEVENT hEvent
);
重置事件对象句柄,将本来产生信号的事件重置为无信号状态:
BOOL WSAAPI WSAResetEvent(
WSAEVENT hEvent
);
同样的有:
BOOL WSAAPI WSASetEvent(
WSAEVENT hEvent
);
这个是将本来无信号的事件重置为有信号状态(但不能指定具体信号状态)。
具体代码:
WSAEVENT wse = WSACreateEvent();
if(WSA_INVALID_EVENT == wse)
{
int err = WSAGetLastError();
printf("创建事件对象失败错误码为:%d\n",err);
closesocket(socketServer);
WSACleanup();
return 0;
}
绑定并投递
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,常见的事件有(删除线代表TCP才有):
操作码(信号) | 发生原因 | 绑定操作 |
---|
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,这里指已经绑定的数量,不是最大数量) 对于UDP而言,就一个事件,就填1。 参数2:多个事件对象数组的指针入口; 参数3:TRUE代表要等多个事件对象都产生信号后才返回,然后将事件对象数组按数组索引依次进行处理,这种方式不常用,会产生由于等待造成较大的延时; FALSE代表只要多个事件对象中有一个产生信号后才返回,返回后用返回值减去宏WSA_WAIT_EVENT_0得到事件对象数组中有信号的事件对象的数组索引(下标),由于事件数组和SOCKET数组下标是一一对应关系(下面有讲),这个时候也获得了SOCKET数组下标。 需要注意的是,如果同时有多个事件对象产生信号,那么这个时候经过宏运算后得到是事件数组中下标最小的那个。 对于UDP而言,只有一个事件,所以这里用TRUE和FALSE没有什么区别。 参数4:等待时长,当查询完毕后,系统等待的时间长度,单位是毫秒。如果在等待过程中有事件信号产生则立刻返回。当超过设置的等待时长则返回WSA_WAIT_TIMEOUT,此时应该继续循环(continue;),相当于每次查询后会停顿一下,再根据if对WSA_WAIT_TIMEOUT的判断进行相应的处理; 当等待时长设置为0时,表示程序查询完时间状态后不等待,直接返回,进行下一轮查询; 当等待时长设置为WSA_INFINITE时,表示查询查询完会一直等待,直到有事件信号产生才返回,反正没信号也没事干,等着也行。 对于UDP而言,如果这里设置为WSA_INFINITE,那么和参数3设置为TRUE的效果是一样的。 参数5:TRUE,在重叠I/O模型中使用; FALSE,在事件选择模型中使用。
返回值: 有信号的数组下标:这里分两种情况:参数3如果是TRUE,那么是整个数组,如果参数3是FALSE那么只返回一个值; 当参数5为TRUE的时候,返回值为:WSA_WAIT_IO_COMPLETION; 当参数4设置了等待时长,超过这个设置的时长没有信号就会返回WSA_WAIT_TIMEOUT,接continue即可。 失败:WSA_WAIT_FAILED 对于UDP而言,这里只有一个事件,不需要计算有信号的数组下标。
DWORD dwRes = WSAWaitForMultipleEvents(1,&wse,FALSE,WSA_INFINITE,FALSE);
if(dwRes == WSA_WAIT_FAILED)
{
int err = WSAGetLastError();
printf("获取事件失败错误码为:%d\n",err);
break;
}
列举事件
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对应具体数组的下标,对应关系:
#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 wsne;
dwRes = WSAEnumNetworkEvents(socketServer,wseSever,&wsne);
if(dwRes == SOCKET_ERROR)
{
int err = WSAGetLastError();
printf("6.4列举事件失败错误码为:%d\n",err);
continue;
}
事件分类处理
事件分类处理逻辑
要用并列的if来进行逐个事件的判断,否则会有bug。 先用按位与来与某个事件进行判断,如果是该事件,那么判断是否有错误码,如果没有,就进行相应操作,以ACCEPT事件为例,固定写法大概如下所示:
if(pNetworkEvents->INetworkEvents&FD_ACCEPT)
{
if(lpNetworkEvents->iErrorCode[FD_ACCEPT_BIT]==0)
{
}
}
FD_READ
if(wsne.lNetworkEvents & FD_READ)
{
if(wsne.iErrorCode[FD_READ_BIT]==0)
{
struct sockaddr sa;
int iSaLen = sizeof(sa);
char strRecvBuff[548]={0};
if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
{
int err = WSAGetLastError();
printf("服务器recvfrom失败错误码为:%d\n",err);
continue;
}
printf("服务器recvfrom消息是:%s\n",strRecvBuff);
if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
{
int err = WSAGetLastError();
printf("服务器sendto失败错误码为:%d\n",err);
continue;
}
}
}
FD_WRITE
这个事件在TCP协议下:只会产生一次,且是自动产生,且在客户端被ACCEPT后,产生RECV后产生。 但是UDP协议下,客户端没有连接操作,也就是没有ACCEPT,因此FD_WRITE事件在服务器端运行后自动产生,也只会产生一次。
if(wsne.lNetworkEvents & FD_WRITE)
{
if(wsne.iErrorCode[FD_WRITE_BIT]==0)
{
printf("FD_WRITE事件执行完成!\n");
}
}
有序处理
TCP中由于socket很多,我们要进行相关的有序处理; UDP就一个socket,就不用考虑顺序问题了。
增加事件数量
TCP中由于socket很多,每个socket要绑定一个事件,所以我们要多事件数量很多的情况进行逻辑处理 UDP就一个,不需要增加事件数量
|