在几年前曾经做过一个网络项目,当时对网络通信仅仅是有点基础。tcp/ip协议的基础还算不错,sockt的应用看起来也不算复杂。于是就用异步非阻塞的sockt通信实现了服务器端和客户端。但是项目在联合调试阶段就出现了重大的性能问题。项目的服务器端同时连入的连接数在几百左右,而服务器端的资源消耗非常厉害。就是在这样的环境下,第一次接触到高效通信模型这个概念,IOCP完成端口 (I/O Completion Port)也是在这个时候成为了我心目中windows平台下这个概念的最高峰,同时也成为了我的最大困惑。
作为当时菜虫级的程度,找遍了网络和书店以及msdn,找到了大量关于IOCP的资料,希望能了解它的原理和应用。遗憾的是,一开始就先入为主的认为IOCP是一种网络通信模型,而走入了误区(都是“端口”惹的祸...),更可悲的是在找到的资料中混杂了“重叠端口”(重叠I/O的另一种翻译...找不到具体说法,只是通过代码发现是一回事),后来又有“重叠I/O”、“完成例成(completion routines)”、“select模式的重叠端口”等相似名称的资料,终于最后在海量的资料里面迷失了。都怪自己e文太烂啊。。。。
理一理这样的一批概念:
同步、异步、阻塞、非阻塞
select模式
重叠I/O(Overlapped I/O)
完成例成(completion routines)
IOCP完成端口 (I/O Completion Port)
上面的概念我不知道该用什么来统称它们,只能说它们都和网络开发相关。比较混乱的说,而写这个文章的目的也是想理一理这些概念,做做头脑风暴。
同步、异步、阻塞、非阻塞是IO的基本原理。同步和异步是针对功能的执行顺序来说的,而阻塞和非阻塞是针对等待IO数据的方式说的。因此这是两对概念,同步与阻塞,异步与非阻塞都没有必然的联系。通俗的说,同步就是工作线程在处理IO时等待IO完成再继续后面的工作;异步就是工作线程不等待IO处理的结果就继续后面的工作,而IO处理结果将通过回调方式返回;阻塞是在等待IO时,如果IO没有可用数据或数据没有传送完成,那么一直等待下去,直到IO处理完数据再返回;非阻塞就是不管IO是否有可用数据或数据已经传送,照样返回。只要把同步和阻塞分清,这四个概念就很容易理清了。
同步、异步、阻塞、非阻塞等原理确定了网络通信的基本网络通信模型结构。它们处理的怎么等待数据和怎么收发数据的问题,但是对通信性能并没有提供更多的指导。特别是服务端,当成千上万的连接发生并发时,降低cpu的占用率,减少内存使用率,减少带宽就成为了很关键的问题。因此便有先有了重叠I/O(Overlapped I/O)。
重叠I/O(Overlapped I/O)是怎么一回事呢?
如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。而阻塞、select、WSAAsyncSelect以及WSAEventSelect等4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
我想之所以叫重叠也就是因为它是两个缓冲重叠的意思。就这么一重叠就剩了不少空间和不少事情。
提重叠I/O(Overlapped I/O)之前应该先提select模式,因为它是最接近同步、异步、阻塞、非阻塞模式的一种模式。select模式、WSAAsyncSelect以及WSAEventSelect都是通过轮询socket列表来确定那个socket是当前有效的(它们的原理还是有点不一样的)。好处是防止在在阻塞模式的套接字里被锁死,避免在非阻塞套接字里重复检查WSAEWOULDBLOCK错误。
也就是说,select模式提供了一种比较优秀的查询方式来判断当前连接的状态,避免了直接使用阻塞或者非阻塞所导致的问题。
提重叠I/O(Overlapped I/O)提供了比select模式更优秀的缓冲管理,那它是不是也提供了比select更优秀的轮询方式呢?可以说是,也可以说不是。因为提重叠I/O(Overlapped I/O)并没有直接提供这样的方法,而是提供了一种手段。这手段是在Overlapped数据结构里标记了该数据所属的socket和有数据到的来。怎么捕获这个事件?那就是完成例成(completion routines)和事件对象通知(event object notification) 所干的事情了。完成例成(completion routines)类似于dotnet里面的异步,在socket接收数据时给WSARecv传送一个完成后回调的函数句柄,来达到捕获数据到来的事件;至于事件对象通知(event object notification) 利用的是Overlapped数据结构里包含“有数据到来的事件”的WSAEVENT hEvent成员与事件句柄关联实现的,这个就象是使用了类为外部提供事件通知的方式。
使用事件或者是回调都要比轮询高效。因此重叠I/O(Overlapped I/O)可以支持很高的连接数,传说可以上万。但是重叠I/O(Overlapped I/O)也有不足的地方。重叠缓冲是很好的做法,但是对应每个socket都要有一个缓冲,那么一万个连接就会有一万个缓冲了。另外还有就是无论是异步还是事件来唤醒线程,都和缓冲存在一个连接一个线程的情况。不面对大量的并发,这两个问题并不明显,但是如果并发连接数量达到某个级别,cpu和内存都将存在严重的浪费。而且其支持的连接数与硬件性能的比只能是一个固定值。
为了解决重叠I/O的问题,便有了IOCP完成端口 (I/O Completion Port)。为了解决大量并发产生的资源浪费,完成端口引入了类似线程池和资源自动分配回收的两种技术(这里之所以说是类似,是因为对于IOCP的原理还没有作更深入的研究说不准其准确的原理,只能通过它的现象来认为那是线程池+高效调度+资源回收综合起来的)。所谓的大量并发连接不是指同一时间内的连接数量,而是在某时间区间内的连接数量。因此在该时间区间内并不是所有的连接线程都是活动的,只要具有高效的线程分配和调度,那么在每个时刻提供比整个区间内连接线程小得多的活动线程也能把该时间区间内的并发处理,这就是线程池的好处。对于大量的并发短连接,其缓冲绝对是可重复使用的,其线程也是可重复使用的,因此准确高效的资源回收能是资源最大效率的运用起来。通过对线程和缓冲的有效再利用使完成端口在更小的资源环境下取得比重叠I/O更好的性能。
完成端口这个名字使不少人迷惑,我们完全可以通过它的原理和应用来理解这个名字。完成端口是通过调度线程和提供资源重用来取得高效的,因此完成端口把连接的调度和其缓冲的分配都进行了封装,提供了很容易使用的接口。我想完成端口的“完成”便是这样而来--调度和资源分配的自动完成,“端口”--就是指IO端口,每个IO设备都有其自己的IO号,这个IO号又被成为IO的端口。那英文名里面的I/O又是什么回事呢?这和重叠I/O是一样的,就是指它们是应用在I/O上的一种技术。广义的说,网络不也是IO?
重叠I/O和IOCP完成端口确实是可以用在所有的IO应用上。而完成端口因为封装了调度和回收机制,还可以把它当作高效的队列处理技术和线程调度技术来使用。
这是我对这些概念的理解,可能理解得并不准确。上面提及的网络模型都是在windows平台上提供了API支持的,能找到的示例代码大部分也是c++的代码。在接触完成端口时的想法是如何把它用在dotnet上,因此在研究完成端口的原理和应用时,也就一直在思考能否在dotnet上使用这么一个高效的模型。当然不仅仅是完成端口,select模式,重叠端口等在dotnet上也是少见的。因此后面就这个方向进行一下探讨。
另外,CodeProject上有个托管环境下的IOCP例子很值得研究。
Managed I/O Completion Ports (IOCP)
Managed I/O Completion Ports (IOCP) - Part 2