对于开发一个不考虑跨平台,只在 Windows Server 环境下运行的高性能服务器来说,IOCP无疑是一个最优的解决方案。最近一个项目要用到 IOCP ,特地找了些资料。网上的资料很多,但很多都是以基础性的介绍为主,代码也是些经典书籍上的标准代码。这些代码对理解IOCP无疑是很重要的,但对于高性能服务器开发来说,细节的实现则似乎更加重要。根据自己最近做的一个项目,有几点体会,特记录下来,以备后查。
1、是在写服务端而不是在写客户端。
服务端与客户端绝对是两码事。在客户端我们提倡 Create/New 和 Free/Dispose,随用随申请,不用即释放。但在服务端要尽量避免这样做。在客户端可以随时使用 string 类型,但在服务端也必须尽量避免使用 string 。string使用起来异常方便,但我们看看编译后的代码恐怕就会只冒冷汗:原来编译器为string的方便做了那么多额外的工作。客户端要为客户解决内存,但服务端能“浪费”则“浪费”。
2、内存管理。
不得不再次佩服一下某大牛说的话:“玩服务器就是玩内存”。
内存管理不当就会造成内存泄漏和内存碎片。对于客户端而言,内存碎片几乎不算是问题。内存泄漏那么一点点也可以接受。但对于 24 * 7 的服务器而言,这却绝对致命,其重要性甚至超过了 IOCP 本身。
关于内存泄漏,只要记得保证申请和释放动作的对称性即可,外加一系列的测试工具,基本就可以把这个问题解决。
其次就是内存碎片。内存碎片问题的重要性绝不亚于内存泄漏。造成碎片的原因也是防不胜防。简单的如每次的 New 和 Dispose ,Create 和 Free ,隐晦一点的如 string 类型的操作。
解决办法:
首先对于Create和Free,尽量少用。换句话说,尽量少用封装。适当的封装是可以的,只要封装的层次不是太深。Delphi 提供了 VCL 源码,我们可以看看即使是直接继承 TObject 那也会多做多少工作!对于频繁调用的函数,不要采用虚拟函数。这些晚绑定的函数,想调用就得查找 VMT,很费时间。对于类的普通函数,由于进行了早绑定,这个和其他非类的常规一样,不会降低效率。其次,相应的,尽量使用结构和函数来代替类。对于结构,New 和 Dispose 也要尽量少用。要集中的使用来避免内存碎片。我们应该一次性把所预料的内存都申请完,服务器就得有服务器的样,放着那么多内存干什么。早晚都得申请,为什么在服务端启动的时候不一次性申请完,在服务端关闭的时候一次性释放掉?既避免了内存碎片又避免了以后的再申请操作,一举两得,何乐而不为?要知道内存分配和释放是非常昂贵的操作。不论是从时间上还是从稳定性上而言。再具体些,怎么保存这些申请到的内存?怎么保证在必要的时候可以很方便的再申请或及时的释放一些内存?我采用的是链表。在每次为一个数据结构申请内存的时候,先查看这个链表是否为空,如果不为空,就从这个链表中取出一个内存块,不需要真正调用函数申请。如果为空,再动态分配。使用完成后,把这个数据结构不释放,而是再把它插入到链表中去,以便下一次使用。再次,不用 string 用什么?用数组!用字符数组!就像C中的字符数组一样。就是这么简单~
3、使用使用 AcceptEx 代替 accept 。
AcceptEx 函数是微软的 Winsosk 扩展函数,这个函数和 accept 或 WSAAccept 是阻塞的,一直要到有客户端连接上来后 accept 才返回,而且,accept 本质上是在接受一个连接的同时再创建一个套接字。而创建一个套接字,对于 Windows 的网络模型而言,代价是非常大的。而 AcceptEx 则避免了这两个问题。首先它是异步的,直接就返回了。其次可以也是必须事先要和某一套接字绑定在一起。这样在接受一个连接时就不必再创建套接字了,而这个套接字我们可以事先使用 WSASocket 函数申请好,就像上面的预申请内存一样。总而言之一句话,“准备工作”一定要做好,到时需要拿来就是了。
这里面还有一个问题,刚开始创建和投递多少 AcceptEx 调用?万一不够用怎么办?这个问题我们可以把 FD_ACCEPT 事件和一个 Event 对象关联起来,然后用 WaitForSingleObject() 等待这个 Event ,若预投递的套接字不够用的话就会触发 FD_ACCEPT 事件, Event 受信,WaitForSingleObject() 返回,我们就重新再发出一些 AcceptEx 调用。
4、要利用好 GetQueuedCompletionStatus() 函数中的 lpCompletionKey 参数。
这个东西传递的是“单句柄数据”,换句话说,是和每个连接/套接字本身而不是在某个连接的 I/O 操作绑定的。在服务端设计当中,不可避免的,我们都会有一些只和该连接本身关联的一些数据(比如这个连接的套接字、客户端的IP,连接的会话密钥等等),如果采用传统的操作手法,将会不可避免采用一些查询机制在每次收发数据时来获取这些信息(比如数据的解密密钥),但现在我们只需要再创建完成端口时把包含这些信息结构体的指针传入就行了,下次直接使用GetQueuedCompletionStatus()取得结构体指针就行了,无需再次查询。方便和高效之极。
5、关于 Delphi 下 WinSock 函数库的封装
这是 Delphi 相对于 C/C++ 特有的问题。这些库 M$ 都是以 C 头文件(.h文件)形式给出的。因此若想在 Delphi 上调用就需要把其中的 C 表达形式转换为 Delphi 表达形式。问题就出在转换这里。抛开在转换期间可能会转换错误以外,由于没有一个强制性的转换标准,所以就会造成好几个转换版本,既便他们都是正确的。这就造成调用时所采用的代码不同。就拿最常用的 GetQueuedCompletionStatus() 函数来说,M$定义如下:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds
);
其中第二、三、四个参数都是需要传入指针形式的。某个 Delphi 转换版本如下:
function GetQueuedCompletionStatus(CompletionPort: THandle;
var lpNumberOfBytesTransferred, lpCompletionKey: DWORD;
var lpOverlapped: POverlapped; dwMilliseconds: DWORD): BOOL; stdcall;
当然这个转换是没问题的,但关键是对于第二、三、四个参数它采用 var 而使得参数进行了引用传递。在调用时只需要填入参数,而不必再使用取运算符 @ 来填入参数地址。另外一个转换版本如下:
function GetQueuedCompletionStatus(CompletionPort: THandle;
lpNumberOfBytesTransferred, lpCompletionKey: PDWORD;
lpOverlapped: PPOverlapped; dwMilliseconds: DWORD): BOOL; stdcall;
这个版本没有采用 var 引用传递,而是采用的指针的值传递。调用时必须先使用运算符 @ 来取得参数地址。
那么这种差异就可能导致我们更换一个 WinSock 声明文件就不能正常编译的问题。更可怕的是编译通过,但是误把指针当引用而引起的运行时错误,更是防不胜防。所以我觉得在一个项目当中有必要统一一下。那么两种声明哪个更好些?虽然第一种在调用时代码可能书写较为美观,但我还是推荐第二种,不采用 var 引用传递的那种。原因有两点:一是这样最接近原 .h 头文件的表达形式,二是调用时也会明显看到是需要传入一个类型还是需要该指向该类型的一个指针。
其他的一些小问题:
1、既然决定采用 IOCP 了,那就不要考虑跨平台了。尽量采用 M$ 提供的扩展版本的 Winsock 函数。这通常会给程序带来一些性能方面的优化。
2、同样的代码,在不同版本的 Windows Server 上表现也是不一样的。通常版本越高级,负载能力越强。
3、虽然 IOCP 是不分 TCP 和 UDP 的,但 IOCP 通常不用在 UDP 服务端上。原因也很简单,UDP 服务端总共就需要一个套接字,但 TCP 每个连接都需要一个套接字。“绑定”在 UDP 上 IOCP 根本没有 IOCP 的感觉。:)
http://www.ibm.com/developerworks/cn/linux/l-async/
分享到:
相关推荐
在Linux操作系统中,提高应用程序性能的关键之一是优化输入/输出(I/O)处理。传统的同步I/O模型在请求发出后会使应用程序阻塞,直到请求完成,这样虽然节省了CPU资源,但在需要并发处理多个I/O操作时效率低下。为了...
而异步I/O则允许服务器在发送I/O指令后立即继续执行其他任务,只有在I/O操作完成时才通知应用程序,这样可以大大提高处理并发请求的能力。 异步I/O在FTP上传服务的流程可以分为以下几个步骤: 1. **打开文件**:...
该适配层位于操作系统提供的异步I/O操作和应用程序之间,旨在简化应用程序开发,并确保在高并发情况下依然能够保持高性能。 ##### 1. 总体结构 适配层的整体结构包括: - **事件驱动框架**:实现事件驱动机制的...
1. **异步I/O模型**:Accelio使用异步I/O模型,允许程序在发起I/O操作后立即返回,继续执行其他任务,而无需等待I/O完成。这大大提高了程序的并发性。 2. **事件通知机制**:Accelio可能使用了类似epoll的事件通知...
通过挂起函数,AsynKio可以在不阻塞线程的情况下执行网络请求或I/O操作,从而提高应用程序的响应速度。 2. **简单易用的API**: AsynKio提供了直观的API,使得开发者能够快速上手。例如,发起一个HTTP GET请求只需...
2. 非阻塞式I/O模型:在非阻塞模式下,recvfrom函数不会阻塞,而是立即返回错误,应用程序需要不断轮询检查数据是否准备好。这种方法避免了阻塞等待,但频繁的轮询会消耗大量CPU资源。 3. I/O复用模型(如Select、...
开发者可以从中学习到如何组织和管理应用程序的不同部分,以及如何处理用户交互、后台服务以及系统事件。 二、MVVM架构 iosched-master采用了现代的Model-View-ViewModel(MVVM)架构,这种架构模式提高了代码的可...
异步I/O则是当I/O操作发生时,应用程序不会等待I/O操作完成,而是继续执行,I/O操作完成后会异步通知应用程序。 在PHP7中,I/O模型内核的剖析涉及到select、poll和epoll/kqueue等系统调用,这些调用能够帮助提高I/O...
异步IO是一种高效的数据处理机制,它允许应用程序在发起一个I/O操作后继续执行其他任务,而无需等待该I/O操作完成。这种模式可以显著提高系统的响应性和吞吐量。 **特点**: - **非阻塞**:程序可以在发起I/O请求后...
3. 高性能 I/O 模型:Netty 使用了高性能的 I/O 模型,包括零拷贝和 Direct Buffer 等技术,来提高 I/O 操作的效率。 4. Pipeline 架构:Netty 的 Pipeline 架构可以使得处理逻辑清晰地分离关注点,使得开发和维护变...
Java NIO(非阻塞I/O)和AIO(异步I/O)是Java平台中用于提高I/O性能的重要技术。在传统的Java BIO(阻塞I/O)模型中,一个线程对应一个连接,当服务器处理大量并发连接时,线程资源消耗大,效率较低。而NIO和AIO则...
1. **异步I/O**:IOCP是基于异步I/O模型的,这意味着在发起I/O操作后,程序可以继续执行其他任务,而无需等待I/O操作完成。这大大提高了程序的执行效率。 2. **创建完成端口**:在易语言中,需要使用特定的API函数...
它允许开发者将多个异步I/O操作关联到一个单一的完成端口,从而实现线程池的高效调度,大大提升了服务器和客户端的性能。本文将深入探讨IOCP在网络底层封装中的应用及其关键概念。 ### IOCP基本原理 IOCP是Windows...
在服务端,我们可以使用CreateIoCompletionPort函数创建完成端口,然后为Socket绑定到这个端口,接着设置Socket为非阻塞模式,并使用WSAAsyncSelect或WSAEventSelect来启动异步I/O。客户端通常也采用类似的方式,...
本文将详细介绍Python如何进行异步编程,包括异步编程的基本概念、Python的异步编程模块、异步协程的使用方法、异步I/O的概念及其实现,以及异步编程在实际应用中的优势与应用场景。 #### 二、什么是异步编程? ...
2. 异步I/O:非阻塞模式下,主线程不会被I/O操作阻塞,提高了系统的响应速度。 3. 资源效率:线程池和I/O调度的优化,减少了CPU上下文切换,降低了内存占用。 四、开发网络通讯程序时的应用 1. 创建服务器:在创建...
总的来说,NIO的引入是Java I/O领域的一次重大革新,它简化了高性能I/O的实现,使得Java开发者能够编写出更加高效、并发友好的应用程序。通过本教程的学习,开发者能够掌握NIO的基本原理和实践技巧,从而在实际项目...
异步I/O模式允许数据库服务器在等待I/O操作完成的同时,可以继续执行其他任务,这样大大提高了系统的并行处理能力和资源利用率。尤其是在大型企业级数据库系统中,这种优化对于保持高性能和低延迟至关重要。 在安装...
IOCP是Windows系统提供的一种异步I/O机制,它允许应用程序在一个单独的线程上处理来自多个套接字的I/O完成事件。当一个I/O操作完成时,系统会将结果放入IOCP,并通知关联的线程,这样就避免了传统的轮询检查或者同步...
IOCP是一种异步I/O模型,它允许应用程序在执行I/O操作时不必等待操作完成,而是通过回调函数或者轮询的方式,当I/O操作完成时得到通知。这种机制显著提高了系统处理大量并发请求的能力,特别适合于网络服务器等需要...