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[])
- {
-
-
-
-
- CTcpServer server(argv[1], argv[2]);
- if (!server.StartListening())
- {
- printf("failed listening/n");
- return 1;
- }
-
-
- SOCKET& listeningSocket = server.Socket();
-
- HANDLE hIocp = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);
-
- if(hIocp == NULL)
- {
- printf("cannot create io completion port/n");
- return 1;
- }
-
-
-
-
-
- if (0 == ::CreateIoCompletionPort((HANDLE)listeningSocket, hIocp, (ULONG_PTR)0, 0))
- {
- printf("cannot associate listening socket to io completion port/n");
- return 1;
- }
-
-
-
-
-
- int threadPoolSize = 4;
-
- HANDLE hWorker;
-
- for(int i=0;i<threadPoolSize; ++i)
- {
- hWorker = CreateThread(NULL, 0, WorkerThreadFunc, (LPVOID)hIocp, 0, NULL);
- CloseHandle(hWorker);
- }
-
-
-
-
- while(true)
- {
- printf("waiting for connection/n");
- if (server.WaitForAcceptEvent(10000))
- {
-
-
- 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)
- {
-
- ret = GetQueuedCompletionStatus(
- hIocp,
- &dwBytesXfered,
- (PULONG_PTR)&PerHandleKey,
- &pOverlap,
- INFINITE);
- if (ret == 0)
- {
-
- printf("cannot get queued completion status/n");
- continue;
- }
-
-
-
-
-
- pOverlapPlus = CONTAINING_RECORD(pOverlap, OVERLAPPEDPLUS, overlapped);
-
-
-
-
- switch (pOverlapPlus->OpCode)
- {
- case OP_ACCEPT:
- printf("accepted/n");
-
-
- free(pOverlapPlus);
- break;
- }
- }
- }
上面的OVERLAPPEDPLUS是一个很重要的自定义结构,可以把你要的东西全部放里面:D
- #pragma once
-
- #include <winsock2.h>
- #include <ws2tcpip.h>
- #include <iphlpapi.h>
- #include <netioapi.h>
- #include <Icmpapi.h>
- #include <mstcpip.h>
- #include <Mswsock.h>
- #include <stdlib.h>
- #include <Windows.h>
- #include <process.h>
- #include <stdio.h>
- #include <stdlib.h>
-
-
- #define DATA_BUFSIZE 4096
-
- typedef struct _OVERLAPPEDPLUS {
- WSAOVERLAPPED overlapped;
- SOCKET serverSock;
- SOCKET clientSock;
- int OpCode;
- WSABUF wbuf;
- DWORD Bytes;
- DWORD Flags;
-
- } OVERLAPPEDPLUS;
-
- #define OP_READ 0
- #define OP_WRITE 1
- #define OP_ACCEPT 2
好,最后就是开始提到的CTcpServer这个对socket的封装,这个类并不复杂,除开封装socket之外(无非就是socket(...),bind,listen),最重要的是检查FD_ACCEPT event,然后调用AcceptEx(如果你去看MSDN,会发现AcceptEx的示例并没有用AcceptEx,汗...),当然,你可以用类似:while(true) { AcceptEx(...);}, 但是这种busy loop显然是极其恶劣的。
先来看.h
- #pragma once
- #include "common.h"
- #include <string>
- using namespace std;
-
- class CTcpServer
- {
- WSADATA wsd;
- SOCKET m_ListeningSocket;
- ADDRINFOW* m_pAddrInfo;
- HANDLE m_AcceptEvent;
-
- public:
- CTcpServer(PCWSTR pIPAddress, PCWSTR port);
-
- ~CTcpServer();
-
- SOCKET& Socket()
- {
- return m_ListeningSocket;
- }
-
- bool StartListening();
-
- int AddressFamily()
- {
- return m_pAddrInfo->ai_family;
- }
-
- int SocketType()
- {
- return m_pAddrInfo->ai_socktype;
- }
-
- int Protocol()
- {
- return m_pAddrInfo->ai_protocol;
- }
-
- BOOL WaitForAcceptEvent(DWORD timeout);
-
- BOOL AcceptNewConnection();
-
- };
没什么稀奇的,如我所说,关键在于WaitForAcceptEvent()上,下面的代码是具体实现:
- #include "stdafx.h"
-
- #include "TcpServer.h"
- #include "common.h"
-
-
- CTcpServer::CTcpServer(PCWSTR pAddress, PCWSTR port)
- :m_ListeningSocket(INVALID_SOCKET), m_pAddrInfo(NULL), m_AcceptEvent(NULL)
- {
- int rc = WSAStartup(MAKEWORD(2, 2), &wsd);
- if (rc != 0) {
- wprintf(L"WSAStartup failed/n");
- throw new exception();
- }
-
-
- ADDRINFOW *result = NULL,
- *ptr = NULL,
- hints = {0};
-
- hints.ai_family = AF_INET;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_protocol = IPPROTO_TCP;
-
- rc =::GetAddrInfoW(pAddress, port, &hints, &m_pAddrInfo);
- if (rc != 0) {
- printf("getaddrinfo failed: %d/n", rc );
- throw new exception();
- }
-
- m_ListeningSocket = WSASocket(AF_INET,
- SOCK_STREAM,
- IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
-
- }
-
-
- CTcpServer::~CTcpServer()
- {
- if (m_ListeningSocket != INVALID_SOCKET)
- {
- closesocket(m_ListeningSocket);
- }
-
- if (m_AcceptEvent != NULL && m_AcceptEvent != INVALID_HANDLE_VALUE)
- {
- ::CloseHandle(m_AcceptEvent);
- }
-
- WSACleanup();
- }
-
-
- bool CTcpServer::StartListening()
- {
- if (bind(m_ListeningSocket, m_pAddrInfo->ai_addr, m_pAddrInfo->ai_addrlen) == SOCKET_ERROR)
- {
- return false;
- }
-
- if (listen(m_ListeningSocket, 1 ) == SOCKET_ERROR)
- {
- printf("failed listening");
- return false;
- }
-
-
-
- m_AcceptEvent = WSACreateEvent();
- if(SOCKET_ERROR == WSAEventSelect(m_ListeningSocket, m_AcceptEvent, FD_ACCEPT))
- {
- printf("WSAEventSelect failed: %d/n", WSAGetLastError());
- return false;
- }
-
- return true;
- }
-
-
-
-
- BOOL CTcpServer::WaitForAcceptEvent(DWORD timeout)
- {
- DWORD ret = WSAWaitForMultipleEvents(1, &m_AcceptEvent, FALSE, timeout, FALSE);
- if (WSA_WAIT_TIMEOUT == ret || WSA_WAIT_FAILED == ret)
- {
- printf("wait for accept failed/n");
- return false;
- }
-
- printf("test accept event/n");
-
- WSANETWORKEVENTS events;
-
-
- int nRet = WSAEnumNetworkEvents(m_ListeningSocket, m_AcceptEvent, &events);
-
- if (nRet == SOCKET_ERROR)
- {
- printf("error when enumerate network events/n");
- return false;
- }
-
-
- if (events.lNetworkEvents & FD_ACCEPT)
- {
- printf("accept event found/n");
- return true;
- }
-
- return false;
- }
-
-
- BOOL CTcpServer::AcceptNewConnection()
- {
- DWORD dwBytes;
-
-
- OVERLAPPEDPLUS* pOverlapPlus = ::CreateOverlappedPlus();
-
- pOverlapPlus->serverSock = m_ListeningSocket;
- pOverlapPlus->OpCode = OP_ACCEPT;
-
- pOverlapPlus->clientSock = socket(m_pAddrInfo->ai_family, m_pAddrInfo->ai_socktype, m_pAddrInfo->ai_protocol);
-
-
-
-
-
-
- return AcceptEx(m_ListeningSocket, pOverlapPlus->clientSock, pOverlapPlus->wbuf.buf, pOverlapPlus->wbuf.len - ((sizeof(sockaddr_in) + 16) * 2), sizeof(sockaddr_in) + 16,sizeof(sockaddr_in) + 16, &dwBytes, &pOverlapPlus->overlapped);
- }
-
-
-
-
- OVERLAPPEDPLUS* CreateOverlappedPlus()
- {
- OVERLAPPEDPLUS* pOverlapPlus = (OVERLAPPEDPLUS *)malloc(sizeof(OVERLAPPEDPLUS));
- memset(pOverlapPlus, 0, sizeof(OVERLAPPEDPLUS));
-
- char* pBuffer = (char*)malloc(DATA_BUFSIZE);
- pOverlapPlus->wbuf.buf = pBuffer;
- pOverlapPlus->wbuf.len = DATA_BUFSIZE;
-
- return pOverlapPlus;
- }
差不多就酱紫。另外说明在thread中,收到的Accept是带了第一个TCP payload的,所以如果你在AcceptEx之后去WSARecvFrom,是收不到东西的(因为已经收到了, 注意AcceptEx用pOverlapPlus->wbuf.buf 接收数据)
偶认为本人这篇是世界上最清晰易懂的IO Completion 教程:D
另外还有个BindIoCompletionPort,似乎要方便点,没try过...反正我这个是标准实现,呵呵。