并发程序设计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 }
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利用率。