本文共 4211 字,大约阅读时间需要 14 分钟。
转载自
完成端口通讯服务器(IOCP Socket Server)设计
(四)一个简单而又灵活的IOCP模块
Copyright © 2009 代码客(卢益贵)版权所有
QQ:48092788 源码博客:http://blog.csdn.net/guestcode
本文对部分IOCP不再多做重复的说明,阅读本文应该对IOCP有一定的了解(本篇也并未包括异步socket)。
(一)、四个IOCP的API
1、创建完成端口:
HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads);
2、关联完成端口
HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads);
3、获取队列完成状态
BOOL WINAPI GetQueuedCompletionStatus( __in HANDLE CompletionPort, __out LPDWORD lpNumberOfBytes, __out PULONG_PTR lpCompletionKey, __out LPOVERLAPPED* lpOverlapped, __in DWORD dwMilliseconds);
4、投递一个队列完成状态
BOOL WINAPI PostQueuedCompletionStatus( __in HANDLE CompletionPort, __in DWORD dwNumberOfBytesTransferred, __in ULONG_PTR dwCompletionKey, __in LPOVERLAPPED lpOverlapped);
创建和关联完成端口是同一个函数仅是参数传递不一样而已,有关其他参数这里不多重复,请参阅MSDN。
(二)两个关键的参数
1、dwCompletionKey
在这里,本人把这个完成键扩展为如下定义:
函数指针定义:
typedef void(*PFN_ON_GIOCP_ERROR)(void* pCompletionKey, void* pOverlapped);typedef void(*PFN_ON_GIOCP_OPER)(DWORD dwBytes, void* pCompletionKey, void* pOverlapped);
完成键结构定义:
typedef struct _COMPLETION_KEY{PFN_ON_GIOCP_OPER pfnOnIocpOper;PFN_ON_GIOCP_ERROR pfnOnIocpError;}GIOCP_COMPLETION_KEY, *PGIOCP_COMPLETION_KEY;
在其他Io操作的时候,满足这个既定格式的,可以在这个数据结构基础之上进行扩展。
2、lpOverlapped
本模块尚未用到,但在以后的异步Socket里面是Io操作的关键。
(三)工作线程源码
typedef struct _GWORKER{_GWORKER* pNext;DWORD dwThreadId;DWORD dwRunCount; //记录工作线程循环次数,为监视窗口提供数据HANDLE hFinished; //表示工作线程已经结束void *pData; //每个工作线程独立拥有的数据项}GWORKER, *PGWORKER; DWORD WINAPI GIocpWorkerThread(PGWORKER pWorker)/*说明:工作线程函数**输入:被创建的工作者的结构指针**输出:*/{ BOOL bResult; DWORD dwBytes; PGIOCP_COMPLETION_KEY pCompletionKey; LPOVERLAPPED pOverlapped; //调用工作线程开始工作的的回调函数,以便创建每个线程独立拥有的会话,比如数据库连接会话,//并使用GIocp_SetWorkerData设置该工作线程独立关联的数据项,比如数据库连接类的指针 pfnOnGIocpWorkerThreadBegin((DWORD)pWorker);//死循环标签,也可以使用for(;;)Loop: //等待完成端口事件 bResult = GetQueuedCompletionStatus(hGIocpCompletionPort, &dwBytes, (PULONG_PTR)&pCompletionKey, &pOverlapped, INFINITE); //工作线程计数器累加,表示工作线程活动计数 pWorker->dwRunCount++; //如果完成键是空,表示要结束工作线程 if(!pCompletionKey) goto End; //等待完成事件失败 if(bResult) //调用读写处理函数 pCompletionKey->pfnOnIocpOper(dwBytes, pCompletionKey, pOverlapped); else //调用错误处理函数 pCompletionKey->pfnOnIocpError(pCompletionKey, pOverlapped); //继续等待完成事件 goto Loop;End: //结束了,调用回调处理函数,比如摧毁数据库库会话对象, pfnOnGIocpWorkerThreadEnd((DWORD)pWorker); //设置结束标志 SetEvent(pWorker->hFinished); return(0);}
从这个工作线程代码来看,它做到了与Io操作结果的无关性,它不知道你是读操作还是写操作的返回,或者是AcceptEx的返回等等。它看上去似乎非常简单,也正因为这样它的扩展性是相当强大的。如果TcpServer功能模块的操作和这个完成端口句柄关联,就可以实现一个面向连接的服务器;如果TcpClient功能模块的操作和这个完成端口句柄关联,就可以实现一个完成端口的客户端,如何与TcpServer功能模块一起使用,即可实现集群服务器之间的通讯;如果UDP功能模块的操作和这个完成端口句柄关联,即可实现完成端口的P2P通讯,如果和TcpServer功能模块一起使用,即可实现双协议服务器。
(四)工作线程的不确定性
当我们创建一定数量的工作线程的时候,这个IOCP的多线程机制和我们常规的多线程机制有着很大的区别。比如说使用PostQueuedCompletionStatus投递一个完成事件,我们无法预知是由哪个线程来处理,这个和常规的线程WaitForSingleObject有着本质区别(SetEvent可以指定某个线程来响应)。并且多个线程的工作率是不平衡,甚至会出现有的工作线程忙得要死有的却闲得要死的现象,即使你设置了并发线程数量(NumberOfConcurrentThreads)足够大(当然在多核或多CPU情况下),还是会出现工作量不平衡的现象。
如下图(单核):
(五)结束工作线程
先看下面代码:
PGWORKER pWorker; //给每个工作者线程抛出结束事件 pWorker = pGIocpWorkerHead; while(pWorker) { PostQueuedCompletionStatus(hGIocpCompletionPort, 0, 0, NULL); pWorker = pWorker->pNext; } //等待每个工作线程都结束 pWorker = pGIocpWorkerHead; while(pWorker) { WaitForSingleObject(pWorker->hFinished, INFINITE); CloseHandle(pWorker->hFinished); pWorker = pWorker->pNext; }由PostQueuedCompletionStatus抛出一个假完成事件,完成键和重叠结构都是空的,工作线程会这样判断结束:if(!pCompletionKey) goto End;
并且注意:有多少个工作线程,就调用多少个PostQueuedCompletionStatus,确保工作线程都结束了(WaitForSingleObject),才能释放Worker占用的内存。
另外,在调用PostQueuedCompletionStatus结束工作线程之前一定确保没有“未决的Io请求”。