ImageVerifierCode 换一换
格式:DOCX , 页数:15 ,大小:93.30KB ,
资源ID:8503396      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/8503396.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(一个简单的完成端口服务端客户端类.docx)为本站会员(b****6)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

一个简单的完成端口服务端客户端类.docx

1、一个简单的完成端口服务端客户端类一个简单的完成端口(服务端/客户端)类作者:spinoza翻译:麦子芽儿, POWERCPP(后面部分内容) 下载源代码原文网址:源码使用了高级的完成端口(IOCP)技术,该技术可以有效地服务于多客户端。本文提出了一些IOCP编程中出现的实际问题的解决方法,并提供了一个简单的echo版本的可以传输文件的客户端/服务器程序。程序截图:1.1 环境要求本文读者需要熟悉C+、TCP/IP、Socket编程、MFC,和多线程。源码使用Winsock 2.0和IOCP技术,要求:Windows NT/2000或以上:要求Windows NT3.5或以后版本Windows

2、95/98/ME:不支持Visual C+.NET,或完整更新过的Visual C+ 6.01.2 摘要当你开发不同类型的软件,你迟早必须处理C/S的开发。对一个程序员来说,写一个通用的C/S编码是一项困难的工作。本文档提供了一份简单但是功能强大的C/S源码,可以扩展到任何类型的C/S应用程序中。这份源码使用了高级的IOCP技术,该技术可以高效的服务于多客户端。IOCP提供了解决“每个客户端占用一个线程”的瓶颈问题的办法,只使用几个处理线程,异步输入/输出来发送/接收。IOCP技术被广泛应用在各种类型的高效服务端,例如Apache等。这份源码也提供了一系列的在处理通信和C/S软件中经常使用的功

3、能,如文件接收/传送功能和逻辑线程池管理。本文重点在于出现在IOCP程序API中实用的解决方案,以及关于源码的全面的文档。另外,一份简单的echo版的可处理多连接和文件传输的C/S程序也在这里提供。2.1 引言本文提出了一个类,可以用在客户端和服务端。这个类使用IOCP(Input Output Completion Ports)和异步(非阻塞)机制。通过这些简单的源码,你可以: 服务或连接多客户端和服务端 异步发送或接收文件 创建并管理一个逻辑工作者线程池,用以处理繁重的客户端/服务器请求或计算找到一份全面但简单的解决客户端/服务器通信的源码是件困难的事情。在网络上找到的源码要么太复杂(超过

4、20个类),要命没有提供足够的效率。本源码的设计尽可能简单,并提供了充足的文档。在这篇文章中,我们简洁的呈现出了Winsock API 2.0支持的IOCP技术,说明了在编写过程中出现的棘手问题,并提出了每一个问题的解决方案。2.2 异步完成端口介绍如果一个服务器应用程序不能同时支持多个客户端,那是毫无意义的,为此,通常使用异步I/O请求和多线程。根据定义,一个异步I/O请求会立即返回,而留下I/O请求处于等待状态。有时,I/O异步请求的结果必须与主线程同步。这可以通过几种不同方式解决。同步可以通过下面的方式实现: 使用事件 当异步请求结束时会马上触发一个信号。这种方式的缺点是线程必须检查并等

5、待事件被触发 使用GetOverlappedResult函数 这种方式与上一种方式有相同的缺点。 使用Asynchronous Procedure Calls(或APC) 这种方式有几个缺点。首先,APC总是在请求线程的上下文中被请求;第二,为了执行APC,请求线程必须在可变等候状态下挂起。 使用IOCP 这种方式的缺点是必须解决很多实际的棘手的编程问题。编写IOCP可能有点麻烦。2.2.1 为什么使用IOCP?通过使用IOCP,我们可以解决“每个客户端占用一个线程”的问题。通常普遍认为如果软件不能运行在真正的多处理器机器上,执行能力会严重降低。线程是系统资源,而这些资源既不是无限的,也不是低

6、价的。IOCP提供了一种方式来使用几个线程“公平的”处理多客户端的输入/输出。线程被挂起,不占用CPU周期直到有事可做。2.3 什么是IOCP?我们已经看到IOCP只是一个线程同步对象,类似于信号灯,因此IOCP并不是一个复杂的概念。一个IOCP对象与几个支持待定异步I/O请求的I/O对象绑定。一个可以访问IOCP的线程可以被挂起,直到一个待定的异步I/O请求结束。3 IOCP是怎样工作的?要使用IOCP,你必须处理三件事情,绑定一个socket到完成端口,创建异步I/O请求,并与线程同步。为从异步I/O请求获得结果,如那个客户端发出的请求,你必须传递两个参数:CompletionKey参数和

7、OVERLAPPED结构。3.1 关键参数第一个参数:CompletionKey,是一个DWORD类型的变量。你可以传递任何你想传递的唯一值,这个值将总是同该对象绑定。正常情况下会传递一个指向结构或类的指针,该结构或类包含了一些客户端的指定对象。在源码中,传递的是一个指向ClientContext的指针。3.2 OVERLAPPED参数这个参数通常用来传递异步I/O请求使用的内存缓冲。很重要的一点是:该数据将会被锁定并不允许从物理内存中换出页面(page out)。3.3 绑定一个socket到完成端口一旦创建完成一个完成端口,可以通过调用CreateIoCompletionPort函数来绑定

8、socket到完成端口。形式如下: BOOL IOCPS:AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey) HANDLE h = CreateIoCompletionPort(HANDLE) socket, hCompletionPort, dwCompletionKey, m_nIOWorkers); return h = hCompletionPort;3.4 响应异步I/O请求响应具体的异步请求,调用函数WSASend和WSARecv。他们也需要一

9、个参数:WSABUF,这个参数包含了一个指向缓冲的指针。一个重要的规则是:通常当服务器/客户端响应一个I/O操作,不是直接响应,而是提交给完成端口,由I/O工作者线程来执行。这么做的原因是:我们希望公平的分割CPU周期。通过发送状态给完成端口来发出I/O请求,如下:BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, pOverlapBuff-GetUsed(), (DWORD) pContext, &pOverlapBuff-m_ol);3.5 与线程同步与I/O工作者线程同步是通过调用GetQueuedCompletio

10、nStatus函数来实现的(如下)。这个函数也提供了CompletionKey参数和OVERLAPPED参数,如下:BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, / handle to completion port LPDWORD lpNumberOfBytes, / bytes transferred PULONG_PTR lpCompletionKey, / file completion key LPOVERLAPPED *lpOverlapped, / buffer DWORD dwMilliseconds / opti

11、onal timeout value );3.6 四个棘手的IOCP编码问题和解决方法使用IOCP时会出现一些问题,其中有一些不是很直观的。在使用IOCP的多线程编程中,一个线程函数的控制流程不是笔直的,因为在线程和通讯直接没有关系。在这一章节中,我们将描述四个不同的问题,可能在使用IOCP开发客户端/服务器应用程序时会出现,分别是:The WSAENOBUFS error problem.(WSAENOBUFS错误问题) The package reordering problem.(包重构问题) The access violation problem.(访问非法问题)3.6.1 WSAE

12、NOBUFS问题这个问题通常很难靠直觉发现,因为当你第一次看见的时候你或许认为是一个内存泄露错误。假定已经开发完成了你的完成端口服务器并且运行的一切良好,但是当你对其进行压力测试的时候突然发现服务器被中止而不处理任何请求了,如果你运气好的话你会很快发现是因为WSAENOBUFS 错误而影响了这一切。 每当我们重叠提交一个send或receive操作的时候,其中指定的发送或接收缓冲区就被锁定了。当内存缓冲区被锁定后,将不能从物理内存进行分页。操作系统有一个锁定最大数的限制,一旦超过这个锁定的限制,那么就会产生WSAENOBUFS 错误了。如果一个服务器提交了非常多的重叠的receive在每一个连

13、接上,那么限制会随着连接数的增长而变化。如果一个服务器能够预先估计可能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的receive在每一个连接上。因为当你提交操作没有缓冲区时,那么也不会存在内存被锁定了。使用这种办法后,当你的receive操作事件完成返回时,该socket底层缓冲区的数据会原封不动的还在其中而没有被读取到receive操作的缓冲区来。此时,服务器可以简单的调用非阻塞式的recv将存在socket缓冲区中的数据全部读出来,一直到recv返回 WSAEWOULDBLOCK 为止。 这种设计非常适合那些可以牺牲数据吞吐量而换取巨大 并发连接数的服务器。当然,你也需要意识到

14、如何让客户端的行为尽量避免对服务器造成影响。在上一个例子中,当一个零缓冲区的receive操作被返回后使 用一个非阻塞的recv去读取socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个receive 来取代非阻塞的recv来进行数据接收。(这比你使用1个缺省的8K缓冲区来接收要好的多。) 源码中提供了一个简单实用的解决WSAENOBUF错误的办法。我们执行了一个零字节缓冲的异步WSARead(.)(参见 OnZeroByteRead(.)。当这个请求完成,我们知道在TCP/IP栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGESI

15、ZE缓冲的异步WSARead(.)去读,解决了WSAENOBUFS问题。但是这种解决方法降低了服务器的吞吐量。总结: 解决方法一: 投递使用空缓冲区的 receive操作,当操作返回后,使用非阻塞的recv来进行真实数据的读取。因此在完成端口的每一个连接中需要使用一个循环的操作来不断的来提交空缓冲区的receive操作。 解决方法二: 在投递几个普通含有缓冲区的receive操作后,进接着开始循环投递一个空缓冲区的receive操作。这样保证它们按照投递顺序依次返回,这样我们就总能对被锁定的内存进行解锁。 3.6.2 包重构问题. . 尽管使用IO完成端口的待发操作将总是按照他们发送的顺序来完

16、成,线程调度安排可能使绑定到完成端口的实际工作不按指定的顺序来处理。例如,如果你有两个I/O工作者线程,你可能接收到“字节块2,字节块1,字节块3”。这就意味着:当你通过向I/O完成端口提交请求数据发送数据时,数据实际上用重新排序过的顺序发送了。这可以通过只使用一个工作者线程来解决,并只提交一个I/O请求,等待它完成。但是如果这么做,我们就失去了IOCP的长处。解决这个问题的一个简单实用办法是给我们的缓冲类添加一个顺序数字,如果缓冲顺序数字是正确的,则处理缓冲中的数据。这意味着:有不正确的数字的缓冲将被存下来以后再用,并且因为执行原因,我们保存缓存到一个HASH MAP对象中(如m_SendB

17、ufferMap 和 m_ReadBufferMap)。获取这种解决方法的更多信息,请查阅源码,仔细查看IOCPS类中如下的函数:GetNextSendBuffer (.) and GetNextReadBuffer(.), to get the ordered send or receive buffer. IncreaseReadSequenceNumber(.) and IncreaseSendSequenceNumber(.), to increase the sequence numbers. 3.6.3 异步等待读 和 字节块包处理问题最通用的服务端协议是一个基于协议的包,首先X个

18、字节代表包头,包头包含了详细的完整的包的长度。服务端可以读包头,计算出需要多少数据,继续读取直到读完一个完整的包。当服务端同时只处理一个异步请求时工作的很好。但是,如果我们想发挥IOCP服务端的全部潜能,我们应该启用几个等待的异步读事件,等待数据到达。这意味着几个异步读操作是不按顺序完成的,通过等待的读事件返回的字节块流将不会按顺序处理。而且,一个字节块流可以包含一个或几个包,也可能包含部分包,如下图所示:这个图形显示了部分包(绿色)和完整包(黄色)是怎样在不同字节块流中异步到达的。这意味着我们必须处理字节流来成功的读取一个完整的包。而且,我们必须处理部分包(图表中绿色的部分)。这就使得字节流

19、的处理更加困难。这个问题的完整解决方法在IOCPS类的ProcessPackage()函数中。3.6.4 访问非法问题这是一个较小的问题,代码设计导致的问题更胜于IOCP的特定问题。假设一个客户端连接已经关闭并且一个I/O请求返回一个错误标志,然后我们知道客户端已经关闭。在参数CompletionKey中,我们传递了一个指向结构ClientContext的指针,该结构中包含了客户端的特定数据。如果我们释放这个ClientContext结构占用的内存,并且同一个客户端处理的一些其它I/O请求返回了错误代码,我们通过转换参数CompletionKey为一个指向ClientContext结构的指针并

20、试图访问或删除它,会发生什么呢?一个非法访问出现了!这个问题的解决方法是添加一个数字到结构中,包含等待的I/O请求的数量(m_nNumberOfPendingIO),然后当我们知道没有等待的I/O请求时删除这个结构。这个功能通过函数EnterIoLoop() 和ReleaseClientContext()来实现。3.7 源码略读源码的目标是提供一系列简单的类来处理所有IOCP编码中的问题。源码也提供了一系列通信和C/S软件中经常使用的函数,如文件接收/传送函数,逻辑线程池处理,等等。上图功能性的图解说明了IOCP类源码。我们有几个IO工作者线程通过完成端口来处理异步IO请求,这些工作者线程调用

21、一些虚函数,这些虚函数可以把需要大量计算的请求放到一个工作队列中。逻辑工作者通过类中提供的这些函数从队列中取出任务、处理并发回结果。GUI经常与主类通信,通过Windows消息(因为MFC不是线程安全的)、通过调用函数或通过使用共享的变量。图三上图显示了类结构纵览。图3中的类说明如下: CIOCPBuffer:管理异步请求的缓存的类。 IOCPS:处理所有通信的主类。 JobItem:保存逻辑工作者线程要处理的任务的结构。 ClientContex:保存客户端特定信息的结构(如状态、数据,等等)。3.7.1 缓冲设计 - CIOCPBuffer类使用异步I/O调用时,我们必须提供私有的缓冲区供

22、I/O操作使用。当我们将帐号信息放入分配的缓冲供使用时有许多情况需要考虑:.分配和释放内存代价高,因此我们应重复使用以及分配的缓冲(内存),因此我们将缓冲保存在列表结构中,如下所示:/ Free Buffer List. CCriticalSection m_FreeBufferListLock; CPtrList m_FreeBufferList;/ OccupiedBuffer List. (Buffers that is currently used) CCriticalSection m_BufferListLock; CPtrList m_BufferList; / Now we u

23、se the function AllocateBuffer(.) / to allocate memory or reuse a buffer.有时,当异步I/O调用完成后,缓冲里可能不是完整的包,因此我们需要分割缓冲去取得完整的信息。在CIOCPS类中提供了SplitBuffer函数。同样,有时候我们需要在缓冲间拷贝信息,IOCPS类提供了AddAndFlush函数。. 众所周知,我们也需要添加序号和状态(IOType 变量, IOZeroReadCompleted, 等等)到我们的缓冲中。. 我们也需要有将数据转换到字节流或将字节流转换到数据的方法,CIOCPBuffer也提供了这些函数

24、。以上所有问题都在CIOCPBuffer中解决。3.8 如何使用源代码从IOCP继承你自己的类(如图3),实现IOCPS类中的虚函数(例如,threadpool),在任何类型的服务端或客户端中实现使用少量的线程有效地管理大量的连接。3.8.1 启动和关闭服务端/客户端调用下面的函数启动服务端BOOL Start(int nPort=999,int iMaxNumConnections=1201, int iMaxIOWorkers=1,int nOfWorkers=1, int iMaxNumberOfFreeBuffer=0, int iMaxNumberOfFreeContext=0, B

25、OOL bOrderedSend=TRUE, BOOL bOrderedRead=TRUE, int iNumberOfPendlingReads=4);nPort服务端侦听的端口. ( -1 客户端模式.)iMaxNumConnections 允许最大的连接数. (使用较大的数.)iMaxIOWorkers I/O工作线程数nOfWorkers 逻辑工作者数量Number of logical workers. (可以在运行时改变.)iMaxNumberOfFreeBuffer 重复使用的缓冲最大数. (-1 不使用, 0= 不限)iMaxNumberOfFreeContext 重复使用的客

26、户端信息对象数 (-1 for 不使用, 0= 不限)bOrderedRead 顺序读取. (我们已经在 3.6.2. 处讨论过)bOrderedSend 顺序写入. (我们已经在 3.6.2. 处讨论过)iNumberOfPendlingReads等待读取数据时未决的异步读取循环数连接到远程服务器(客户端模式nPort=-1),调用函数:CodeConnect(const CString &strIPAddr, int nPort).strIPAddr 远程服务器的IP地址.nPort 端口调用ShutDown()关闭连接例如:if(!m_iocp.Start(-1,1210,2,1,0,0

27、)AfxMessageBox(Error could not start the Client);.m_iocp.ShutDown();4.1 源代码描述更多关于源代码的信息请参考代码里的注释。4.1.1 虚函数NotifyNewConnection 新的连接已接受NotifyNewClientContext 空的ClientContext结构被分配NotifyDisconnectedClient 客户端连接断开ProcessJob 逻辑工作者需要处理一个工作NotifyReceivedPackage 新的包到达NotifyFileCompleted 文件传送完成。4.1.2 重要变量所有变量

28、共享使用时必须加锁避免存取违例,所有需要加锁的变量,名称为XXX则锁变量名称为XXXLock。m_ContextMapLock; 保存所有客户端数据(socket,客户端数据,等等)ContextMap m_ContextMap; m_NumberOfActiveConnections 保存已连接的连接数4.1.3 重要函数GetNumberOfConnections() 返回连接数CString GetHostAdress(ClientContext* p) 提供客户端上下文,返回主机地址BOOL ASendToAll(CIOCPBuffer *pBuff); 发送缓冲上下文到所有连接的客户

29、端DisconnectClient(CString sID) 根据客户端唯一编号,断开指定的客户端CString GetHostIP() 返回本地IPJobItem* GetJob() 将JobItem从队列中移出, 如果没有job,返回 NULLBOOL AddJob(JobItem *pJob) 添加Job到队列BOOL SetWorkers(int nThreads) 设置可以任何时候调用的逻辑工作者数量DisconnectAll(); 断开所有客户端ARead() 异步读取ASend() 异步发送,发送数据到客户端ClientContext* FindClient(CString st

30、rClient) 根据字符串ID寻找客户(非线程安全)DisconnectClient(ClientContext* pContext, BOOL bGraceful=FALSE); 端口客户DisconnectAll() 端口所有客户StartSendFile(ClientContext *pContext) 根据ClientContext结构发送文件(使用经优化的transmitfile(.) 函数)PrepareReceiveFile(.) 接收文件准备。调用该函数时,所有进入的字节流已被写入到文件。PrepareSendFile(.) 打开文件并发送包含文件信息的数据包。函数禁用ASe

31、nd(.)函数,直到文件传送关闭或中断。DisableSendFile(.) 禁止发送文件模式DisableRecevideFile(.) 禁止文件接收模式5.1 文件传输文件传输使用Winsock 2.0 中的TransmitFile函数。TransmitFile函数通过连接的socket句柄传送文件数据。函数使用操作系统的高速缓冲管理器(cache manager)接收文件数据,通过sockets提供高性能的文件数据传输。异步文件传输要点:在TransmitFile函数返回前,所有其他发送或写入到该socket的操作都将无法执行,因为这将使文件数据混乱。因此,在PrepareSendFile()函数调用之后,所有ASend都被禁止。因为

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1