完成端口通讯服务器(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请求”。