并发程序设计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 }
View Code

   先简要说明一下上面代码的逻辑:创建非阻塞重复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的主要内容,如果未能解决你的问题,请参考以下文章

Visual Studio并发Qpar优化效果

IOCP关键部分设计

IOCP模型

IOCP 是不是创建自己的线程?

分享我写的IOCP:源码+思路(转载)

HttpServer: 基于IOCP模型且集成Openssl的轻量级高性能web服务器