并发程序设计6:IOCP
Posted yuanwebpage
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发程序设计6:IOCP相关的知识,希望对你有一定的参考价值。
本节记录Windows下与epoll类似的机制IOCP(input outpout completion port)。对于单台电脑的多TCP连接请求,IOCP和epoll是比较好的选择。因为IOCP会用到重叠IO的一些函数,因此先记录重叠IO。
1. 重叠IO
1.1 关键函数
由于IOCP的使用会用到较多重叠IO相关的函数,先记录一下重叠IO。所谓重叠IO,就是在异步IO时,由于发送/接收函数调用后立即返回,那么单线程就能同时收发多个数据,表现出来就是IO同时向不同套接字同时发送,如下图所示:
重叠IO模型
为了实现重叠IO,必须创建适用于重叠IO的套接字。
#include <winsock2.h> SOCKET WSASocket(int af,int type,int protocol,LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g,DWORD DWFlags); //创建适用于重叠IO的套接字示例: WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
创建了重叠IO的套接字还要能使用重叠IO的发送和接收函数
#include <winsock2.h> int WSASend ( SOCKET s, //发送的套接字 LPWSABUF lpBuffers, //存储待发送数据的数组 DWORD dwBufferCount, //数组个数,一般为1
LPDWORD lpNumberofBytesSent, LPWSAOVERLAPPED lpOverlapped, //非常重要 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine )
int WSARecv
(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberofByteRecved,
LPWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
)
下面来说明上面比较重要的参数
第二个参数lpBuffers存放发送和接收数据的数组,结构体定义如下:
typedef struct __WSABUF { u_long len; //数组长度 char FAR* buf; } WSABUF,* LPWSABUF
第四个参数代表收发的具体字节数,由于是异步收发,如果调用能当场完成,存储的就是实际收发的字节数;如果不能当场完成,会返回SOCKET_ERROR,此时再调用以下函数:
WSAEVENT event=WSACreateEvent();
WSAOVERLAPPED overlapped;
overlapped.hEvent=event; //通过该事件获取返回通知
if(WSASend(...)==SOCKET_ERROR) { if(WSAGetLastError()==WSA_IO_PENDING) { WSAWaitForMultipleEvents(1,&event,TRUE,WSA_IFINITE,FALSE); WSAGetOverlappedResult(sock,&overlapped,&sentbyte,FALSE,NULL); //sentbyte此时存储的为实际发送字节,接收与此相同,具体示例查看完整代码 } }
第六个参数lpOverlapped为LPWSAOVERLAPPED类型结构体指针,该结构体有个关键变量WSAEVENT,通过该事件可以获取收发完成的通知。如果调用函数时该变量NULL,将会以阻塞方式工作。第七个参数也用于获取收发完成的通知。
1.2 获取收发完成通知
获取收发完成的通知有两种方式,第一种为通过WSASend和WSARecv第六个参数lpOverlapped中的WSAEVENT获取。即收发完成,lpOverlapped的事件会置为signaled状态,调用WSAWaitForMultipleEvents()即可,上面在介绍获取收发实际字节数时采用的即时此方法。
下面主要介绍利用最后一个参数的方法。WSASend和WSARecv指定一个函数来验证收发完成情况,一旦收发完成,就调用该函数。为了不在主函数执行重要任务时被打断,规定只有在alertable_wait状态才能调用验证收发完成的函数。使程序进入alertable_wait有以下几个函数:
WaitForSingleObjectEx
WaitForMultipleObjectsEx
WSAWaitForMultipleEvents
SleepEx
最后利用重叠IO完成一个回声服务器服务器端函数的编写,代码如下:
1 #include <iostream> 2 #include <stdlib.h> 3 #include <Winsock2.h> 4 #include <stdio.h> 5 #include <WS2tcpip.h> 6 #define BUF_SIZE 50 7 #pragma comment(lib,"ws2_32.lib") 8 9 10 void CALLBACK ReadCmpRoutine(DWORD dwError,DWORD szRecvBytes,LPWSAOVERLAPPED lpoverlapped,DWORD flags); //验证接收完成的函数,传入参数固定 11 void CALLBACK WriteCmpRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpoverlapped, DWORD flags); 12 void error_handle(const char* msg) 13 { 14 printf("%s ", msg); 15 exit(0); 16 } 17 18 typedef struct 19 { 20 SOCKET sock; 21 char buf[BUF_SIZE]; 22 WSABUF wsabuf; 23 }IODATA,*LPIODATA; 24 25 26 int main() 27 { 28 SOCKET servsock, clntsock; 29 SOCKADDR_IN servaddr, clntaddr; 30 31 WSADATA wsadata; 32 if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) 33 error_handle("Startup() error"); 34 35 u_long mode = 1; 36 servsock = WSASocket(PF_INET, SOCK_STREAM, 0,NULL,0,WSA_FLAG_OVERLAPPED); 37 ioctlsocket(servsock,FIONBIO,&mode); //创建非阻塞套接字 38 39 memset(&servaddr, 0, sizeof(servaddr)); 40 servaddr.sin_family = AF_INET; 41 const char* host = "192.168.0.105"; //本机主机IP 42 inet_pton(AF_INET, host, (void*)&servaddr.sin_addr); 43 servaddr.sin_port = htons(8000); //由于VS版本检查,一些早期的函数会报错 44 45 if (bind(servsock, (SOCKADDR*)&servaddr, sizeof(sockaddr)) == SOCKET_ERROR) 46 error_handle("bind() error"); 47 if (listen(servsock, 5) == SOCKET_ERROR) 48 error_handle("listen() error"); 49 int clntlen = sizeof(clntaddr); 50 51 LPWSAOVERLAPPED overlapped; //overlapped指针 52 LPIODATA IOData; 53 DWORD recvBytes, flaginfo=0; 54 55 while (1) 56 { 57 SleepEx(100, TRUE); //进入alertable_wait状态 58 clntsock = accept(servsock, (SOCKADDR*)&clntaddr, &clntlen); 59 if (clntsock == INVALID_SOCKET) 60 { 61 if (WSAGetLastError() == WSAEWOULDBLOCK) //当前没有连接请求 62 continue; 63 else 64 error_handle("accept() error"); 65 } 66 overlapped = (LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED)); 67 IOData = (LPIODATA)malloc(sizeof(IODATA)); 68 IOData->sock =clntsock; 69 (IOData->wsabuf).buf = IOData->buf; 70 (IOData->wsabuf).len = BUF_SIZE; //调用WSASend和WSARecv函数必须的结构体声明 71 overlapped->hEvent =(HANDLE)IOData;//因为不采用事件作为接收完成的通知,此处利用hEvent传递其他数据 72 73 WSARecv(clntsock, &(IOData->wsabuf), 1, &recvBytes, &flaginfo, overlapped, ReadCmpRoutine); 74 } 75 closesocket(clntsock); 76 closesocket(servsock); 77 WSACleanup(); 78 return 0; 79 } 80 81 void CALLBACK ReadCmpRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpoverlapped, DWORD flags) 82 { 83 LPIODATA IOData = (LPIODATA)lpoverlapped->hEvent; //获取传递过来的数据 84 DWORD SentBytes; 85 if (szRecvBytes == 0) //关闭套接字请求 86 { 87 closesocket(IOData->sock); 88 free(IOData); 89 free(lpoverlapped); 90 } 91 else //发送回客户端 92 { 93 (IOData->wsabuf).len = szRecvBytes; 94 WSASend(IOData->sock, &(IOData->wsabuf), 1, &SentBytes, 0, lpoverlapped, WriteCmpRoutine); 95 } 96 } 97 98 void CALLBACK WriteCmpRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpoverlapped, DWORD flags) 99 { 100 LPIODATA IOData = (LPIODATA)lpoverlapped->hEvent; //获取传递过来的数据 101 DWORD flaginfo = 0; 102 DWORD recvBytes; 103 WSARecv(IOData->sock, &(IOData->wsabuf), 1, &recvBytes, &flaginfo, lpoverlapped, ReadCmpRoutine); 104 }
先简要说明一下上面代码的逻辑:创建非阻塞重复IO的套接字之后,开始不断监测连接请求,当有连接请求时,调用WSARecv(),此时注册了ReadCmpRoutine函数,因此接收完成就会跳入ReadCmpRoutine函数,然后该函数又调用WSASend()把数据发送回去。在发送回去的同时,注册WriteCmpRoutine函数,该函数又调用WSARecv开始接收数据,并注册ReadCmpRoutine。因此接收到数据之后,又会执行ReadCmpRoutine函数,如此往复。
注:
(1) 66-71行申请并初始化相关指针,通过将指针赋值给overlapped->hEvent进行指针传递,这样就将客户端套接字和接收消息的数组传给了ReadCmpRoutine函数,然后客户端套接字和接收消息的数组就在ReadCmpRoutine与WriteCmpRoutine函数之间传递;
(2) 93行接收到消息后,将wsabuf的长度设置为实际接收数据的长度再发送,那么发送的长度就和接收长度一致了,否则wsabuf接收到的数据后面的乱码也会发送;
(3) 重复调用accept和SleepEx是影响该程序性能的主要原因。
以上是关于并发程序设计6:IOCP的主要内容,如果未能解决你的问题,请参考以下文章