IO Completion Port, Windows上提供的最有效实现高性能server的方式(无论是file server, web server还是别的任何类似大量并发io请求的server),IIS本身就是基于此的。可惜,到目前为止没有一个真正简单的示例。今日便让我打响这第一炮吧。
没有一个简明例程的根源,可以说是因为IoCompletionPort本身的API设计非常糟糕,一个CreateIoCompletionPort包含了太多功能,名称又很confusing,让人云里雾里。所有的的例程,为了便于理解,都把这些让人迷惑的API封装,构造自己的class,但是呢,这样虽然从软件设计角度来说清晰了,但是对于了解IoCompletionPort的使用来说,反而更迷惑了(因为调用被分散到各个class中)。
本文的目的是用最简明的例子来介绍如何使用IO Completion Port。
在此之前,先要说IO Completion Port到底是什么东西-----就是threads pool,一个由Windows自动管理的threads pool. 好,你就需要了解这么多,再说多了就违背了本文的宗旨---提供简明例程。
1. IO Completion Port的程序,大致上可以划分为以下步骤:
2. CreateIOCompletionPort (可以理解为初始化threads pool)
3. 建立threads (就是一般的CreateThread或者_beginthreadex,将第一步所得到的HANDLE作为参数传进去,这个跟一般的thread没任何差别)
4. 开始IO 操作,比如建立SOCKET, bind...
5. 在第一个Async IO之前,将上一步建立的HANDLE(比如socket)绑定到第一步得到的IO Completion Port的HANDLE 上
6. 根据具体情况操作IO
好吧,还是用代码来看比较直接:
先来看主程序:
- int _tmain(int argc, _TCHAR* argv[])
- {
- // argv[1]为ip, argv[2]为port
- // CTcpServer只是一个对socket的简单封装,代码
- // 后面给出
- CTcpServer server(argv[1], argv[2]);
- if (!server.StartListening())
- {
- printf("failed listening/n");
- return 1;
- }
- // initialize IoCompletionPort
- SOCKET& listeningSocket = server.Socket();
- //1. 初始化IO Completion Port
- HANDLE hIocp = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);
- if(hIocp == NULL)
- {
- printf("cannot create io completion port/n");
- return 1;
- }
- //2. 绑定socket到completion port
- //这意思是说,后面所有在这个socket上的async
- // io 操作,都会notify这个completion port的
- //threads
- if (0 == ::CreateIoCompletionPort((HANDLE)listeningSocket, hIocp, (ULONG_PTR)0, 0))
- {
- printf("cannot associate listening socket to io completion port/n");
- return 1;
- }
- //3. 创建threads,将io completion port
- // HANDLE 作为参数传入,这样每个thread
- // 都能在有IO请求时query其status,参见
- // thread的具体代码
- int threadPoolSize = 4;
- HANDLE hWorker;
- for(int i=0;i<threadPoolSize; ++i)
- {
- hWorker = CreateThread(NULL, 0, WorkerThreadFunc, (LPVOID)hIocp, 0, NULL);
- CloseHandle(hWorker);
- }
- //4. 等待新连接,因为我们不要busy loop, 所以
- // 每个TCP连接需要等待并检查
- // FD_ACCEPT event,然后用AcceptEx
- while(true)
- {
- printf("waiting for connection/n");
- if (server.WaitForAcceptEvent(10000))
- {
- // 只管Accept了,至于Recv/Send由上面建立的
- // thread来负责,后面会说thread的功能
- server.AcceptNewConnection();
- }
- }
- return 1;
- }
然后先来看thread的实现. IO Completion Port的 thread因为是放入一个thread pool中,所以每个thread是“通用”的,换句话说,每个thread要能够完成多种功能,用伪代码来说是这样:
Wait For IO Notification; ---> 等待比如Socket上的一个event,至于是什么event先不管。无妨想象成interrupt,也比较类似WaitForSingleObject,总之thread在这时候是sleep的
Check IO status Operation; ----> 检查IO状态,更关键是看到底是什么Event
switch (event.status)
{
case ACCEPT: ...
case READ: ...
case WRITE: ...
case WHATEVER: ...
}
所以要清楚,io completion port的thread并不是去给每个read或者write建一个thread(也不是不可以,不过就是画蛇添足多此一举),而是依靠自定义的Overlapped结构来判断到底对IO进行什么操作。还是看下面的源代码吧。
- DWORD WINAPI WorkerThreadFunc(LPVOID lpParam)
- {
- ULONG_PTR *PerHandleKey;
- WSAOVERLAPPED *pOverlap;
- OVERLAPPEDPLUS *pOverlapPlus,
- *newolp;
- DWORD dwBytesXfered;
- int ret;
- HANDLE hIocp = (HANDLE)lpParam;
- while (true)
- {
- //这里查询IO 状态
- ret = GetQueuedCompletionStatus(
- hIocp,
- &dwBytesXfered,
- (PULONG_PTR)&PerHandleKey,
- &pOverlap,
- INFINITE);
- if (ret == 0)
- {
- // Operation failed
- printf("cannot get queued completion status/n");
- continue;
- }
- //OVERLAPPEDPLUS是我们自己定义的data structure,一般把正常的
- //OVERLAPPED作为第一个field,所以指针是指向同样地址,后面会给出具
- //体定义. CONTAINING_RECORD是一个标准win32 macro
- pOverlapPlus = CONTAINING_RECORD(pOverlap, OVERLAPPEDPLUS, overlapped);
- // OP_ACCEPT也是我们自己定义的 value,只是一个标识:
- // #define OP_ACCEPT 1
- switch (pOverlapPlus->OpCode)
- {
- case OP_ACCEPT: