理解异步通知I/O模型
理解同步和异步
首先解释“异步”(Asynchronous)的含义。异步主要指“不一致”,它在数据I/O中非常有用。之前的Windows示例中主要通过send&recv函数进行同步I/O。调用send函数时,完成数据传输后才能从函数返回;而调用recv函数时,只有读到期望大小的数据后才能返回。因此,相当于同步方式的I/O处理。
同步的关键是函数的调用及返回时刻,以及数据传输的开始和完成时刻。
“调用send函数的瞬间开始传输数据,send函数执行完的时刻完成数据传输。”
“调用recv函数的瞬间开始接收数据,recv函数执行完的时刻完成数据接收。”
那异步I/O的含义又是什么呢?异步I/O是指I/O函数的返回时刻与数据收发的完成时刻不一致。
同步I/O的缺点及异步方式的解决方案
异步I/O就是为了克服同步I/O的缺点而设计的模型。同步I/O有哪些缺点?异步方式又是如何解决的呢?其实,第17章的最后部分“条件触发和边缘触发孰强孰弱”中给出过答案。各位可能忘记这些内容,考虑到这一点,我将以不同的、更简单的方式解释。同步I/O的缺点:“进行I/O的过程中函数无法返回,所以不能执行其他任务!”而异步无论数据是否完成交换都返回函数,这就意味着可以执行其他任务。所以说“异步方式能够比同步方式更有效地使用CPU"
理解异步通知I/O模型
之前分析了同步和异步方式的I/O函数,确切地说,分析了同步和异步方式下I/O函数返回时
间点的差异。下面我希望扩展讨论的对象。
本章题目为“异步通知I/O模型”,意为“通知I/O”是以异步方式工作的。
首先了解一下“通知I/O”的含义:“通知输入缓冲收到数据并需要读取,以及输出缓冲为空故可以发送数据。"
顾名思义,“通知I/O”是指发生了I/O相关的特定情况。典型的通知I/O模型是select方式。还记得select监视的3种情况吗?其中具代表性的就是“收到数据的情况”。select函数就是从返回调点用的函数时通知需要I/O处理的,或可以进行I/O处理的情况。但这种通知是以同步方式进行的,原因在于,需要I/O或可以进行I/O的时间点与select函数的返回时间点一致。
相信各位已理解通知I/O模型的含义。与“select函数只在需要或可以进行I/O的情况下返回”不同,异步通知I/O模型中函数的返回与I/O状态无关。本章的WSAEventSelect函数就是select函数的异步版本。
可能有人疑问:“既然函数的返回与I/O状态无关,那是否需要监视I/O状态变化?”
当然需要!异步通知I/O中,指定I/O监视对象的函数和实际验证状态变化的函数是相互分离的。因此,指定监视对象后可以离开执行其他任务,最后再回来验证状态变化。以上就是通知I/O的所有理论,下面通过具体函数实现该模型。
理解和实现异步通知I/O模型
WSAEventSelect函数和通知
如前所述,告知I/O状态变化的操作就是“通知”。I/O的状态变化可以分为不同情况。
□套接字的状态变化:套接字的I/O状态变化。
□发生套接字相关事件:发生套接字I/O相关事件。
这2种情况都意味着发生了需要或可以进行I/O的事件,我将根据上下文适当混用这些概念。
首先介绍WSAEventSelect函数,该函数用于指定某一套接字为事件监视对象。
#include<winsock2.h>
int WSAEventSelect(SOCKET s,WSAEVENT hEventObeject,long lNetworkEvent);
//成功时返回0,失败时返回 SOCKET_ERROR。
s //监视对象的套接字句柄。
hEventObject //传递事件对象句柄以验证事件发生与否。
INetworkEvents //希望监视的事件类型信息。
传入参数s的套接字内只要发生INetworkEvents中指定的事件之一,WSAEventSelect函数就将hEventObject句柄所指内核对象改为signaled状态。因此,该函数又称“连接事件对象和套接字的函数。"
另外一个重要的事实是,无论事件发生与否,WSAEventSelect函数调用后都会直接返回。也就是说,该函数以异步通知方式工作。
下面介绍作为该函数第三个参数的事件类型信息,可以通过位或运算同时指定多个信息。
□ FD_READ:是否存在需要接收的数据?
□ FD_WRITE:能否以非阻塞方式传输数据?
□FD_OOB:是否收到带外数据?
□ FD_ACCEPT:是否有新的连接请求?
□ FD_CLOSE:是否有断开连接的请求?
以上就是WSAEventSelect函数的调用方法。
各位或许有如下疑问:“啊?select函数可以针对多个套接字对象调用,但WSAEventSelect函数只能针对1个套接字对象调用!”
的确,仅从概念上看,WSAEventSelect函数的功能偏弱。但使用该函数时,没必要针对多个套接字进行调用。从select函数返回时,为了验证事件的发生需要再次针对所有句柄调用函数,但通过调用WSAEventSelect函数传递的套接字信息已注册到操作系统,所以无需再次调用。这反而是WSAEventSelect函数比select函数的优势所在。
从前面关于WSAEventSelect函数的说明中可以看出,需要补充如下内容。
□WSAEventSelect函数的第二个参数中用到的事件对象的创建方法。
□调用WSAEventSelect函数后发生事件的验证方法。
□验证事件发生后事件类型的查看方法。
manual-reset 模式事件对象的其他创建方法
我们之前利用CreateEvent函数创建了事件对象。CreateEvent函数在创建事件对象时,可以在auto-reset模式和manual-reset模式中任选其一。但我们只需要manual-reset模式non-signaled状态的事件对象,所以利用如下函数创建较为方便。
#include<winsock2.h>
WSAEVENT WSACreateEvent(void);
//成功时返回事件对象句柄,失败时返回WSA_INVALID_EVENT。
上述声明中返回类型WSAEVENT的定义如下:
#define WSAEVENT HANDLE
实际上就是我们熟悉的内核对象句柄,这一点需要注意。
另外,为了销毁通过上述函数创建的事件对象,系统提供了如下函数。
#include<winsock2.h>
BOOL WSACloseEvent(WSAEVENT hEvent);//成功时返回TRUE,失败时返回FALSE
验证是否发生事件
既然介绍了WSACreateEvent函数,那调用WSAEventSelect函数应该不成问题。接下来就要考虑调用WSAEventSelect函数后的处理。为了验证是否发生事件,需要查看事件对象。完成该任务的函数如下,除了多1个参数外,其余部分与WaitForMultipleObjects函数完全相同。
#include<winsock2.h>
DWORD WSAWaitForMultipleEvent(DWORD cEvents, const WSAEVENT * lphEvents, BOOL fwaitAll, DWORD dwTimeout,BOOL fAlertable);
//成功时返回发生事件的对象信息,失败时返回WSA_INVALID_EVENT。
cEvents //需要验证是否转为signaled状态的事件对象的个数
IphEvents //存有事件对象句柄数组地址值
fWaitAll //传递TRUE时,所有事件对象在signaled状态时返回;传递FALSE时,只要其中1个变为
//signaled状态就返回。
dwTimeout //以1/1000秒为单位指定超时,传递WSA_INFINITE时,直到变为signaled状态时才会返
//回。
fAlertable//传递TRUE时进入alertable_wait(可警告等待)状态(第22章)
返回值 //返回值减去常量WSA_WAIT_EVENT_0时,可以得到转变为signaled状态的事件对象句柄对
//应的素引,可以通过该索引在第二个参数指定的数组中查找句柄。如果有多个事件对象变为
//signaled状态,则会得到其中较小的值。发生超时将返回WSA_WAIT_TIMEOUT。
由于发生套接字事件,事件对象转为signaled状态后该函数才返回,所以它非常有利于确认事件发生与否。但由于最多可传递64个事件对象,如果需要监视更多句柄,就只能创建线程或扩展保存句柄的数组,并多次调用上述函数。
对于WSAWaitForMultipleEvents函数,各位可能产生如下疑问:"WSAWaitForMultipleEvents函数如何得到转为signaled状态的所有事件对象句柄的信息?”
答案是:只通过1次函数调用无法得到转为signaled状态的所有事件对象句柄的信息。通过该函数可以得到转为signaled状态的事件对象中的一个(按数组中的保存顺序)索引值。但可以利用“事件对象为manual-reset模式”的特点,通过如下方式获得所有signaled状态的事件对象。
int posInfo,startIdx,i;
.....
posInfo=WSAWaitForMultipleEvent(numOfSock,hEventArray,FALSE,WSA_INFINITE,FALSE);
startIdx=posInfo-WSA_WAIT_EVENT_0;
.....
for(i=stratIdx;i<numOfSock;i++){
int sigEventIdx=WSAWaitForMultipleEvents(1,&hEventArray[i],TRUE,0,FALSE);
.....
}
注意观察上述代码中的循环。循环中从第一个事件对象到最后一个事件对象逐一依序验证是否转为signaled状态(超时信息为0,所以调用函数后立即返回)。之所以能做到这一点,完全是因为事件对象为manual-reset模式,这也解释了为何在异步通知I/O模型中事件对象必须为manual-reset模式。
区分事件类型
既然已经通过WSAWaitForMultipleEvents函数得到了转为signaled状态的事件对象,最后就要确定相应对象进入signaled状态的原因。为完成该任务,我们引入如下函数。调用此函数时,不仅需要signaled状态的事件对象句柄,还需要与之连接的(由WSAEventSelect函数调用引发的)发生的套接字句柄。
#include<winsock2.h>
int WSAEnumNetworkEvent(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvent);
//成功时返回0,失败时返回SOCKET_ERROR。
s //发生事件的套接字句柄。
hEventObject //与套接字相连的(由WSAEventSelect函数调用引发的)signaled状态的件对象句柄。
lpNetworkEvents//保存发生的事件类型信息和错误信息的WSANETWORKEVENTS结构体变量地址值。
上述函数将manual-reset模式的事件对象改为non-signaled状态,所以得到发生的事件类型后,不必单独调用ResetEvent函数。下面介绍与上述函数有关的WSANETWORKEVENTS结构体。
typedef struct _WSANETWORKEVENTS{
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
}WSANETWORKEVENTS,* LPWSANETWORKEVENTS;
上述结构体的INetworkEvents成员将保存发生的事件信息。与WSAEventSelect函数的第三个参数相同,需要接收数据时,该成员为FD_READ;连接请求时,该成员为FD_ACCEPT。因此,可通过如下方式查看发生的事件类型。
WSANETWORKEVENTS netEvents;
.....
WSAEnumNetworkEvents(hSock,hEvent,&netEvents);
if(netEvents.lNetworkEvents & FD_ACCEPT){
//FD_ACCEPT事件的处理
}
if(netEvents.lNetworkEvents & FD_READ){
//FD_READ事件的处理
}
if(netEvents.lNetworkEvents & FD_CLOSE){
//FD_CLOSE事件的处理
}
另外,错误信息将保存到声明为成员的iErorCode数组(发生错误的原因可能很多,因此用数组声明)。验证方法如下。
□如果发生FD_READ相关错误,则在iErrorCode[FD_READ_BIT]中保存除0以外的其他值。
□如果发生FD_WRITE相关错误,则在iErrorCode[FD_WRITE_BIT]中保存除0以外的其他值。
可以通过以下的描述来理解上述内容。
“如果发生FD_XXX相关错误,则在iErorCode[FD_XXX_BIT]中保存除0以外的其他值"
因此可以用如下方式检查错误。
WSANETWORKEVENTS netEvents;
......
WSAEnumNetworkEvents(hSock,hEvent,&netEvents);
if(netEvents.iErrorCode[FD_READ_BIT] != 0){
//发生FD_READ事件相关错误
}
以上就是异步通知I/O模型的全部内容,下面通过这些知识编写示例。
利用异步通知I/O模型实现回声服务器端
由于代码较长,所以将分成多个部分进行介绍。
#include<stdio.h>
#include<string.h>
#include<winsock2.h>
#define BUF_SIZE 100
void CompressSockets(SOCKET hSockArr[],int idx,int total);
void CompressEvents(WSAEVENT hEventArr,int idx,int total);
void ErrorHandling(char *msg);
int main(int argc,char *argv[]){
WSADATA wsaData;
SOCKET hServSock,hClntSock;
SOCKADDR_IN serAdr,clntAdr;
SOCK hSockArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT newEvent;
WSANETWORKEVENTS netEvents;
int numOfClntSock=0;
int strLen,i;
int posInfo,startIdx;
int clntAdrLen;
char msg[BUF_SIZE];
if(argc!=2){
printf("Usage: %s <port>\n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
ErrorHandling("WSAStartup() error!");
以上是初始化和声明代码,没有什么要特别说明的。
hServSock=socket(PF_INET,SOCK_STREAM,0);
memset(&servAdr,0,sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));
if(bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hServSock,5)==SOCK_ERROR)
ErrorHandlind("listen() error");
newEvent=WSACreateEvent();
if(WSAEventSelect(hServSock,newEvent,FD_ACCEPT)==SOCKET_ERROR)
ErrorHandling("WSAEventSelect() error");
hSockArr[numOfClntSock]=hServSock;
hEventArr[numOfClntSock]=newEvent;
numOfClntSock++;
上述代码创建了用于接收客户端连接请求的服务器端套接字。为了完成监听任务,针对FD_ACCEPT事件调用了WSAEventSelect函数。此处需要注意如下2条语句。
hSockArr[numOfClntSock]=hServSock;
hEventArr[numOfClntSock]=newEvent;
这段代码把通过WSAEventSelect函数连接的套接字和事件对象的句柄分别存人hSockArr和hEventArr数组。也就是说,应该可以通过hSockArr[idx]找到连接到套接字的事件对象,反之,也可以通过hEventArr[idx]找到连接到事件对象的套接字。因此,该示例将套接字和事件对象句柄保存到数组时统一了保存位置。也就有了下列公式。
□ 与hSockArr[n]中的套接字相连的事件对象应保存到hEventArr[n]。
□与hEventArr[n]中的事件对象相连的套接字应保存到hSockArr[n]。
接下来是while循环部分,之前学习的大部分内容都在此。
while(1){
posInfo=WSAWaitForMultipleEvents(numOfClntSock,hEventAdrr,FALSE,WSA_INFINITE,FALSE);
startIdx=posInfo-WSA_WAIT_EVENT_0;
for(i=startIdx;i<numOfClntSock;i++){
int sigEventIdx=WSAWaitForMultipleEvents(1,&hEventArr[i],TRUE,0,FALSE);
if((sigEventIdx==WSA_WAIT_FAILED||sigEventIdx==WSA_WAIT_TIMEOUT)){
continue;
}
else{
sigEventIdx=i;
WSAEnumNetworkEvents(hSockArr[sigEventIdx],hEventArr[sigEventIdx],&netEvent);
if(netEvents.lNetworkEvents& FD_ACCEPT){//判定为请求连接的IO通知
if(netEvents.iErrorCode[FD_ACCEPT_BIT]!=0){
puts("Accept Error");
break;
}
clntAdrLen=sizeof(clntAdr);
hClntSock=accept(hSockArr[sigEventIdx],(SOCKADDR*)&clntAdr,clntAdrLen);
newEvent=WSACreateEvent();//创建对应客户端连接的事件对象
WSAEventSelect(hClntSock,newEvent,FD_READ|FD_CLOSE);
hEventArr[numOfClntSock]=newEvent
hSockArr[numOfClntSock]=hClntSock;
numOfClntSock++;
puts("connected new client...");
}
if(netEvents.lNetworkEvents &FD_READ){//判定为需要读取的IO通知
if(netEvents.iErrorCode[FD_READ_BIT]!=0){
puts("Read Error");
break;
}
strLen=recv(hSockArr[sigEventIdx],msg,sizeof(msg),0);
send(hSockArr[sigEventIdx],msg,strLen,0);
}
if(netEvents.lNetworkEvents &FD_CLOSE){//判定为结束连接的IO通知
if(netEvents.iErrorCode[FD_CLOSE_BIT]!=0){
puts("Close Error");
break;
}
WSACloseEvent(hEventArr[sigEventIdx]);
closesocket(hSockArr[sigEventIdx]);
numOfClntSock--;
CompressSockets(hSockArr,sigEventIdx,numOfClntSock);
CompressEvents(hEventArr,sigEventIdx,numOfClntSock);
}
}
}
}
WSACleanup();
return 0;
}
最后给出上述代码调用的两个函数CompressSockets和CompressEvents的函数声明。
void CompressSockets(SOCKET hSockArr[],int idx,int total){
int i;
for(i=idx;i<total;i++)
hSockArr[i]=hSockArr[i+1];//覆盖已经不用的套接字句柄
}
void CompressEvents(WSAEVENT hEventArr[],int idx,int total){
int i;
for(i=idx;i<total;i++)
hEventArr[i]=hEventArr[i+1];//覆盖已经不用的事件对象句柄
}
void ErrorHandling(char *msg){
fputs(msg,stderr);
fputc('\n',stderr);
exit(1);
}
断开连接并从数组中删除套接字及与之相连接的事件对象时同时调用上述两个函数才能维持套接字和事件对象之间的关系。