并发程序设计5:Windows下异步通知IO

PS:在开始Windows下异步IO之前,需要了解一些重要概念。

(1)内核对象,句柄和线程ID:操作系统为了记录并管理某一类资源,如进程,线程,文件,会创建记录相关资源信息的内部数据结构,称为内核对象(如管理进程的进程控制块PCB);句柄是Windows下为了管理内核对象提供的可访问内核对象的一种数据(此处非严格定义),类似于Linux下的文件描述符,;线程ID是区分不同线程的标志号。不同进程中句柄可能重复出现,但是线程ID是唯一的

(2)内核对象的两个状态:在多线程编程中常用到两个函数WaitForSingleObject()和WaitForMultipleObjects()以等待线程结束之后主函数才终止。这是因为线程结束之后,其内核对象会进入signaled状态,而上述两个函数就是在有signaled状态时才返回。即内核有signaled状态和non-signaled两种状态。WaitForSingleObject()和WaitForMultipleObjects()通常会等待signaled状态发生(此函数只要是涉及到内核状态的都会用到,如同步时采用的信号量和互斥量,在互斥量未使用或信号量大于零时都处于signaled状态)。有些函数等待到signaled状态后会自动将其置为non-signaled,称为"auto-reset"模式;而等到signaled之后不置为non-signaled称为“manual-reset"模式。

 

1. Windows下异步IO的处理

  Windows下的异步通知IO类比到Linux下就是实现跟select类似的功能。只是select是同步通知IO(注册和结果在调用函数时同时发生),Windows下是异步通知IO。关于同步和异步的概念,以send和recv为例,见下图:

          

 

                                           同步IO                                                                                                   异步IO

即同步和异步关键看函数返回时刻与函数动作完成时刻是否一致。同步的情况下,在函数执行时就要一直阻塞。

 

Windows下实现异步通知IO有三个主要步骤:注册,查看IO通知,查看具体消息类型。每一步对应着一个函数,主要函数如下:

int WSAEventSelect(    
    SOCKET s,                 //套接字  
    WSAEVENT hEventObject,    //网络事件对象  
    long lNetworkEvents       //需要关注的事件  
);

//上面的函数对应着注册,第二个事件结构体对应查看IO通知(通过事件查看),第三个结构体对应着具体消息类型,主要类型有:
FD_READ  FD_WRITE  FD_ACCEPT  FD_CLOSE

下面给出一个调用上面函数的示例:

WSAEVENT event = WSACreateEvent();  //创建事件,此函数的创建处于non-signaled的"manual-reset"事件
if (WSAEventSelect(servsock, event, FD_ACCEPT) == SOCKET_ERROR) //服务器套接字注册接收请求
        printf("WSAEventSelect() error");

 

注册了套接字监测的消息类型后,该套接字的信息就注册到了操作系统中,因此无需重复注册。然后是异步接收IO通知,主要函数如下:

DWORD WSAWaitForMultipleEvents(DWORD cEvents,const WSAEVENT FAR *lphEvents, BOOL fWaitAll,DWORD dwTimeout, BOOL fAlertable);  

cEvents:指定了事件对象数组里边的个数,即可以监听的最多套接字,最大值为WSA_MAXIMUM_WAIT_EVENTS(一般是64);
lphEvents:事件对象数组 ,一般每个套接字对应一个事件,监听多个套接字需要一个存储事件的数组;
fWaitAll: 等待类型,TRUE表示要数组里全部有信号才返回,FALSE表示至少有一个就返回;
dwTimeout:等待超时时间
fAlertable:当为true时进入alertable wait状态,一般false
返回值:返回值减去常量WSA_WAIT_EVENT_0,得到转变为signaled状态事件对应句柄的开始索引

只调用上面函数一次是无法得到所有signaled事件的,此时需要用到事件的"manual-reset"特性。即调用一次WSAWaitForMultipleEvents()事件检查函数不会将事件置为non-signaled,因此可以重复调用以变量所有发生的事件。具体操作流程如下:

int pos,startidx;
pos=WSAWaitForMultipleEvents(socknum, events, FALSE, WSA_INFINITE, FALSE);
startidx=pos-WSA_WAIT_EVENT_0;
for(int i=startidx;i<socknum;i++)
{
    int eventidx = WSAWaitForMultipleEvents(1, &events[i], TRUE, 0, FALSE); //挨个事件检测,因为多次调用了该函数,这也说明了事件必须为manual-reset
}

 

最后对于每个signaled事件的具体消息类型,再调用如下函数查看:

int WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents);
s:指定的socket hEventObject:套接字对应的事件对象 lpNetworkEvents:一个WSANETWORKEVENTS结构体,该结构体有两个变量
long lNetworkEvents:表明具体事件类型,如想查看是否是accept事件,采用如下代码 if(netevent.lNetworkEvents & FD_ACCEPT) //发生accept事件 iErrorCode:检查是否是对应事件发生错误,比如发生了accept事件,再调用 if(netevent.iErrorCode[FD_ACCEPT_BIT]!=0) //accept error

调用WSAEnumNetworkEvents()后,相应事件会从signaled状态置为non-signaled状态,不需要再显式操作了。

 

到此为止,需要用到的所有函数(其实就三个)就讲解完了,下面看回声服务器服务器端的完整代码:

  1 #include <Winsock2.h>
  2 #include <stdio.h>
  3 #include <WS2tcpip.h>
  4 #pragma comment(lib,"ws2_32.lib")  //在visual studio中使用网络编程需加此句
  5 
  6 
  7 void CompressSocket(SOCKET sock[],int idx,int total); //移出关闭的TCP套接字
  8 void CompressEvent(WSAEVENT events[], int idx, int total); //移出关闭的套接字的事件
  9 
 10 int main()
 11 {
 12     SOCKET sock[WSA_MAXIMUM_WAIT_EVENTS];  //创建保存所有套接字的数组
 13     WSAEVENT events[WSA_MAXIMUM_WAIT_EVENTS]; //保存所有套接字对应事件的数组
 14     int socknum = 0; //记录套接字数量
 15     int pos,startidx;
 16     char msg[50];
 17 
 18     WSADATA wsadata;
 19     if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
 20         printf("WSAStartup() error\n");
 21 
 22     SOCKET servsock, clntsock;
 23     SOCKADDR_IN servaddr, clntaddr;
 24     servsock = socket(PF_INET, SOCK_STREAM, 0);
 25     memset(&servaddr, 0, sizeof(servaddr));
 26     servaddr.sin_family = AF_INET;
 27     const char* host = "192.168.0.105"; //本机主机IP
 28     inet_pton(AF_INET,host, (void*)&servaddr.sin_addr);
 29     servaddr.sin_port = htons(8000);  //由于VS版本检查,一些早期的函数会报错
 30 
 31     if (bind(servsock, (SOCKADDR*)&servaddr, sizeof(sockaddr)) == SOCKET_ERROR)
 32         printf("bind() error");
 33     if (listen(servsock, 5) == SOCKET_ERROR)
 34         printf("listen() error");
 35 
 36     WSAEVENT event = WSACreateEvent(); //创建non-signaled的"manual-reset"的事件
 37     if (WSAEventSelect(servsock, event, FD_ACCEPT) == SOCKET_ERROR)
 38         printf("WSAEventSelect() error");
 39 
 40     sock[socknum] = servsock;
 41     events[socknum++] = event;  //保存套接字及其对应事件
 42 
 43     WSANETWORKEVENTS netevent;  //用于查看具体消息类型时会用到
 44     while (1)
 45     {
 46         pos=WSAWaitForMultipleEvents(socknum, events, FALSE, WSA_INFINITE, FALSE);
 47         startidx = pos - WSA_WAIT_EVENT_0; //获取转变为signal状态的事件的索引
 48         for (int i = startidx; i < socknum; i++)  //查看是哪些套接字具体产生了变换
 49         {
 50             int eventidx = WSAWaitForMultipleEvents(1, &events[i], TRUE, 0, FALSE);
 51             if (eventidx == WSA_WAIT_FAILED || eventidx == WSA_WAIT_TIMEOUT)
 52                 continue;
 53             else
 54             {
 55                 eventidx = i;
 56                 WSAEnumNetworkEvents(sock[eventidx], events[eventidx],&netevent);
 57                 if (netevent.lNetworkEvents & FD_ACCEPT) //连接请求
 58                 {
 59                     if (netevent.iErrorCode[FD_ACCEPT_BIT] != 0)
 60                     {
 61                         printf("accept() error"); 
 62                         break;
 63                     }
 64                     int clntlen = sizeof(clntaddr);
 65                     clntsock = accept(sock[eventidx], (SOCKADDR*) & clntaddr, &clntlen);  //接收连接请求
 66                     event = WSACreateEvent();
 67                     WSAEventSelect(clntsock, event, FD_READ | FD_CLOSE);
 68                     sock[socknum] = clntsock;
 69                     events[socknum++] = event;
 70                 }
 71                 if (netevent.lNetworkEvents & FD_READ)  //接收数据请求
 72                 {
 73                     if (netevent.iErrorCode[FD_READ_BIT] != 0)
 74                     {
 75                         printf("Read() error");
 76                         break;
 77                     }
 78                     int strlen = recv(sock[eventidx], msg, sizeof(msg), 0);
 79                     send(sock[eventidx], msg, strlen, 0);
 80                 }
 81                 if (netevent.lNetworkEvents & FD_CLOSE)  //关闭套接字请求
 82                 {
 83                     if (netevent.iErrorCode[FD_CLOSE_BIT] != 0)
 84                     {
 85                         printf("close() error");
 86                         break;
 87                     }
 88                     WSACloseEvent(events[eventidx]);
 89                     closesocket(sock[eventidx]);
 90                     socknum--;
 91                     CompressSocket(sock, eventidx, socknum);
 92                     CompressEvent(events, eventidx, socknum);
 93                 }
 94 
 95             }
 96         }
 97     }
 98     closesocket(servsock);
 99     WSACleanup();
100     return 0;
101 }
102 
103 void CompressSocket(SOCKET sock[], int idx, int total)
104 {
105     for (int i = idx; i < total; i++)
106         sock[i] = sock[i+1];
107 }
108 
109 void CompressEvent(WSAEVENT events[], int idx, int total)
110 {
111     for (int i = idx; i < total; i++)
112         events[i] = events[i + 1];
113 }
View Code

 

 

2. WSAEventSelect, epoll, select比较

相同点:都能同时监测多个IO变换

不同点:WSAEventSelect叫做异步通知IO,即注册和通知是异步的,这跟epoll比较像,只需要一次注册到操作系统,然后在需要时再调用查看具体IO是否发生变化,所以epoll也是异步通知。而select函数每次调用时都需要注册,注册的同时监听IO是否变化,且一直阻塞到有IO变化,即注册和通知是同步的,所以select是同步通知IO。

注意:

(1)需要注意的是此时异步是通知的异步而不是IO异步,IO调用的recv和send函数都是在读写完成时才返回,是同步IO

(2)由于每个WSAEventSelect能监听的最大套接字数很有限,,只有64个,因此超过64个需要配合多线程工作。

(3)异步IO的好处是执行完函数之后立即返回,不会阻塞,因此可以提高CPU利用率。

 

posted @ 2020-03-26 10:18  晨枫1  阅读(328)  评论(0编辑  收藏  举报