完成端口通讯服务器(IOCP Socket Server)设计
(五)妙用PerHandleData和PerIoData
Copyright © 2009 代码客(卢益贵)版权所有
QQ:48092788 源码博客:http://blog.csdn.net/guestcode
在部分系统需求下,使用PerHandleData和PerIoData来设计IOCP服务器是非常有效的。但随之而来的问题也是麻烦的:如何才能高效的申请和回收PerHandleData和PerIoData?
曾有不少人说动态申请内存和初始化是高效的,优于使用临界锁(或其他锁)的内存池。持这个看法的人,估计和我前面说的“迷信API”的情形似乎有些类似,不要觉得“显式使用临界”就低效于直接使用API获得资源。有个问题我们可以深思一下:在多线程下操作系统是如何同步管理内存的?难道它不需要同步?事实上,它不仅需要同步,而且所做的工作远不比你自己的内存池出栈这么简单,因为你的申请是带有“size”的。
我个人认为自己的内存池和动态申请系统内存已经没有必要再去争议。下面我就介绍自己如何设计PerHandleData和PerIoData的,希望对初学者有帮助,也希望得到高人指点。
一、PerIoData
关于PerIoData的定义:
typedef struct _GIO_DATA_INFO
{
OVERLAPPED Overlapped;
WSABUF WSABuf;
GIO_OPER_TYPE OperType;
void *pOwner;
}GIO_DATA_INFO, *PGIO_DATA_INFO;
typedef struct _GIO_DATA
{
OVERLAPPED Overlapped;
WSABUF WSABuf;
GIO_OPER_TYPE OperType;
void *pOwner;
char cData[1];
}GIO_DATA, *PGIO_DATA;
GIO_DATA_INFO作为PerIoData的固定定义,是IO操作必要的信息。GIO_DATA 从cData开始才是真正的IO数据区,cData[]不固定多大,默认是1,可以在系统运行前根据参数设置来设置PerIoData的大小,扣除GIO_DATA_INFO部分就是IO有效数据部分。
另外,我们可以在通讯层严格封装,在业务层向通讯层申请内存的时候,返回的是cData地址:
PGIO_BUF GIoDat_AllocGBuf(void)
{
EnterCriticalSection(&GIoDataPoolHeadCS);
if(pGIoDataPoolHead->pNext)
{
PGIO_BUF Result;
Result = (PGIO_BUF)pGIoDataPoolHead;
pGIoDataPoolHead = pGIoDataPoolHead->pNext;
LeaveCriticalSection(&GIoDataPoolHeadCS);
return((char *)Result + dwGBufOffset);
}else
{
LeaveCriticalSection(&GIoDataPoolHeadCS);
return(NULL);
}
}
说明:
“return((char*)Result+dwGBufOffset);”是将返回地址偏移后指向cData;dwGBufOffset的值 = sizeof(GIO_DATA_INFO) + sizeof(PackHead),无通讯协议情况下: sizeof(PackHead) = 0;
业务层发送的时候调用的发送函数:
BOOL GCommProt_PostSendGBuf(DWORD dwClientContext, PGIO_BUF pGBuf, DWORD dwBytes)
{
pGBuf = (PGIO_BUF)((char *)pGBuf - dwGBufOffset);
((PGIO_DATA)pGBuf)->WSABuf.len = dwBytes;
…
…WSASend(…)…
}
第一句“pGBuf = (PGIO_BUF)((char *)pGBuf - dwGBufOffset);”已经使pGBuf和PGIO_DATA对齐了,与申请时“return((char*)Result+dwGBufOffset);”是反操作。这样避免了发送数据的复制过程(当然同一缓冲多发的时候另当别论),即可直接调用WSASend投递。
二、PerHandleData
关于PerHandleData的定义:
typedef struct _GHND_DATA
{
void* pfnOnIocpOper;
void* pfnOnIocpError;
SOCKET Socket;
_GHND_DATA* pNext;
_GHND_DATA* pPrior;
GHND_TYPE htType;
GHND_STATE hsState;
DWORD dwAddr;
DWORD dwPort;
DWORD dwTickCountAcitve;
void* pOwner;
void* pData;
}GHND_DATA, *PGHND_DATA;
成员pfnOnIocpOper和pfnOnIocpError的说明请看前一篇。使用双向链表是为了建立客户连接链表时使用,避免删除节点时做遍历工作。pData是为了建立客户登录模式的时候与UserInfo关联:pData =pUserInfo, pUserInfo->dwClientContext=(DWORD)pHndData,可用于服务器主动发送模式。
PerHandleData的难点是何时回收。同一个PerHandleData可以投递多个PerIoData,为了确保所有IO请求都返回的时候,才能回收PerHandleData,一般有两个做法:1、使用重用计数,即每次IO投递都要进行计数操作;2、记录所有IO操作的PerIOData,即在PerHandleData定义一个链表头把每个IO请求的PerIoData放入这个链表,每个IO请求返回后再出列。当计数器为0或该IO链表头为空的时候,即可回收PerHandleData,以上两个方法都需要同步,且属于“后回收”方式。我们都知道,Socekt最频繁的就是收发操作,对于高频率的地方使用同步,无疑降低了不少性能(尽管我们使用高效的内核锁)。本人使用的是“前回收”方式,即不必等“所有IO请求都返回”在连接断开(错误发生)的时候立即回收PerHandleData,但要保证在“所有IO请求没有返回”前不能再利用刚回收的PerHandleData。要做到这个目的,就需要延时。当然在这里不能设置延时操作,这样的话会适得其反。我们都知道,后进先出的链栈是不可能保证这个要求的。假如后进后出呢?有这个可能性,但要有一定数量的节点在刚回收的节点前面,就是“等前面的使用完了才会轮到我”即可达到延时的目的。对于建立连接模式的socket,都需要有MaxConnection来限制连接数量,以确保不超出服务器极限资源。假如把PerHandleData的数量设置超过MaxConnection值,那么即可达到上述目的:
当MaxConnection=1000时,设置PerHandleData的数量为1100(多出的100这个数可以称之为“延时量”)。那么在PerHandleData的资源链表里面,至少永远有100个节点在空闲着,这个后进后出的链表即可实现PerHandleData再利用的延时功能。问题也来了:“延时量”设置多少为合适?个人认为极限不低于MaxConnection的10%,50%为宜,保守是100%以上。即MaxConnection=5000的时候,延时量=5000,PerHandleData数量=10000(牺牲这点内存是值得的)。
要实现这个后进后出的方式,看来使用内核链栈是不可能了。我还是使用“双锁单链表”的算法方式,表头和表尾使用不同的临界锁(算法请看前面第三篇)。这个时候有人可能有人又批评了,临界效率太低了,聪明反被聪明误。我倒不觉得,客户连接和断开的频率远比数据收发的低,还是能达到目的的(特定模式的需求除外,比如:连接上来发个数据包就断开)。