用C++实现HTTP服务器 - 处理完成端口模型(IOCP)的超时问题

如何处理完成端口模型(IOCP)的超时问题.

作者: 阙荣文  2011/7/12


前言
完成端口(IOCP)是所有Windows I/O模型中最复杂,也是性能最好的一种.在关于IOCP的编程中,难点之一就是超时控制.
以下以HTTP服务器程序为例说一说.

其实超时控制也不是很难,问题是Windows的IOCP模型本身并没有提供关于超时的支持(也行以后的版本会有?),所以一切都要有程序员来完成.并且超时控制对于服务器程序来说是必须的: HTTP服务器程序对一个新的客户端连接需要在完成端口上投递一个 WSARecv() 操作,以接收客户端的请求,如果这个客户端连接一直不发送数据(所谓的恶意连接)那么投递的这个请求永远不会从完成端口队列中返回,占用了服务器资源.如果有大量的恶意连接,服务很快就会不堪重负.所以服务器程序必须为投递给完成端口的请求设置一个超时时间.

那么如何做超时控制呢?

一般有两种思路:
1. 创建一个单独的线程,每隔一段时间,轮询一次所有的I/O请求队列,发现有超时则取消这个I/O投递请求.
优点:
简单,一个线程,一个循环就可以了.

缺点:
精度和效率难以两全.比如设定的超时时间为60秒,如果每60秒轮询一次所有套接字,那么有可能出现 (60 - 1 + 60)秒后才被检测到的超时;而如果提高轮询频率,那么性能又会受到影响:轮询时,肯定要对套接字队列加锁.所以设置恰当的轮询间隔是个两难的选择.另外,有些程序采用 min heap 最小堆算法对轮询进行优化可以进一步提高效率.

2. 为每一个I/O投递请求单独设定一个定时器.
优点:
精度高, Windows定时器大致能保证15毫秒左右的精度.

缺点:
资源消耗大,很明显如果有大量的连接,就需要同样数量的定时器.幸好,针对需要大量定时器的应用,Windows提供了 Timer Queue,相对于SetTimer()创建的定时器对象,用CreateTimerQueueTimer()创建的是经过优化的轻量级的对象,并且系统内部对Timer Queue也有优化,比如用线程池内的线程执行超时回调函数等.一个进程最多可以创建多少个 TimerQueueTimer也还不清楚,我在MSDN上也没找到相关的说明,这可能成为服务支持的最大连接数的瓶颈.(我在自己机器上(Win7 Home Basic + VS2010)测试过,第一次运行附录3的代码机器几乎失去响应,但是没出错.第二次加了几个条件断点,反正到3万个Timer的时候,超时函数都被执行了,机器响应还很快.所以TimerQueueTimer的数量应该没有限制或者是一个很大的数.没权威资料,还是不确定.)

两种方法都是可以的,具体怎么做还是取决于程序要求.
我在设计Que's HTTP Server 时,用的是Timer Queue,根据需要,为每个socket都分配了两个TimerQueueTimer,一个设置会话超时(即一个socket最长可以和服务器保持多少时间的连接),一个设定为死连接超时,如果一个连接在指定的时间内,既没有发送数据也没有接收数据,就会被判定为是死连接而被关闭,服务器在每次接收或发送数据成功时,都调用ChangeTimerQueueTimer()重置该定时器.只可惜条件有限,没有在大压力环境下测试过.只在本机上跑过几天(极限200个左右的连接,80MB/s左右的带宽,每秒调用几百次ChangeTimerQueueTimer()重置定时器,超时误差在8到15个毫秒左右,完全可以接受.)

HTTP服务器编程中几个需要注意的点

1. 如果一个IO请求正在处理中,则一定要确保传人的 LPWSAOVERLAPPED 指针的有效性.这是在程序设计时无条件要保证的,否则肯定会崩溃.至于怎么保证这点,是程序员的事,而不是IOCP的问题.要释放LPWSAOVERLAPPED 指向的结构只能等到 I/O 操作从完成端口队列返回之后才可以进行. 即只有在GetQueuedCompletionStatus()返回之后.如果在多个I/O请求中用了同一个 WSAOVERLAPPED 结构,可以设置一个引用计数,每次从GetQueuedCompletionStatus()返回计数减一,到零时可以释放(最好避免这种设计).

2. 如何取消已经投递的I/O请求?

答案是没办法取消.当然,关闭完成端口的句柄可以取消所有的I/O请求,但是这只适用于程序退出时.不过,针对HTTP服务器,关闭套接字,可以使该套接字相关的所有I/O请求都被标记为失败,并从 GetQueuedCompletionStatus() 中返回(返回值不一定为FALSE,详见下节)).这样,只要在超时回调函数中关闭对应的套接字,不释放任何资源,完成端口服务线程就是从 GetQueuedCompletionStatus()返回,在确保这个套接字对应的所有I/O请求都从完成端口队列中清除后,就可以回收资源了(主要是投递请求时传人的 LPWSAOVERLAPPED 指针,现在可以放心大胆的删除了).

2011-12-09更正: 用 CancelIoEx(hSocket, NULL) 可以取消一个套接字的所有未决的I/O操作.当然,如上文说的那样直接关闭套接字句柄也会导致所有未决I/O操作失败从而达到"取消"一样的效果.


3. GetQueuedCompletionStatus()函数返回值研究(参考MSDN),原型如下:
BOOL WINAPI GetQueuedCompletionStatus(
  __in   HANDLE CompletionPort,
  __out  LPDWORD lpNumberOfBytes,
  __out  PULONG_PTR lpCompletionKey,
  __out  LPOVERLAPPED *lpOverlapped,
  __in   DWORD dwMilliseconds
);

(1) 如果I/O操作(WSASend() / WSARecv())成功完成,那么返回值为TRUE,并且 lpNumberOfBytes 为已传送的字节数.注意,已传送的字节数有可能小于你请求发送/接收的字节数.
(2) 如果对方关闭了套接字,那么有两种情况
(a) I/O操作已经完成了一部分,比如WSASend()请求发送1K字节,并且其中的512字节已经发送完成,则返回值为TRUE, lpNumberOfBytes 指向的值为512, lpOverlapped 有效.
(b) I/O操作没有完成,那么返回值为FALSE, lpNumberOfBytes 指向的值为0,
  • 1
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
IOCP是一种高效的I/O多路复用模型,适用于Windows系统上的网络编程。下面是一个简单的使用IOCP模型的C代码示例,实现一个简单的回显服务器: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #include <windows.h> #define BUFSIZE 1024 typedef struct _PER_HANDLE_DATA { SOCKET Socket; SOCKADDR_STORAGE ClientAddr; } PER_HANDLE_DATA, *LPPER_HANDLE_DATA; typedef struct _PER_IO_DATA { OVERLAPPED Overlapped; WSABUF DataBuf; char Buffer[BUFSIZE]; int OperationType; } PER_IO_DATA, *LPPER_IO_DATA; DWORD WINAPI WorkerThread(LPVOID lpParam); int main(int argc, char* argv[]) { WSADATA wsaData; SOCKET ListenSocket, AcceptSocket; SOCKADDR_STORAGE LocalAddr, ClientAddr; DWORD Flags; LPPER_HANDLE_DATA PerHandleData; LPPER_IO_DATA PerIoData; DWORD RecvBytes, SendBytes; DWORD BytesTransferred; DWORD ThreadId; HANDLE CompletionPort, ThreadHandle; int i; // 初始化Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed with error: %d\n", WSAGetLastError()); return 1; } // 创建完成端口 CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); if (CompletionPort == NULL) { printf("CreateIoCompletionPort failed with error: %d\n", GetLastError()); return 1; } // 创建监听套接字 ListenSocket = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); if (ListenSocket == INVALID_SOCKET) { printf("socket failed with error: %d\n", WSAGetLastError()); return 1; } // 绑定地址并监听 memset(&LocalAddr, 0, sizeof(LocalAddr)); LocalAddr.ss_family = AF_INET6; ((SOCKADDR_IN6*)&LocalAddr)->sin6_port = htons(12345); ((SOCKADDR_IN6*)&LocalAddr)->sin6_addr = in6addr_any; if (bind(ListenSocket, (SOCKADDR*)&LocalAddr, sizeof(LocalAddr)) == SOCKET_ERROR) { printf("bind failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); return 1; } if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) { printf("listen failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); return 1; } // 创建工作线程 for (i = 0; i < 2; i++) { ThreadHandle = CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &ThreadId); if (ThreadHandle == NULL) { printf("CreateThread failed with error: %d\n", GetLastError()); return 1; } CloseHandle(ThreadHandle); } // 接受连接并关联到完成端口 while (1) { AcceptSocket = accept(ListenSocket, (SOCKADDR*)&ClientAddr, NULL); if (AcceptSocket == INVALID_SOCKET) { printf("accept failed with error: %d\n", WS

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值