《一个简单的完成端口类》由会员分享,可在线阅读,更多相关《一个简单的完成端口类(18页珍藏版)》请在金锄头文库上搜索。
1、一个简单的完成端口(服务端/客户端)类作者:spinoza翻译:麦子芽儿,POWERCPP(后面部分内容)下载源代码原文网址: http:/ 本文提出了一些IOCP编程中出现的实际问题的解决方法,并提供了一个简单的 echo版本的可以传输文件的客户端/服务器程序。程序截图:1.1环境要求本文读者需要熟悉C+、TCP/IP、Socket编程、MFC,和多线程。源码使用Winsock 2.0 和IOCP技术,要求:Windows NT/2000 或以上:要求 Windows NT3.5 或以后版本Windows 95/98/ME :不支持Visual C+.NET或完整更新过的Visual C+
2、6.01.2摘要当你开发不同类型的软件,你迟早必须处理C/S的开发。对一个程序员来说,写 一个通用的C/S编码是一项困难的工作。本文档提供了一份简单但是功能强大的 C/S源码,可以扩展到任何类型的C/S应用程序中。这份源码使用了高级的IOCP 技术,该技术可以高效的服务于多客户端。IOCP提供了解决每个客户端占用一 个线程的瓶颈问题的办法,只使用几个处理线程,异步输入/输出来发送/接收。 IOCP技术被广泛应用在各种类型的高效服务端,例如Apache等。这份源码也 提供了一系列的在处理通信和C/S软件中经常使用的功能,如文件接收/传送功 能和逻辑线程池管理。本文重点在于出现在IOCP程序API
3、中实用的解决方案, 以及关于源码的全面的文档。另外,一份简单的echo版的可处理多连接和文件 传输的C/S程序也在这里提供。2.1引言本文提出了一个类,可以用在客户端和服务端。这个类使用IOCP(Input Output Completion Ports) 和异步(非阻塞)机制。通过这些简单的源码,你可以:服务或连接多客户端和服务端异步发送或接收文件创建并管理一个逻辑工作者线程池,用以处理繁重的客户端/服务器请求或计找到一份全面但简单的解决客户端/服务器通信的源码是件困难的事情。在网络 上找到的源码要么太复杂(超过20个类),要命没有提供足够的效率。本源码 的设计尽可能简单,并提供了充足的文档
4、。在这篇文章中,我们简洁的呈现出了 Winsock API 2.0 支持的IOCP技术,说明了在编写过程中出现的棘手问题,并 提出了每一个问题的解决方案。2.2异步完成端口介绍如果一个服务器应用程序不能同时支持多个客户端,那是毫无意义的,为此,通 常使用异步I/O请求和多线程。根据定义,一个异步I/O请求会立即返回,而留 下I/O请求处于等待状态。有时,I/O异步请求的结果必须与主线程同步。这可 以通过几种不同方式解决。同步可以通过下面的方式实现:使用事件-当异步请求结束时会马上触发一个信号。这种方式的缺点是线程 必须检查并等待事件被触发使用GetOverlappedResult 函数-这种方
5、式与上一种方式有相同的缺点。使用Asynchronous Procedure Calls (或APC )-这种方式有几个缺点。首 先,APC总是在请求线程的上下文中被请求;第二,为了执行APC,请求线程必 须在可变等候状态下挂起。使用IOCP -这种方式的缺点是必须解决很多实际的棘手的编程问题。编写 IOCP可能有点麻烦。2.2.1为什么使用IOCP ?通过使用IOCP,我们可以解决每个客户端占用一个线程的问题。通常普遍认 为如果软件不能运行在真正的多处理器机器上,执行能力会严重降低。线程是系 统资源,而这些资源既不是无限的,也不是低价的。IOCP提供了一种方式来使用几个线程公平的处理多客户端
6、的输入/输出。线程 被挂起,不占用CPU周期直到有事可做。2.3什么是IOCP ?我们已经看到IOCP只是一个线程同步对象,类似于信号灯,因此IOCP并不是 一个复杂的概念。一个IOCP对象与几个支持待定异步I/O请求的I/O对象绑定。 一个可以访问IOCP的线程可以被挂起,直到一个待定的异步I/O请求结束。3 IOCP是怎样工作的?要使用IOCP,你必须处理三件事情,绑定一个socket到完成端口,创建异步I/O 请求,并与线程同步。为从异步I/O请求获得结果,如那个客户端发出的请求, 你必须传递两个参数:CompletionKey 参数和OVERLAPPED 结构。3.1关键参数第一个参数
7、:CompletionKey,是一个DWORD类型的变量。你可以传递任何你 想传递的唯一值,这个值将总是同该对象绑定。正常情况下会传递一个指向结构 或类的指针,该结构或类包含了一些客户端的指定对象。在源码中,传递的是一 个指向ClientContext 的指针。3.2 OVERLAPPED 参数这个参数通常用来传递异步I/O请求使用的内存缓冲。很重要的一点是:该数据 将会被锁定并不允许从物理内存中换出页面(page out)。3.3绑定一个socket到完成端口一旦创建完成一个完成端口,可以通过调用CreateIoCompletionPort 函数来绑定socket到完成端口。形式如下:BOO
8、L 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。他们也需要一个参数: WSABUF,这个参数包含了一个指向缓
9、冲的指针。一个重要的规则是:通常当服 务器/客户端响应一个I/O操作,不是直接响应,而是提交给完成端口,由I/O 工作者线程来执行。这么做的原因是:我们希望公平的分割CPU周期。通过发 送状态给完成端口来发出I/O请求,如下:BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,pOverlapBuff-GetUsed(),(DWORD) pContext,&pOverlapBuff-m_ol);3.5与线程同步与I/O工作者线程同步是通过调用GetQueuedCompletionStatus 函数来实现的 (如下)。这个函数
10、也提供了 CompletionKey 参数和OVERLAPPED 参数,如下:BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, / handle to completion portLPDWORD lpNumberOfBytes, / bytes transferredPULONG_PTR lpCompletionKey, / file completion keyLPOVERLAPPED *lpOverlapped, / bufferDWORD dwMilliseconds / optional timeout value);3.6
11、四个棘手的IOCP编码问题和解决方法使用IOCP时会出现一些问题,其中有一些不是很直观的。在使用IOCP的多线 程编程中,一个线程函数的控制流程不是笔直的,因为在线程和通讯直接没有关 系。在这一章节中,我们将描述四个不同的问题,可能在使用IOCP开发客户端 /服务器应用程序时会出现,分别是:The WSAENOBUFS error problem. ( WSAENOBUFS 错误问题)The package reordering problem.(包重构 问题)The access violation problem. (访 问 非法 问 题)3.6.1 WSAENOBUFS 问题 这个问题通
12、常很难靠直觉发现,因为当你第一次看见的时候你或许认为是一个内 存泄露错误。假定已经开发完成了你的完成端口服务器并且运行的一切良好,但 是当你对其进行压力测试的时候突然发现服务器被中止而不处理任何请求了,如 果你运气好的话你会很快发现是因为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栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGES
15、IZE 缓冲的异步 WSARead(.)去读,解决了 WSAENOBUFS 问题。但是这种解决方法降低了服务器的吞吐量。总结: 解决方法一: 投递使用空缓冲区的receive操作,当操作返回后,使用非阻塞的recv来进行 真实数据的读取。因此在完成端口的每一个连接中需要使用一个循环的操作来不 断的来提交空缓冲区的receive操作。解决方法二:在投递几个普通含有缓冲区的receive操作后,进接着开始循环投递一个空缓冲 区的receive操作。这样保证它们按照投递顺序依次返回,这样我们就总能对被 锁定的内存进行解锁。3.6.2包重构问题.尽管使用IO完成端口的待发操作将总是按照他们发送的顺序来完成,线 程调度安排可能使绑定到完成端口的实际工作不按指定的顺序来处理。例如,如 果你有两个I/O工作者线程,你可能接收到字节块2,字节块1,字节块3。这就意味着:当你通过向I/O完成端口提交请求数据发送数据时,数据实际上用 重新排序过的顺序发送了。这可以通过只使用一个工作者线程来解决,并只提交一个I/O请求,等待它完成。 但是如果这么做,我们就失去了 IOCP的长处。解决这个问题的一个简单实用办法