文件名称:
一个简单的完成端口(服务端-客户端)类
开发工具:
文件大小: 293kb
下载次数: 0
上传时间: 2019-03-16
详细说明:本文的源码使用了高级的完成端口(IOCP)技术,该技术可以有效地服务于多客户端。本文提出了一些 IOCP 编程中出现的实际问题的解
决方法,并提供了一个简单的 echo 版本的可以传输文件的客户端/服务器程序。第一个参数: Comp onKey,是一个 DWORD类型的变量。你可以传递任何你想传递的唯一值,这个值将总是同该对象绑定。正常情况下
会传递一个指向结构或类的指针,该结构或类包含了一些客户端的指定对象。在源码中,传递的是一个指向 Client context的指针
OVERLAPPED参数
这个参数通常用来传递异步10请求使用的内存缓冲。很重要的一点是:该数据将会被锁定并不允许从物理内存中换出页面( page out)
绑定一个 socket到完成端口
一旦创建完成一个完成端口,可以通过调用 Gr eate locompletionport函数来绑定 socket到完成端口。形式如下
BOOL 10CPS:: AssociateSocketWithCompletion Port(SOCKET socket, HANDLE hComp let i onPort, dWORd dwComp let ionKey)
HANDLE h= CreateloComp let ionPort ((HANDLe) socket, hComp letionPort, dwComp let ionKey m nl OWor kers)
return h = complet inpOrt
响应异步10请求
响应具体的异步请求,调用函数 WSASend和 VSARecy。他们也需要一个参数: WSABUF,这个参数包含了一个指向缓冲的指针。一个重要的
规则是:通常当服务器/客户端响应一个10操作,不是直接响应,而是提交给完成端口,由1/0工作者线程来执行。这么做的原因是
我们希望公平的分割CPU周期。通过发送状态给完成端口来发出|/0请求,如下:
BOOL bSuccess PostQueuedComp let ion Status(m hCompletionPort
pOver lapBuff->GetUsed o
(DWORD) cOntext
&pOver I apBuff->m ol)
与线程同步
与1/0工作者线程同步是通过调用 GetQueued CompletionStatus函数来实现的(如下)这个函数也提供了 Complet i onKey参数和0 VERLAPPED
参数,如下
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // handle to completion port
LPDWORd I pNumber OfBytes, //bytes transferred
PULONG PtR I p Completionkey, / file comp letion key
LPOVERLAPPED *lpOver I apped, / buffer
dWoRD dwMil I i seconds //opt ional timeout value
四个棘手的10P编码问题和解决方法
使用0P时会出现一些问题,其中有一些不是很直观的。在使用0叩卩的多线程编程中,一个线程函数的控制流程不是笔直的,因为在线
程和通讯直接没有关系。在这一章节中,我们将描述四个不同的问题,可能在使用10CP开发客户端/服务器应用程序时会出现,分别是
The WSAENOBUFS error problem.( WSAENOBUFS错误问题)
The package reor der ing prob lem.(包重构问题)
The access violation problem.(访问非法问题)
WSAENOBUFS问题
这个问题通常很难靠直觉发现,因为当你第一次看见的时候你或许认为是一个内存泄露错误。假定已经开发完成了你的完成端口服务器并
且运行的一切良好,但是当你对其进行压力测试的时候突然发现服务器被中止而不处理任何请求了,如果你运气好的话你会很快发现是
因为 WSAENOBUFS错误而影响了这一切。
每当我们重叠提交一^send或rε ce i ve操作的时候,其中指定的发送或接收缓冲区就被锁定了。当内存缓冲区被锁定后,将不能从物理
内存进行分页。操作系统有一个锁定最大数的限制,一旦超过这个锁定的限制,那么就会产生 WSAENOBUFS错误了。
如果一个服务器提交了非常多的重叠的 receive在每一个连接上,那么限制会随着连接数的增长而变化,如果一个服务器能够预先估计可
能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的 rece l ve在每一个连接上。因为当你提交操作没有缓冲区时,那么也
不会存在内存被锁定了。使用这种办法后,当你的 receive操作事件完成返回时,该 socket底层缓冲区的数据会原封不动的还在其中而
没有被读取到 rece i ve操作的缓冲区来。此时,服务器可以简单的调用非阻塞式的reow将存在 socket缓冲区中的数据全部读出来,
直到recν返回 WSAE WOULDBLOCK为止。这种设计非常适合那些可以牺牲数据吞吐量而换取巨大并发连接数的服务器。当然,你也需要
意识到如何让客户端的行为尽量避免对服务器造成影响。在上一个例子中,当一个零缓冲区的 rece i ve操作被返回后使用一个非阻塞的
recⅴ去读取 socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个 receive来取
代非阻塞的recν来进行数据接收。(这比你使用1个缺省的&K缓冲区来接收要好的多。)
源码中提供了一个简单实用的解决 WSAEN0BUF错误的办法。我们执行了一个零字节缓冲的异步 VISARead(..)(参见0 nZeroByteRead())
当这个请求完成,我们知道在τCP/P栈中有数据,然后我们通过执行几个有 MAX I MUMPACKAGES|E缓冲的异步 WSARead()去读,解决
了 WSAENOBUFS问题。但是这种解决方法降低了服务器的吞吐量。
解决方法一
投递使用空缓冲区的 recei ve操作,当操作返回后,使用非阻塞的re∝ν来进行真实数据的读取。因此在完成端口的毎一个连接中需要使
用一个循环的操作来不断的来提交空缓冲区的 recei ve操作。
解决方法二
在投递几个普通含有缓冲区的 receive操作后,进接着开始循环投递一个空缓冲区的 rece i ve操作。这样保证它们按照投递顺序依次返回,
这样我们就总能对被锁定的内存进行解锁
包重构问题
尽管使用I0完成端口的待发操作将总是按照他们发送的顺序来完成,线程调度安排可能使绑定到完成端口的实际工作不按指定的顺序来
处理。例如,如果你有两个10工作者线程,你可能接收到“字节块2,字节块1,字节块3”。这就意味着:当你诵过向10完成端口
提交请求数据发送数据时,数据实际上用重新排序过的顺序发送了。
这可以通过只使用一个工作者线程来解决,并只提交一个10请求,等待它完成。但是如果这么做,我们就失去了10CP的长处。
解决这个问题的一个简单实用办法是给我们的缓冲类添加一个顺序数字,如果缓冲顺序数字是正确的,则处理缓冲中的数据。这意味着:
有不正确的数字的缓冲将被存下来以后再用,并且因为执行原因,我们保存缓存到一个 HASH MAP对象中(如 m Send Buffer Map和
m Re ad Buffer Map
获取这种解决方法的更多信息,请查阋源码,仔细查看0CPS类中如下的函数
GetNextSendBuffer (. and GetNextReadBuffer (.), to get the ordered send or receive buffer
ncr easeReadsequenceNumber
and Increase Send SequenceNumber (.) to increase the sequence numbers
异步等待读和字节块包处理问题
最通用的服务端协议是一个基于协议的包,首先X个字节代表包头,包头包含了详细的完整的包的长度。服务端可以读包头,计算出需要
多少数据,继续读取直到读完一个完整的包。当服务端同时只处理一个异步请求时工作的很好。但是,如果我们想发挥0CP服务端的全
部潜能,我们应该启用几个等待的异步读事件,等待数据到达。这意味着几个异步读操作是不按顺序完成的,通过等待的读事件返回的
字节块流将不会按顺序处理。而且,一个字节块流可以包含一个或几个包,也可能包含部分包,如下图所示
Pending asynchronous reads
Byte stream
1
Partial packages
Complete packages
这个图形显示了部分包(绿色)和完整包(黄色)是怎样在不同字节块流中异步到达的。这意味着我们必须处理字节流来成功的读取一个
完整的包。而且,我们必须处理部分包(图表中绿色的部分)。这就使得字节流的处理更加困难。这个问题的完整解决方法在10CPS类的
ProcessPackage(…)函数中。
访问非法问题
这是一个较小的问题,代码设计导致的问题更胜于I0℃P的特定问颕。假设一个客户端连接已经关闭并且一个10请求返回一个错误标志,
然后我们知道客户端已经关闭。在参数 CompletionKey中,我们传递了一个指向结构 Client Context的指针,该结构中包含了客户端的
特定数据。如果我们释放这个 Client Context结构占用的内存,并且同一个客户端处理的一些其它l/0请求返回了错误代码,我们通过
转换参数 Complet monKey为一个指向 Iient coηtext结构的指针并试图访问或删除它,会发生什么呢?一个非法访问出现了!
这个问题的解决方法是添加一个数字到结构中,包含等待的丨/0请求的数量( m nNumberOfPend ing0),然后当我们知道没有等待的1/0
请求时删除这个结构。这个功能通过函数 Enter loLoop(…)和 Re l easeClientContext(…)来实现。
源码略读
源码的目标是提供一系列简单的类来处理所有I0CP编码中的问题。源码也提供了一系列通信和C/S软件中经常使用的函数,如文件接收
传送函数,逻辑线程池处理,等等。下图功能性的图解说明了10C尸类源码。
Client
Client
Client
Connection
WSASyndo
Listner Thread
Asynchronous I/O Calls
Iupilr outpur Comple tion Porl(IOCP)
gical Workers
IO Worker threads
Virtual functions
Useful Soket functions
Work queue
Windowsmessages
GUI
我们有几个10工作者线程通过完成端口来处理异步10请求,这些工作者线程调用一些虚函数,这些虚函数可以把需要大量计算的请求放
到一个工作队列中。逻辑工作者通过类中提供的这些函数从队列中取出任务、处理并发回结果。GU|经常与主类通信,通过 Windows消息
(因为WFC不是线程安全的)、通过调用函数或通过使用共享的变量。下图图显示了类结构纵览。
CIOCPBuffer
IOCPS
ObiTer
Clientcontext
Your own inherited class
GUI
上图中的类说明如下
G|0 CPBuffer:管理异步请求的缓存的类。
0CPS:处理所有通信的主类
Job Item:保存逻辑工作者线程要处理的任务的结构。
ClientContex:保存客户端特定信息的结构(如状态、数据,等等)。
缓冲设计:c0 CPBuffer类
使用异步1/0调用时,我们必须提供私有的缓冲区供0操作使用。当我们将帐号信息放入分配的缓冲供使用时有许多情况需要考虑。
分配和釋放内存代价高,因此我们应重复使用以及分配的缓冲(内存),因此我们将缓冲保存在列表结构中,如下所示:
Free Buffer List
CCritica I Section m Free BufferListLock
CPtrList m free Bufferlist
OccupiedBuffer List. (Buffers that is currently used)
CCritical Sect ion m BufferListLock
CPtrList m BufferList
Now we use the function AllocateBuffer(.)
to al locate memory or reuse a buffer
有时,当异步10调用完成后,缓冲里可能不是完整的包,因此我们需要分割缓冲去取得完整的信息。在CI0CPS类中提供了 SplitBuffer
函数
同样,有时候我们需要在缓冲间拷贝信息,10PS类提供了 AddAndFush函数。
众所周知,我们也需要添加序号和状态( oType变量,I0 ZeroReadcompleted,等等)到我们的缓冲中。我们也需要有将数据转换到字节
流或将字节流转换到数据的方法,CI0 CPBuffer也提供了这些函数。
以上所有问题都在C|0 CPBuffer中解决。
如何使用源代码
从0尸继承你自己的类(如图3),实现I0PS类中的虚函数(例如, threadpool〕,在任何类型的服务端或客户端中实现使用少量的线
程有效地管理大量的连接
启动和关闭服务端/客户端
调用下面的函数启动服务端
BOOL Start(int nPort=999, int i MaxNumConnect ions=1201
nt i Max l kers=l int nofWorkers=1
int i Max Number OfFreeBuffer=0
nt i Max NumberOfFreeContext=0
bool bOr dered Send=TRUE
boo bOr dered Read=TRUE
int i Number OfPend I ingReads=4)
nPort,服务端侦听的端口.(-1客户端模式.)
i MaxNum ions,允许最大的连接数.(使用较大的数.)
i Max WoRkers,I/0工作线程数
nOfWorker s,逻辑工作者数量№ umber of log ica workers.(可以在运行时改变.)
i MaxNumber0 fFree Buffer,重复使用的缓冲最大数.(-1不使用,0=不跟)
MaxNumber0 FFreecontext,重复使用的客户端信息对象数(-1for不使用,0=不限)
· bOrderedRead,顺序读取.(我们已经在3.6.2.处讨论过)
b0 rderedSend,顺序写入.(我们已经在3.6.2.处讨论过)
Number OfPendl ingReads,等待读取数据时未决的异步读取循环数
连接到远程服务器(客户端模式ηPort=-1),调用函数:
Code Connect (const CStr ing &str I PAddr, int nPort)
str| PAddr,远程服务器的IP地址
Port,端口
调用 Shut Down(关闭连接,例如:
f(! m Iocp. Start(-1,1210,2,1,0,0)
AfxMessageBox( Error could not start the Client
m iocp, Shut O)
源代码描述
更多关于源代码的信息请参考代码里的注释。
事件/虚函数
Not Connection,新的连接已接受
Not i fyNevclientContext,空的 I ient context结构被分配
Not i fyI sconnectedclient,客户端连接断开
Process job,逻辑工作者需要处理一个工作
Not i fyRece i vedPackage,新的包到达
Not i fyF i completed,文件传送完成
重要变量
所有变量共享使用时必须加锁避免存取违例,所有需要加锁的变量,名称为XXX则锁变量名称为 XXXLock
m_ ContextMapLock,数据保护锁
m_ˆonteκtMa:,保存所有客户端数据( socket,客户端数据.等等)
m Number0 fActiveconnecti ons,保存已连接的连接数
重要函数
GetNumber ofConnecti ons(,返回连接数
Str ing GetHostAdress( Client context*p),提供客户端上下文,返回主机地址
BOOL ASendToAl I(c0 CPBuffer*pBuf),发送缓冲上下文到所有连接的客户端
Di sconnectcl ient( cString sID,根据客户端唯一编号,断开指定的客户端
CStr ing GetHostIP(,返回本地|P
Job tem* GetJob(),将 Job Item从队列中移出,如果没有job,返回NULL
BooL AddJob( Job ltem*pdob),添加Job到队列
BooL Setor ker s( int nThreads),设置可以任何时候调用的逻辑工作者数量
Di sconnectA(,断开所有客户端
ARead(…),异步读取
SEnd(…),异步发送,发送数据到客户端
ClientContex* Findclient( GString strclient),根据字符串1D寻找客户(非线程安全)
· Di sconnectClient( Client context* cOntext, bool bGr acefu|干 FALSE),端口客户
Di sconnectA(,端口所有客户
● StartSendFile( ClientContext* cOntext),根据 Client Context结构发送文件(使用经优化的 transmitfile(..)函数)
Prepare Rece iveFile(.),接收文件准备。调用该函数时,所有进入的字节流已被写入到文件
Prepare SendFile(),打开文件并发送包含文件信息的数据包。函数禁用 SEnd()函数,直到文件传送关闭或中断。
Disablesendfile(),禁止发送文件模式
· Di sab leRecevideFile),禁止文件接收模式
文件传输
文件传输使用 Winsock2.0中的 TransmitFile函数。 Transmitfile函数通过连接的 socket句柄传送文件数据。函数使用操作系统的高
速缓冲管理器( cache manager〕接收文件数据,通过 sockets提供高性能的文件数据传输。
在 TransmitFile函数返回前,所有其他发送或写入到该 socket的操作都将无法执行,因为这将使文件数据混乱。因此,在 PrepareSendFile()
函数调用之后,所有 SEnd都被禁止。因为操作系统连续读取文件数据,你可以使用 FILE FLAG SE0 UENT IAL SCAN参数来优化高速缓冲性
能
发送文件时我们使用了内核异步操作 TE USE KERNEL APC)。 TE USE KERNEL APC的使用可以更好地提升性能。有可能,无论如
何, TransmitFile在线程中的大量使用,这种情形可能会阻止APGs的调用
文件传输按如下顺序执行:服务器调用 Prepare SendFile()函数初始化文件传输。客户端接收文件信息时,调用 PrepareRece iveFi le(.)
作接收前的准备,并发送一个包到服务器告知开始文件传送。当包到达服务器端,服务器端调用 Start SendFile()采用高性能的
r ansi tFile函数发送指定文件。
源代码示例
提供的源代码演示代码是一个echo客户端/服务器端稈序,并提供了对文件传输的支持(图4)。在代码中,M0cP类从0CP继承,处
理客户端/服务器端的通讯,所涉及的虚函数可以参见4.1.1处。
客户端或服务器端最重要的部分是虚函数 Not i fyRecei vedPackage,定义如下
void My 10CP:: Not i fyRece i vedPackage(ClOCPBuffer *pOver lapBuff
int nSize, Cl ient Context *pContext)
BYTE Package Type=pOver l apBuff->GetPackageType o
switch (Package Type
case Job SendText2client
Packagetext (pOver l apBuff, nSize, pContext)
break
case Job SendFi leInfo
PackageFileTransfer(pOver l apBuff, nSize, pContext
case Job Star tFileTransfer
PackageStartFi leTransfer(pOver lapBuff, nSize, pContext)
br eak
(系统自动生成,下载前可以参看下载内容)
下载文件列表
相关说明
- 本站资源为会员上传分享交流与学习,如有侵犯您的权益,请联系我们删除.
- 本站是交换下载平台,提供交流渠道,下载内容来自于网络,除下载问题外,其它问题请自行百度。
- 本站已设置防盗链,请勿用迅雷、QQ旋风等多线程下载软件下载资源,下载后用WinRAR最新版进行解压.
- 如果您发现内容无法下载,请稍后再次尝试;或者到消费记录里找到下载记录反馈给我们.
- 下载后发现下载的内容跟说明不相乎,请到消费记录里找到下载记录反馈给我们,经确认后退回积分.
- 如下载前有疑问,可以通过点击"提供者"的名字,查看对方的联系方式,联系对方咨询.