可伸缩服务器系统设计实例笔记

近期读《windows网络与通信程序设计》一书,第四章,完成端口IO模型,学习到4.4可伸缩服务器系统设计实例时,代码晦涩难懂,通读之后结合理解记录如下:

先看两个重要的结构体:

struct CIOCPBuffer
{
	WSAOVERLAPPED ol;

	SOCKET sClient;			// AcceptEx接收的客户方套节字
	char *buff;				// I/O操作使用的缓冲区
	int nLen;				// buff缓冲区(使用的)大小
	ULONG nSequenceNumber;	// 此I/O的序列号
	int nOperation;			// 操作类型
#define OP_ACCEPT	1
#define OP_WRITE	2
#define OP_READ		3
	CIOCPBuffer *pNext;
};

// 这是per-Handle数据。它包含了一个套节字的信息
struct CIOCPContext
{
	SOCKET s;						// 套节字句柄
	SOCKADDR_IN addrLocal;			// 连接的本地地址
	SOCKADDR_IN addrRemote;			// 连接的远程地址
	BOOL bClosing;					// 套节字是否关闭
	int nOutstandingRecv;			// 此套节字上抛出的重叠操作的数量
	int nOutstandingSend;
	ULONG nReadSequence;			// 安排给接收的下一个序列号
	ULONG nCurrentReadSequence;		// 当前要读的序列号
	CIOCPBuffer *pOutOfOrderReads;	// 记录没有按顺序完成的读I/O
	CRITICAL_SECTION Lock;			// 保护这个结构
	CIOCPContext *pNext;
};

结合两个结构体理解书中的函数:

几个帮助函数:重叠IO时,每建立一个套接字时,都需要申请内存区域,建立与之关联的CIOCPBuffer和CIOCPContext。AllocateContext,ReleaseContext,AllocateContext,ReleaseContext是与之对应的申请内存和释放内存函数。

书中代码主要运行思路:服务器启动,建立监听socket,为其申请pContext,将其与完成端口对象关联,随后建立监听线程_ListenThreadProc。监听线程中,在监听socket上循环申请pBuffer,使用每个pBuffer投递acceptex重叠请求,增加未决IO计数。随后建立几个工作线程_WorkerThreadProc。工作线程在关联到完成端口的所有套节字上等待I/O完成,传递pContext和pBuffer及其他参数至HandleIO函数进行处理。HandleIO在IO完成时,首先减少对应的未决IO计数,然后针对OP_ACCEPT OP_WRITE OP_READ此3种情形分别进行处理,在接受新连接时需要为新套接字申请pContext,并关联到完成端口对象。处理完IO后,要释放当前IO申请的pBuffer,然后新申请pBuffer,投递新的连接、读、写IO(不投递后续就接不到数据了啊,完成端口对象上就没东西了啊,当然写IO要根据任务需要来投递)。

详细解读:

CIOCPBuffer pBuffer和CIOCPContext pContext在程序中如何使用并发挥作用:通过CreateIoCompletionPort的CompletionKey参数,可以传递套接字附加信息pContext,通过WSASocket,WSARecv,WSASend函数,可传递overlapped结构(pBuffer->ol),通过pBuffer->ol可的得到pBuffer地址。通过以上两步工作(1.将套接字关联到完成端口对象;2.在套接字上投递接受、发送或接收请求),就可以使用GetQueuedCompletionStatus函数等待关联到完成端口的所有请求,当某一请求完成时,GetQueuedCompletionStatus函数的lpcompletionkey和lpoverlapped两个参数,即可返回对应的pContext和pBuffer。

pContext与建立的套接字相关,每个套接字对应一个pContext;pBuffer与投递的重叠IO相关,每在一个套接字上投递一个重叠IO都需要新申请一个pBuffer。也就是说一个套接字可能会申请多个pBuffer。而每一个重叠IO完成后,都需要清理掉该IO投递时申请的pBuffer,再根据需要申请新的pBuffer并投递IO请求。


程序中的Start函数会把监听socket对应的pContext,通过CreateIoCompletionPort关联到完成端口对象,HandleIO会把新接受连接申请的客户上下文对象pContext,通过CreateIoCompletionPort关联新连接到完成端口对象。这样,就建立了所有连接以及连接对应的pContext与完成端口之间的联系。之后,新申请pBuffer,并通过WSASocket,WSARecv,WSASend函数在每个连接上投递请求,在请求完成时,GetQueuedCompletionStatus即可返回对应的pContext和pBuffer。

为了方便管理所有连接,AddAConnection将建立新连接套接字所关联的CIOCPContext *pContext存入m_pConnectionList列表,并更新计数。CloseAConnection从列表中移除指定套接字关联的*pContext结构,关闭套接字,并减少计数。
CloseAllConnections一次性关闭所有套接字,释放m_pConnectionList列表中所有*pContext结构,计数清零。

InsertPendingAccept,RemovePendingAccept,管理未决请求所使用的缓冲区(CIOCPBuffer *pBuffer)的一对方法,需提前申请pBuffer,这对方法不真正申请和销毁内存。当服务器在监听套接字上投递多个accept请求时,还未接收到新连接的accept请求称之为未决请求,所有未决请求都需提前申请一块CIOCPBuffer *pBuffer,也就是I/O缓冲区对象,然后将其插入到m_pPendingAccepts表以备使用。当未决请求连接到新客户端后,未决变为已决,需要将其使用的*pBuffer从m_pPendingAccepts表中移除,并更新计数。
GetNextReadBuffer,负责按正确顺序逐个返回当前应读取的缓冲区,并将接收到的尚未被处理的数据缓冲区按顺序排序,其接收两个参数:CIOCPContext和CIOCPBuffer,CIOCPContext结构中有CIOCPBuffer *pOutOfOrderReads字段,存储对应客户socket接收到的所有pBuffer指针。正确的顺序存储于CIOCPBuffer的nSequenceNumber字段,这是在服务器通过PostRecv方法投递接收请求时新申请的CIOCPBuffer并设置的。
PostAccept,投递一个新的接收连接请求。接收建立好的*pBuffer,设置pbuffer的IO类型OP_ACCEPT,通过WSASocket函数建立用于准备接收新连接的socket,并通过m_lpfnAcceptEx实现在监听套接字上等待新连接。该函数不增加监听套接字的重叠(未决)计数,因为这是在调用该函数前,申请pBuffer时,由其他函数调用InsertPendingAccept实现计数。
PostRecv,投递新的接收数据请求。接收建立好的*pContext,*pBuffer,设置pbuffer的IO类型OP_READ,设置pBuffer的nSequenceNumber序列号,这样在通过WSARecv接收到数据后,就可以根据序号排序处理了。然后通过WSARecv实现在指定套接字上等待接收数据。投递新请求的同时,增加套节字上的重叠(未决)I/O计数和读序列号计数。pContext的nOutstandingRecv和nReadSequence字段。
PostSend,投递新的发送数据请求。接收建立好的*pContext,*pBuffer,设置pbuffer的IO类型OP_WRITE,然后通过WSASend实现在指定套接字上等待发送数据。投递新请求的同时,设置pContext->nOutstandingSend值,增加重叠(未决)计数。
Start方法:检查服务是否已经启动,保存用户参数,初始化类的状态变量,建立监听套接字,绑定本地端口进行监听,创建完成端口对象,加载扩展函数AcceptEx和GetAcceptExSockaddrs,将监听套节字关联到完成端口,注意,这里为它传递的CompletionKey为0(监听套接字只用于监听,不需要连接客户,因此不需要pContext信息)。注册FD_ACCEPT事件(如果投递的AcceptEx I/O不够,线程会接收到FD_ACCEPT网络事件,说明应该投递更多的AcceptEx I/O)。创建监听线程m_hListenThread。
Shutdown:关闭服务器。
_ListenThreadProc:监听线程,先在监听套节字上投递几个Accept I/O(申请pBuffer,InsertPendingAccept加入未决请求缓冲区列表,PostAccept投递接收请求),构建事件对象数组hWaitEvents,以便在上面调用WSAWaitForMultipleEvents函数。
其中hWaitEvents[0]是m_hAcceptEvent,说明投递的Accept请求不够,需要增加,hWaitEvents[1]是m_hRepostEvent,说明处理I/O的线程接受到新的客户,hWaitEvents[2]之后是CreateThread创建的工作线程句柄。然后无限循环处理事件对象数组中的事件,主要完成4项工作:1.首先检查是否要停止服务,并进行相关处理;2.定时检查所有未返回的AcceptEx I/O的连接建立了多长时间;3.m_hAcceptEvent事件对象受信时,说明投递的Accept请求不够,根据程序配置投递一定数量的新accept;4.m_hRepostEvent事件对象受信,说明处理I/O的线程接受到新的客户,此时需要投递m_nRepostCount个新accept请求,然后重置m_nRepostCount为0(m_nRepostCount变量会在HandleIO线程接收到新客户连接时递增1)。
_WorkerThreadProc:工作线程,通过GetQueuedCompletionStatus函数在关联到此完成端口的所有套节字上等待I/O完成,关闭有错误发生的套接字,最后通过HandleIO函数处理IO。
HandleIO:IO处理线程,首先减少套节字上的未决I/O计数,检查参数pBuffer->nOperation值(OP_READ,OP_WRITE),根据值递减pContext->nOutstandingRecv或pContext->nOutstandingSend,这表示之前通过PostRecv和PostSend投递的其中一个请求已经完成了,要减少对应的重叠计数,后续可根据重叠计数情况适当再投递新的recv和send。检查套接字是否发生错误,并进行处理。开始正式处理,有3种情形:1.当接收到新连接时,为新接受的连接申请客户上下文对象CIOCPContext,通过CreateIoCompletionPort关联新连接到完成端口对象,通知用户,向新连接投递几个Read请求(这些空间在套节字关闭或出错时释放),当前Accept请求完成,释放I/O缓冲区,通知监听线程继续再投递一个Accept请求;2.pBuffer->nOperation == OP_READ时,按照I/O投递的顺序读取接收到的数据,通知用户,增加要读的序列号的值,当前读请求完成,释放IO缓冲区对象,继续投递一个新的接收请求;3.pBuffer->nOperation == OP_WRITE,写操作完成,通知用户,释放SendText函数申请的缓冲区,也就是写操作对应的pBuffer。
SendText:向客户端发送文本函数,申请一块新的pBuffer,复制要发送的数据,通过PostSend向指定的pContext投递发送请求。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值