写在前面
前面的示例中都是通过send & recv函数进行同步IO。
调用send函数时,完成数据传输后才能从函数返回(确切地说,只有把数据完全传输到输出缓冲后才能返回)。
而调用recv函数时,只有读到期望大小的数据后才能返回。
因此这里同步IO的缺点也显而易见:进行IO的过程中函数无法返回,所以不能执行其他任务。
异步通知IO模型
顾名思义,通知IO是指发生了IO相关的特定情况。典型的通知IO模型是select方式,还记得selcect实现IO复用中调用select等待IO事件发生。不过这种通知是以同步方式进行的,原因在于,需要IO或可以进行IO的时间点(简言之就是IO相关事件发生的时间点)与select函数的返回时间点一致,即数据发送完成后触发select函数返回。
而异步通知IO模型,则可以在调用的时候返回,在后台进行IO时处理其他任务,最后在回来判断是否IO完成。
使用WSAEventSelect实现异步通知IO
如前所述,告知IO状态变化的操作就是“通知”。IO的状态可以分为不同情况:
- 套接字的状态变化:套接字的IO状态变化
- 发生套接字相关事件:发生套接字IO相关事件
这两种情况都意味着发生了需要或可以进行IO的事件。
可以使用WSAEventSelect函数监视某一套接字的上述事件,原型如下:
#include <winsock2.h>
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
参数及返回值说明:
s: 监视对象的套接字句柄
hEventObject: 传递事件对象句柄以验证事件发生与否
lNetworkEvents: 希望监视的事件类型信息
返回值:成功时返回0,失败时番红花SOCKET_ERROR
传输参数s的套接字内只要发生lNetworkEvents中指定的事件之一,WSAEventSelect函数就将hEventObject句柄所指的内核对象改为signaled状态。因此,该函数又称“连接事件对象和套接字的函数”。
另外还需说明的是,无法事件发生与否,WSAEventSelect函数调用后都会直接返回(与select函数调用后阻塞不同),所以可以执行其他任务。即该函数以异步方式工作。
参数三的可监视的事件类型信息如下,可以通过位或运算同时指定多个信息:
- FD_READ: 是否存在需要接收的数据
- FD_WRITE: 能否以非阻塞方式传输数据
- FD_OOB:是否收到带外数据
- FD_ACCEPT:是否有新的连接请求
- FD_CLOSE:是否有断开连接的请求
与select比较
select实现IO复用一文中,select可以把多个套接字集中到一起监视,而WSAEventSelect只能监视一个套接字。
但使用select处理相关IO事件后,需要再次调用select监视,WSAEventSelect则无需多次调用,因为通过调用WSAEventSelect函数传递的套接字信息已注册到操作系统,所以无需再次调用。
使用步骤
从WSAEventSelect函数的说明中可以看出,要想使用该函数,还需要以下信息:
①WSAEventSelect函数的第二个参数中用到的事件对象的创建方法
②调用WSAEventSelect函数后发生事件的验证方法
③验证事件发生后事件类型的查看方法
事件对象的创建
因为WSAEventSelect函数中的事件对象必须时manual-reset类型的事件对象,因此可以通过CreateEvent函数创建。但这里介绍另一个直接创建manual-reset事件对象的函数:
#include <winsock2.h>
WSAEVENT WSACreateEvnet();
//WSAEVENT定义如下:
#define WSAEVENT HANDLE
该函数调用成功时返回manual-reset类型的事件对象句柄,失败时返回WSA_INVALID_EVENT。
与之对应的销毁函数如下:
BOOL WSACloseEvent(WSAEVENT hEvent);
验证事件是否发生
之前接触到的验证事件对象是否触发的函数有WaitForSingleObject和WaitForMultipleObjects函数。因此本章会监听多个套接字事件,因此会用到WaitForMutipleObjects函数。同理,这里会有另一个WSA前缀的Wait函数,除了多一个参数外,其他均与WaitForMultipleObjects函数相同。
#include <winsock2.h>
DWORD WSAWaitForMultipleEvents(DWORD cEvents, const WSAEVENT* lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable);
参数及返回值说明:
cEvents:需要验证是否状态signaled状态的事件对象的个数
lphEvents:存有事件对象句柄的数组地址值
fWaitAll:传递TRUE时,所有事件对象在signaled状态时返回;传递FALSE时,只有其中1个变为signaled状态就返回
dwTimeout:以ms为单位传递超时时间,传递WSA_INFINITE时会永久阻塞,传递0时会马上返回。
fAlertable:传递TRUE时进入alertable wait(可警告等待)状态,比WaitForMultipleObjects多的一个参数,会在后续重叠IO模型中使用
返回值:返回值减去常量WSA_WAIT_EVENT_0时,可以得到转变为signaled状态的事件对象句柄对应的索引,通过该索引可以在第二个参数的事件对象数组中找到对应的事件对象句柄。如果有多个事件对象变为signaled状态,则会得到其中较小的值。发生超时返回WSA_WAIT_TIMEOUT。
通过返回值,可以得知只通过调用一次函数无法得到转为signaled状态的所有事件对象句柄的信息。有多个转为signaled状态的事件对象时,只会返回索引较小的哪个。
但可以通过事件对象为manual-reset的特性,通过以下方式得到所有转为signaled状态的事件对象句柄:
int nPosInfo, nStartIndex;
//有一个事件变为signaled状态就返回
nPosInfo = WSAWaitForMultipleEvents(numOfSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
//得到句柄在事件对象数组中的索引
nStartIndex = nPosInfo - WSA_WAIT_EVENT_0;
//从第一个触发的事件索引开始遍历,得到后面(包括第一个触发事件对象)已触发的事件对象句柄
for (int i = nStartIndex; i < numOfSock, i++)
{
//超时时间为0,立即返回
//因为事件对象都是manual-reset类型,因此触发后不会自动变化为non-signaled状态
int nSignaledEventPosInfo = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
if (nSignaledEventPosInfo == WSA_WAIT_FAILED || nSignaledEventPosInfo == WSA_WAIT_TIMEOUT)
{
//失败或超时
continue;
}
else
{
//此时的i即为触发事件对象在事件对象数组中的索引
WSAEVENT hEvent = hEventArr[i];
}
}
上面代码解释了,为什么异步通知IO模型中事件对象必须为manual-reset模式,是为了得到所有触发的事件对象。
区分事件类型
WSAEventSelect的第三个参数中若注册了多个事件类型,那么事件触发后可以通过以下函数区分是哪个类型导致的触发。
#include <winsock2.h>
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
s: 发生事件的套接字句柄
hEventObject:与套接字相连(即由WSAEnumNetworkEvents调用绑定)的事件对象句柄
lpNetworkEvents: 保存发生的事件类型信息和错误信息的WSANETWORKEVENTS结构体变量地址值
注意:该函数会将manual-reset模式的事件对象由signaled状态改为non-signaled状态。所以得到发生的事件对象后,不必再单独调用ResetEvent手动置为non-signaled状态。
LPWSANETWORKEVENTS 是结构体WSANETWORKEVENTS的指针类型,结构体定义如下:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents; //触发事件类型:FD_ACCEPT, FD_READ, FD_CLOSE等
int iErrorCode[FD_MAX_EVENTS]; //保存错误信息
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
这里注意如何判断是否发生错误:
- 如果发生FD_READ相关错误,则在iErroCode[FD_READ_BIT]中保存除0以为的其他值
- 同理,如果发生FD_WRITE相关错误,则在iErrorCode[FD_WRITE_BIT]中保存除0以为的其他值
示例如下:
WSANETWORKEVENTS netEvents;
//触发事件,得到事件句柄
//...
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);
if (netEvents.lNetworkEvents & FD_READ)
{
if (netEvents.iErrorCode[FD_READ_BIT] != 0)
{
//发生FD_READ事件相关错误
}
else
{
//recv读取数据
}
}
if (netEvents.lNetworkEvents & FD_WRITE)
{
if (netEvents.iErrorCode[FD_WRITE_BIT] != 0)
{
//发生FD_WRITE事件相关错误
}
else
{
//write写入输出缓冲完成
}
}
此次,异步通知IO模型的内容已介绍完成,下面给出利用异步通知IO模型实现的回声服务器的完整代码。
基于异步通知IO模型的回声服务器
// AsynNotiServer.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 100
void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
puts("argc error!");
return -1;
}
WSADATA wsaData;
if (0 != WSAStartup(MAKEWORD(2, 2, ), &wsaData))
{
puts("WSAStartup error!");
return -1;
}
SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == srvSock)
{
puts("socket error!");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
puts("bind error!");
closesocket(srvSock);
WSACleanup();
return -1;
}
if (SOCKET_ERROR == listen(srvSock, 5))
{
puts("listen error!");
closesocket(srvSock);
WSACleanup();
return -1;
}
//创建manual-reset模式的事件内核对象
WSAEVENT newEvents = WSACreateEvent();
if (SOCKET_ERROR == WSAEventSelect(srvSock, newEvents, FD_ACCEPT))
{
puts("WSAEventSelect error!");
closesocket(srvSock);
WSACleanup();
return -1;
}
//创建异步通知IO模型
SOCKET hScokArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
int numOfCltSock = 0;
//注意这里hScokArr 和 hEventArr通过numOfCltSock一一对应
hScokArr[numOfCltSock] = srvSock;
hEventArr[numOfCltSock] = newEvents;
numOfCltSock++;
WSANETWORKEVENTS netEvents;
int posInfo = 0, startIdx = 0;
SOCKADDR_IN cltAddr;
memset(&cltAddr, 0, sizeof(cltAddr));
int nCltAddrSize = sizeof(cltAddr);
int nRecvLen = 0;
char msg[BUF_SIZE] = {};
while (true)
{
posInfo = WSAWaitForMultipleEvents(numOfCltSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
startIdx = posInfo - WSA_WAIT_EVENT_0;
//遍历得到所有触发的事件及对应套接字句柄
for (int i = startIdx; i < numOfCltSock; i++)
{
//这里逐个判断事件释放触发
int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
printf("sigEventIdx: %d, i: %d \n", sigEventIdx, i);
//判断hEventArr[i]事件是否触发
if (sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT)
{
puts("wait failed or timeout!");
continue;
}
else
{
//得到触发事件在事件数组中的索引
sigEventIdx = i;
//得到触发类型
WSAEnumNetworkEvents(hScokArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);
//判断是否是监听套接字的接受连接事件
if (netEvents.lNetworkEvents & FD_ACCEPT)
{
if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
{
printf("WSAEnumNetworkEvents--socket《%d》 accept error! errorcode: %d \n"
, hScokArr[sigEventIdx], netEvents.iErrorCode[FD_ACCEPT_BIT]);
break;
}
nCltAddrSize = sizeof(cltAddr);
SOCKET cltSock = accept(hScokArr[sigEventIdx], (sockaddr*)&cltAddr, &nCltAddrSize);
if (cltSock == INVALID_SOCKET)
{
printf("WSAEnumNetworkEvents--socket《%d》 accept error!\n", hScokArr[sigEventIdx]);
break;
}
//添加到异步通知IO模型中
newEvents = WSACreateEvent();
WSAEventSelect(cltSock, newEvents, FD_READ | FD_CLOSE);
//这里也可判断绑定套接字和事件是否成功
hScokArr[numOfCltSock] = cltSock;
hEventArr[numOfCltSock] = newEvents;
numOfCltSock++;
printf("client《%d》 connected...\n", cltSock);
}//end if (netEvents.lNetworkEvents & FD_ACCEPT)
//判断是否是客户端套接字的数据接收事件
if (netEvents.lNetworkEvents & FD_READ)
{
if (netEvents.iErrorCode[FD_READ_BIT] != 0)
{
printf("client<%d> read event error, msg: %d\n", hScokArr[sigEventIdx], netEvents.iErrorCode[FD_READ_BIT]);
break;
}
nRecvLen = recv(hScokArr[sigEventIdx], msg, BUF_SIZE, 0);
msg[nRecvLen] = 0;
//回发
send(hScokArr[sigEventIdx], msg, nRecvLen, 0);
}
//判断是否是客户端套接字的断开连接事件
if (netEvents.lNetworkEvents & FD_CLOSE)
{
if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0)
{
printf("client<%d> close event error, msg: %d\n", hScokArr[sigEventIdx], netEvents.iErrorCode[FD_CLOSE_BIT]);
break;
}
//释放套接字对应事件内核对象资源
WSACloseEvent(hEventArr[sigEventIdx]);
//关闭套接字
closesocket(hScokArr[sigEventIdx]);
//更新数组资源
numOfCltSock--;
CompressSockets(hScokArr, sigEventIdx, numOfCltSock);
CompressEvents(hEventArr, sigEventIdx, numOfCltSock);
}
}
}
}
//服务器开启后会一直在循环中等待客户端连接,只能强制终止,所以代码不会运行到这
closesocket(srvSock);
WSACleanup();
puts("任意键继续...");
getchar();
return 0;
}
void CompressSockets(SOCKET hSockArr[], int idx, int total)
{
for (int i = idx; i < total; i++)
{
hSockArr[i] = hSockArr[i + 1];
}
}
void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{
for (int i = idx; i < total; i++)
{
hEventArr[i] = hEventArr[i + 1];
}
}
为方便调试,这里同样给出客户端代码:
// echo_client.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
printf("arg error!");
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup error!");
return -1;
}
SOCKET cltSock = socket(PF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == cltSock)
{
printf("socket error!");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
srvAddr.sin_port = htons(_ttoi(argv[2]));
if (SOCKET_ERROR == connect(cltSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("connect error!");
closesocket(cltSock);
WSACleanup();
return -1;
}
char Msg[BUF_SIZE];
int strLen = 0;
int sendLen = 0;
while (true)
{
fputs("Input Msg(Q to quit): ", stdout);
fgets(Msg, BUF_SIZE, stdin);
if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n"))
{
break;
}
sendLen = 0;
sendLen += send(cltSock, Msg, strlen(Msg), 0);
//回声客户端的问题:这里send后马上recv,当send的字符串过大,服务器防分多次会发时,这里马上recv有可能没有收到全部的数据
//延时recv解决:问题是不知延时多久。
//Sleep(3000);
//strLen = recv(cltSock, Msg, BUF_SIZE - 1, 0);
//Msg[strLen] = 0;
//正确解决:
strLen = 0;
while (strLen < sendLen)
{
int recvLen = recv(cltSock, &Msg[strLen], BUF_SIZE - 1, 0);
if (recvLen == -1)
{
closesocket(cltSock);
WSACleanup();
return -1;
}
strLen += recvLen;
}
Msg[strLen] = 0;
printf("Msg From Server: %s \n", Msg);
}
closesocket(cltSock);
WSACleanup();
return 0;
}
总结
同select实现IO复用一样,WSAEventSelect函数是实现异步通知IO模型的关键,因此也需熟练掌握。
上文比较了通过select使用的同步通知IO模型和WSAEventSelect函数实现异步通知IO模型的差异及优缺点,同样也介绍了WSAEventSelect的相关扩展函数及使用步骤。
最后给出了基于异步通知IO模型实现的回声服务器代码,可在实际工作中按需调整。