一个简单的完成端口服务端客户端类文档格式.docx
《一个简单的完成端口服务端客户端类文档格式.docx》由会员分享,可在线阅读,更多相关《一个简单的完成端口服务端客户端类文档格式.docx(15页珍藏版)》请在冰豆网上搜索。
通过这些简单的源码,你可以:
·
服务或连接多客户端和服务端
异步发送或接收文件
创建并管理一个逻辑工作者线程池,用以处理繁重的客户端/服务器请求或计算
找到一份全面但简单的解决客户端/服务器通信的源码是件困难的事情。
在网络上找到的源码要么太复杂(超过20个类),要命没有提供足够的效率。
本源码的设计尽可能简单,并提供了充足的文档。
在这篇文章中,我们简洁的呈现出了WinsockAPI2.0支持的IOCP技术,说明了在编写过程中出现的棘手问题,并提出了每一个问题的解决方案。
2.2异步完成端口介绍
如果一个服务器应用程序不能同时支持多个客户端,那是毫无意义的,为此,通常使用异步I/O请求和多线程。
根据定义,一个异步I/O请求会立即返回,而留下I/O请求处于等待状态。
有时,I/O异步请求的结果必须与主线程同步。
这可以通过几种不同方式解决。
同步可以通过下面的方式实现:
>
使用事件–当异步请求结束时会马上触发一个信号。
这种方式的缺点是线程必须检查并等待事件被触发
使用GetOverlappedResult函数–这种方式与上一种方式有相同的缺点。
使用AsynchronousProcedureCalls(或APC)–这种方式有几个缺点。
首先,APC总是在请求线程的上下文中被请求;
第二,为了执行APC,请求线程必须在可变等候状态下挂起。
使用IOCP–这种方式的缺点是必须解决很多实际的棘手的编程问题。
编写IOCP可能有点麻烦。
2.2.1为什么使用IOCP?
通过使用IOCP,我们可以解决“每个客户端占用一个线程”的问题。
通常普遍认为如果软件不能运行在真正的多处理器机器上,执行能力会严重降低。
线程是系统资源,而这些资源既不是无限的,也不是低价的。
IOCP提供了一种方式来使用几个线程“公平的”处理多客户端的输入/输出。
线程被挂起,不占用CPU周期直到有事可做。
2.3什么是IOCP?
我们已经看到IOCP只是一个线程同步对象,类似于信号灯,因此IOCP并不是一个复杂的概念。
一个IOCP对象与几个支持待定异步I/O请求的I/O对象绑定。
一个可以访问IOCP的线程可以被挂起,直到一个待定的异步I/O请求结束。
3IOCP是怎样工作的?
要使用IOCP,你必须处理三件事情,绑定一个socket到完成端口,创建异步I/O请求,并与线程同步。
为从异步I/O请求获得结果,如那个客户端发出的请求,你必须传递两个参数:
CompletionKey参数和OVERLAPPED结构。
3.1关键参数
第一个参数:
CompletionKey,是一个DWORD类型的变量。
你可以传递任何你想传递的唯一值,这个值将总是同该对象绑定。
正常情况下会传递一个指向结构或类的指针,该结构或类包含了一些客户端的指定对象。
在源码中,传递的是一个指向ClientContext的指针。
3.2OVERLAPPED参数
这个参数通常用来传递异步I/O请求使用的内存缓冲。
很重要的一点是:
该数据将会被锁定并不允许从物理内存中换出页面(pageout)。
3.3绑定一个socket到完成端口
一旦创建完成一个完成端口,可以通过调用CreateIoCompletionPort函数来绑定socket到完成端口。
形式如下:
BOOLIOCPS:
:
AssociateSocketWithCompletionPort(SOCKETsocket,HANDLEhCompletionPort,DWORDdwCompletionKey)
{
HANDLEh=CreateIoCompletionPort((HANDLE)socket,hCompletionPort,dwCompletionKey,m_nIOWorkers);
returnh==hCompletionPort;
}
3.4响应异步I/O请求
响应具体的异步请求,调用函数WSASend和WSARecv。
他们也需要一个参数:
WSABUF,这个参数包含了一个指向缓冲的指针。
一个重要的规则是:
通常当服务器/客户端响应一个I/O操作,不是直接响应,而是提交给完成端口,由I/O工作者线程来执行。
这么做的原因是:
我们希望公平的分割CPU周期。
通过发送状态给完成端口来发出I/O请求,如下:
BOOLbSuccess=PostQueuedCompletionStatus(m_hCompletionPort,
pOverlapBuff->
GetUsed(),
(DWORD)pContext,
&
pOverlapBuff->
m_ol);
3.5与线程同步
与I/O工作者线程同步是通过调用GetQueuedCompletionStatus函数来实现的(如下)。
这个函数也提供了CompletionKey参数和OVERLAPPED参数,如下:
BOOLGetQueuedCompletionStatus(HANDLECompletionPort,//handletocompletionport
LPDWORDlpNumberOfBytes,//bytestransferred
PULONG_PTRlpCompletionKey,//filecompletionkey
LPOVERLAPPED*lpOverlapped,//buffer
DWORDdwMilliseconds//optionaltimeoutvalue
);
3.6四个棘手的IOCP编码问题和解决方法
使用IOCP时会出现一些问题,其中有一些不是很直观的。
在使用IOCP的多线程编程中,一个线程函数的控制流程不是笔直的,因为在线程和通讯直接没有关系。
在这一章节中,我们将描述四个不同的问题,可能在使用IOCP开发客户端/服务器应用程序时会出现,分别是:
TheWSAENOBUFSerrorproblem.(WSAENOBUFS错误问题)
Thepackagereorderingproblem.(包重构问题)
Theaccessviolationproblem.(访问非法问题)
3.6.1WSAENOBUFS问题
这个问题通常很难靠直觉发现,因为当你第一次看见的时候你或许认为是一个内存泄露错误。
假定已经开发完成了你的完成端口服务器并且运行的一切良好,但是当你对其进行压力测试的时候突然发现服务器被中止而不处理任何请求了,如果你运气好的话你会很快发现是因为WSAENOBUFS错误而影响了这一切。
每当我们重叠提交一个send或receive操作的时候,其中指定的发送或接收缓冲区就被锁定了。
当内存缓冲区被锁定后,将不能从物理内存进行分页。
操作系统有一个锁定最大数的限制,一旦超过这个锁定的限制,那么就会产生WSAENOBUFS错误了。
如果一个服务器提交了非常多的重叠的receive在每一个连接上,那么限制会随着连接数的增长而变化。
如果一个服务器能够预先估计可能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的receive在每一个连接上。
因为当你提交操作没有缓冲区时,那么也不会存在内存被锁定了。
使用这种办法后,当你的receive操作事件完成返回时,该socket底层缓冲区的数据会原封不动的还在其中而没有被读取到receive操作的缓冲区来。
此时,服务器可以简单的调用非阻塞式的recv将存在socket缓冲区中的数据全部读出来,一直到recv返回WSAEWOULDBLOCK为止。
这种设计非常适合那些可以牺牲数据吞吐量而换取巨大并发连接数的服务器。
当然,你也需要意识到如何让客户端的行为尽量避免对服务器造成影响。
在上一个例子中,当一个零缓冲区的receive操作被返回后使用一个非阻塞的recv去读取socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个receive来取代非阻塞的recv来进行数据接收。
(这比你使用1个缺省的8K缓冲区来接收要好的多。
)
源码中提供了一个简单实用的解决WSAENOBUF错误的办法。
我们执行了一个零字节缓冲的异步WSARead(...)(参见OnZeroByteRead(..))。
当这个请求完成,我们知道在TCP/IP栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGESIZE缓冲的异步WSARead(...)去读,解决了WSAENOBUFS问题。
但是这种解决方法降低了服务器的吞吐量。
总结:
解决方法一:
投递使用空缓冲区的receive操作,当操作返回后,使用非阻塞的recv来进行真实数据的读取。
因此在完成端口的每一个连接中需要使用一个循环的操作来不断的来提交空缓冲区的receive操作。
解决方法二:
在投递几个普通含有缓冲区的receive操作后,进接着开始循环投递一个空缓冲区的receive操作。
这样保证它们按照投递顺序依次返回,这样我们就总能对被锁定的内存进行解锁。
3.6.2包重构问题
......尽管使用IO完成端口的待发操作将总是按照他们发送的顺序来完成,线程调度安排可能使绑定到完成端口的实际工作不按指定的顺序来处理。
例如,如果你有两个I/O工作者线程,你可能接收到“字节块2,字节块1,字节块3”。
这就意味着:
当你通过向I/O完成端口提交请求数据发送数据时,数据实际上用重新排序过的顺序发送了。
这可以通过只使用一个工作者线程来解决,并只提交一个I/O请求,等待它完成。
但是如果这么做,我们就失去了IOCP的长处。
解决这个问题的一个简单实用办法是给我们的缓冲类添加一个顺序数字,如果缓冲顺序数字是正确的,则处理缓冲中的数据。
这意味着:
有不正确的数字的缓冲将被存下来以后再用,并且因为执行原因,我们保存缓存到一个HASHMAP对象中(如m_SendBufferMap和m_ReadBufferMap)。
获取这种解决方法的更多信息,请查阅源码,仔细查看IOCPS类中如下的函数:
GetNextSendBuffer(..)andGetNextReadBuffer(..),togettheorderedsendorreceivebuffer.
IncreaseReadSequenceNumber(..)andIncreaseSendSequenceNumber(..),toincreasethesequencenumbers.
3.6.3异步等待读和字节块包处理问题
最通用的服务端协议是一个基于协议的包,首先X个字节代表包头,包头包含了详细的完整的包的长度。
服务端可以读包头,计算出需要多少数据,继续读取直到读完一个完整的包。
当服务端同时只处理一个异步请求时工作的很好。
但是,如果我们想发挥IOCP服务端的全部潜能,我们应该启用几个等待的异步读事件,等待数据到达。
这意味着几个异步读操作是不按顺序完成的,通过等待的读事件返回的字节块流将不会按顺序处理。
而且,一个字节块流可以包含一个或几个包,也可能包含部分包,如下图所示:
这个图形显示了部分包(绿色)和完整包(黄色)是怎样在不同字节块流中异步到达的。
这意味着我们必须处理字节流来成功的读取一个完整的包。
而且,我们必须处理部分包(图表中绿色的部分)。
这就使得字节流的处理更加困难。
这个问题的完整解决方法在IOCPS类的ProcessPackage(…)函数中。
3.6.4访问非法问题
这是一个较小的问题,代码设计导致的问题更胜于IOCP的特定问题。
假设一个客户端连接已经关闭并且一个I/O请求返回一个错误标志,然后我们知道客户端已经关闭。
在参数CompletionKey中,我们传递了一个指向结构ClientContext的指针,该结构中包含了客户端的特定数据。
如果我们释放这个ClientContext结构占用的内存,并且同一个客户端处理的一些其它I/O请求返回了错误代码,我们通过转换参数CompletionKey为一个指向ClientContext结构的指针并试图访问或删除它,会发生什么呢?
一个非法访问出现了!
这个问题的解决方法是添加一个数字到结构中,包含等待的I/O请求的数量(m_nNumberOfPendingIO),然后当我们知道没有等待的I/O请求时删除这个结构。
这个功能通过函数EnterIoLoop(…)和ReleaseClientContext(…)来实现。
3.7源码略读
源码的目标是提供一系列简单的类来处理所有IOCP编码中的问题。
源码也提供了一系列通信和C/S软件中经常使用的函数,如文件接收/传送函数,逻辑线程池处理,等等。
上图功能性的图解说明了IOCP类源码。
我们有几个IO工作者线程通过完成端口来处理异步IO请求,这些工作者线程调用一些虚函数,这些虚函数可以把需要大量计算的请求放到一个工作队列中。
逻辑工作者通过类中提供的这些函数从队列中取出任务、处理并发回结果。
GUI经常与主类通信,通过Windows消息(因为MFC不是线程安全的)、通过调用函数或通过使用共享的变量。
图三
上图显示了类结构纵览。
图3中的类说明如下:
CIOCPBuffer:
管理异步请求的缓存的类。
IOCPS:
处理所有通信的主类。
JobItem:
保存逻辑工作者线程要处理的任务的结构。
ClientContex:
保存客户端特定信息的结构(如状态、数据,等等)。
3.7.1缓冲设计-CIOCPBuffer类
使用异步I/O调用时,我们必须提供私有的缓冲区供I/O操作使用。
当我们将帐号信息放入分配的缓冲供使用时有许多情况需要考虑:
.分配和释放内存代价高,因此我们应重复使用以及分配的缓冲(内存),
因此我们将缓冲保存在列表结构中,如下所示:
//FreeBufferList..
CCriticalSectionm_FreeBufferListLock;
CPtrListm_FreeBufferList;
//OccupiedBufferList..(Buffersthatiscurrentlyused)
CCriticalSectionm_BufferListLock;
CPtrListm_BufferList;
//NowweusethefunctionAllocateBuffer(..)
//toallocatememoryorreuseabuffer.
.有时,当异步I/O调用完成后,缓冲里可能不是完整的包,因此我们需要分割缓冲去取得完整的信息。
在CIOCPS类中提供了SplitBuffer函数。
同样,有时候我们需要在缓冲间拷贝信息,IOCPS类提供了AddAndFlush函数。
.众所周知,我们也需要添加序号和状态(IOType变量,IOZeroReadCompleted,等等)到我们的缓冲中。
.我们也需要有将数据转换到字节流或将字节流转换到数据的方法,CIOCPBuffer也提供了这些函数。
以上所有问题都在CIOCPBuffer中解决。
3.8如何使用源代码
从IOCP继承你自己的类(如图3),实现IOCPS类中的虚函数(例如,threadpool),
在任何类型的服务端或客户端中实现使用少量的线程有效地管理大量的连接。
3.8.1启动和关闭服务端/客户端
调用下面的函数启动服务端
BOOLStart(intnPort=999,intiMaxNumConnections=1201,
intiMaxIOWorkers=1,intnOfWorkers=1,
intiMaxNumberOfFreeBuffer=0,
intiMaxNumberOfFreeContext=0,
BOOLbOrderedSend=TRUE,
BOOLbOrderedRead=TRUE,
intiNumberOfPendlingReads=4);
nPort
服务端侦听的端口.(-1客户端模式.)
iMaxNumConnections
允许最大的连接数.(使用较大的数.)
iMaxIOWorkers
I/O工作线程数
nOfWorkers
逻辑工作者数量Numberoflogicalworkers.(可以在运行时改变.)
iMaxNumberOfFreeBuffer
重复使用的缓冲最大数.(-1不使用,0=不限)
iMaxNumberOfFreeContext
重复使用的客户端信息对象数(-1for不使用,0=不限)
bOrderedRead
顺序读取.(我们已经在3.6.2.处讨论过)
bOrderedSend
顺序写入.(我们已经在3.6.2.处讨论过)
iNumberOfPendlingReads
等待读取数据时未决的异步读取循环数
连接到远程服务器(客户端模式nPort=-1),调用函数:
CodeConnect(constCString&
strIPAddr,intnPort)
.strIPAddr
远程服务器的IP地址
.nPort
端口
调用ShutDown()关闭连接
例如:
if(!
m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("
ErrorcouldnotstarttheClient"
….
m_iocp.ShutDown();
4.1源代码描述
更多关于源代码的信息请参考代码里的注释。
4.1.1虚函数
NotifyNewConnection
新的连接已接受
NotifyNewClientContext
空的ClientContext结构被分配
NotifyDisconnectedClient
客户端连接断开
ProcessJob
逻辑工作者需要处理一个工作
NotifyReceivedPackage
新的包到达
NotifyFileCompleted
文件传送完成。
4.1.2重要变量
所有变量共享使用时必须加锁避免存取违例,所有需要加锁的变量,名称为XXX则锁变量名称为XXXLock。
m_ContextMapLock;
保存所有客户端数据(socket,客户端数据,等等)
ContextMapm_ContextMap;
m_NumberOfActiveConnections
保存已连接的连接数
4.1.3重要函数
GetNumberOfConnections()
返回连接数
CStringGetHostAdress(ClientContext*p)
提供客户端上下文,返回主机地址
BOOLASendToAll(CIOCPBuffer*pBuff);
发送缓冲上下文到所有连接的客户端
DisconnectClient(CStringsID)
根据客户端唯一编号,断开指定的客户端
CStringGetHostIP()
返回本地IP
JobItem*GetJob()
将JobItem从队列中移出,如果没有job,返回NULL
BOOLAddJob(JobItem*pJob)
添加Job到队列
BOOLSetWorkers(intnThreads)
设置可以任何时候调用的逻辑工作者数量
DisconnectAll();
断开所有客户端
ARead(…)
异步读取
ASend(…)
异步发送,发送数据到客户端
ClientContext*FindClient(CStringstrClient)
根据字符串ID寻找客户(非线程安全)
DisconnectClient(ClientContext*pContext,BOOLbGraceful=FALSE);
端口客户
DisconnectAll()
端口所有客户
StartSendFile(ClientContext*pContext)
根据ClientContext结构发送文件(使用经优化的transmitfile(..)函数)
PrepareReceiveFile(..)
接收文件准备。
调用该函数时,所有进入的字节流已被写入到文件。
PrepareSendFile(..)
打开文件并发送包含文件信息的数据包。
函数禁用ASend(..)函数,直到文件传送关闭或中断。
DisableSendFile(..)
禁止发送文件模式
DisableRecevideFile(..)
禁止文件接收模式
5.1文件传输
文件传输使用Winsock2.0中的TransmitFile函数。
TransmitFile函数通过连接的socket句柄传送文件数据。
函数使用操作系统的高速缓冲管理器(cachemanager)接收文件数据,通过sockets提供高性能的文件数据传输。
异步文件传输要点:
在TransmitFile函数返回前,所有其他发送或写入到该socket的操作都将无法执行,因为这将使文件数据混乱。
因此,在PrepareSendFile()函数调用之后,所有ASend都被禁止。
因为