IOCP模型
IOCP(I/O completion port,I/O完成端口)是伸缩性最好的一种I/O模型。
I/O完成端口是应用程序使用线程池处理异步I/O请求的一种机制。处理多个并发异步I/O请求时,使用I/O完成端口比在I/O请求时创建线程更快更有效。
I/O完成端口最初的设计时应用程序发出一些异步I/O请求,当这些请求完成时,设备驱动将把这些工作项目排序到完成端口,这样,在完成端口上等待的线程池便可以处理这些完成I/O。
完成端口实际上时一个Windows I/O结构,它可以接收多个对象的句柄,如文件对象、套接字对象等。
1 接口
// 创建完成端口对象
// 使用完成端口模型,首先要调用CreateIoCompletionPort函数创建一个完成端口对象,Winsock将使用这个对象为任意数量的套接字句柄管理I/O请求。
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANLDE ExistingCompletionPort,
ULONG_PRT CompletionKey,
DWORD NumberOfConcurrentThreads
);
// 函数功能:
// 1:创建一个完成端口对象
// 2:将一个或者多个文件句柄关联到I/O完成端口对象
HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
// 获取完成端口的消息队列中的完成通知封包
// 向完成端口关联套接字句柄之后,便可以通过在套接字上投递重叠发送和接收请求处理I/O了。在这些I/O操作完成时,I/O系统会向完成端口对象发送一个完成通知封包。I/O完成端口以先进先出的方式为这些封包排队。应用程序使用GetQueuedCompletionStatus函数可以取得这些队列中的封包。这个函数应该在处理完成对象I/O的服务线程中调用。
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // 完成端口对象句柄
LPDWORD lpNumberOfBytes, // 取得I/O操作期间传输的字节数
PULONG_PTR lpCompletionKey, // 取得在关联套接字时指定的句柄唯一数据
LPOVERLAPPED * lpOverlapped, // 取得投递I/O操作时指定的OVERLAPPED结构
DWORD dwMilliseconds // 如果完成端口没有完成而封包,此参数指定了等待的事件,INFINITE为无穷大。
);
// 注: I/O服务线程调用 GetQueuedCompletionStatus函数取得有事件发生的套接字的信息,通过lpNumberOfBytes参数得到传输的字节数量,通过lpCompletionKey参数得到捆绑到完成端口的套接字的唯一(per-handle)句柄数据,通过lpOverlapped参数得到投递I/O请求时使用的重叠对象地址,进一步得到I/O唯一(per-I/O)数据。
// 这些参数中,最重要的时per-handle数据和per-I/O数据。
// lpCompletionKey参数包含了我们称为per-handle的数据,因为当套接字第一次与完成端口关联时,这个数据就关联到了一个套接字句柄。这是传递给CreateIoCompletionPort函数的CompletionKey参数。如前所述,可以给这个参数传递任何类型的数据。
// lpOverlapped参数指向一个OVERLAPPED结构,结构后面便是我们称为per-I/O的数据,这可以是工作线程处理完成封包时想要知道的任何信息。
2 实例
// 初始化Winsock库
CInitSock theSock;
#define BUFFER_SIZE 1024
typedef struct _PER_HANDLE_DATA // per_handle数据
{
SOCKET s; // 对应的套接字句柄
sockaddr_in addr; // 客户方地址
}PER_HANDLE_DATA, *PPER_HANDLE_DATA;
typedef struct _PER_IO_DATA // per-I/O数据
{
OVERLAPPED ol; // 重叠结构
char buf[BUFFER_SIZE]; // 数据缓冲区
int nOperationType; // 操作类型
#define OP_READ 1
#define OP_WRITE 2
#define OP_ACCEPT 3
}PER_IO_DATA, *PPER_IO_DATA;
//注:主线程首先创建完成端口对象,创建工作线程处理完成端口对象中的事件;然后再创建监听套接字,开始监听服务端口;接下来便进入无限循环,处理到来的连接请求,这个过程如下:
// 1:调用accept函数等待接收未决的连接请求
// 2: 接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象。
// 3:再新接受的套接字上投递一个接收请求。这个I/O完成之后,由工作线程负责处理。
void main()
{
int nPort = 4567;
// 创建完成端口对象,创建工作线程处理完成端口对象中事件
HANLDE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);
// 创建监听套接字,绑定到本地地址,开始监听
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = ::ntos(nPort);
si.sin_addr.S_un.S_addr = INADDR_ANY;
::bind(sListen, (sockaddr*)&si, sizeof(si));
::listen(sListen, 5);
// 循环处理到来的连接
while(TRUE)
{
// 等待接受未决的连接请求
SOCKADDR_IN saRemote;
int nRemoteLen = sizeof(saRemote);
SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);
// 接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象
PPER_HANDLE_DATA pPerHandle =
(PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
pPerHandle->s = sNew;
memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);
::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (DWORD)pPerHandle, 0);
// 投递一个接收请求
PPER_IO_DATA pPerIO = (PPER_IO_DATA)::GlobalAlloc(GPTR, sizeof(PER_IO_DATA));
pPerIO->nOperationType = OP_READ;
WSABUF buf;
buf.buf = pPerIO->buf;
buf.len = BUFFER_SIZE;
DWORD dwRecv;
DWORD dwFlags = 0;
::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, &pPerIO->ol, NULL);
}
}
// I/O服务线程循环调用GetQueuedCompletionStatus函数从I/O完成端口移除完成的I/O封包,然后根据封包的类型进行处理。
DWORD WINAPI ServerThread(LPVOID lpParam)
{
// 得到完成端口对象句柄
HANDLE hCompletion = (HANDLE)lpParam;
DWORD dwTrans;
PPER_HANDLE_DATA pPerHandle;
PPER_IO_DATA pPerIO;
while(TRUE)
{
// 在关联到此完成端口的所有套接字上等待I/O完成
BOOL bOK = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (LPDWORD)&pPerHandle, (LPOVERLAPPED*)&pPerIO, WSA_INFINITE);
if(!bOK){ // 在此套接字上有错误发生
::closesocket(pPerHandle->s);
::GlobalFree(pPerHandle);
::GlobalFree(pPerIO);
continue;
}
// 套接字被对方关闭
if(dwTrans == 0 &&
(pPerIO->nOperation == OP_READ || pPerIO->nOperationType == OP_WRITE)){
::closesocket(pPerHandle->s);
::GlobalFree(pPerHandle);
::GlobalFree(pPerIO);
continue;
}
// 通过per-I/O数据中的nOperationType域查看什么I/O请求完成了
switch(pPerIO->nOperationType){
case OP_READ: // 完成一个接收请求
{
pPerIO->buf[dwTrans] = '\0';
printf(pPerIO->buf);
// 继续投递接收I/O请求
WSABUF buf;
buf.buf = pPerIO->buf;
buf.len = BUFFER_SIZE;
pPerIO->nOperationType = OP_READ;
DWORD nFlags = 0;
::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, &pPerIO->ol, NULL);
}
break;
case OP_WRITE: // 本例中没有投递这些类型的I/O请求
case OP_ACCEPT:
break;
}
}
return 0;
}