1. 事件选择模型的意义
上一篇博客5种Windows网络模型之select模型介绍了select模型克服了CS模型的几种缺点,但是select模型也存在缺陷,比如当select函数将所有socket投递给操作系统后,系统系统帮我们把有信号的socket装进fd_set结构体,直至返回有事件的socket集合这一过程程序是阻塞等待的,为了克服这个缺点人们提出了事件选择模型。
2. Windows处理用户行为的两种方式
2.1 事件机制
核心:事件集合
处理过程:根据需要,我们为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少就创建多少,将事件投递给系统,让系统帮忙监管。
特点:所有的时间都是用户自己定义的,系统只是帮用户置成有无信号,事件是没有顺序的(我们介绍的事件选择模型就是应用该机制)。
2.2消息机制
核心:消息对列
处理过程:所有的用户操作,比如点击鼠标、键盘等,所有的操作均依次按顺序被记录,装进一个队列。
特点:消息对列由操作系统维护,咱们做的操作被操作系统取出来分类处理(有先后顺序)。
3. 事件选择模型
3.1 事件选择模型的整体逻辑
- 创建一个事件对象(WSACreateEvent);
- 为每一个事件对象绑定一个socket以及操作(accept、recv等)并投递给操作系统,程序员无需其它操作(注:这里select函数是循环检测是否有socket响应,而事件选择模型使主动件有响应的socket投递给操作系统)。
- 查看事件是否有信号(WSAWaitForMultipleEvents);
- 有信号的话就分类处理(WSAEnumNetWorkEvents)
3.2 函数详解
- 创建一个事件对象
WSAEVENT eventSever = WSACreateEvent();
参数:无
返回值:如果没有错误则返回事件对象的句柄;
如果出错则返回 WSA_INVALID_EVENT。 - 绑定并投递
函数原型:
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。
- 询问事件
函数原型:
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,则代表函数执行失败。
- 列举事件
函数原型:
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函数在向收发缓冲区读取或写入数据时仍然是阻塞的,要想从根本上克服这个缺点,利用多线程机制是一种行之有效的方法。