最近接触了NIO@H_301_6@,类名涉及不少术语,查了下原来这些术语均出自@H_301_6@Reactor/Proactor@H_301_6@两种@H_301_6@经典的@H_301_6@IO设计模式@H_301_6@@H_301_6@。读了《@H_301_6@两种高性能I/O设计模式的比较@H_301_6@@H_301_6@》(附于文末)之后,中午在麦当劳点餐的时候突然意识到@H_301_6@I/O过程和点餐这回事儿@H_301_6@可以很好地类比:@H_301_6@
=====================@H_301_6@
@H_301_6@
场景1@H_301_6@、餐厅闲时点餐的情景:@H_301_6@@H_301_6@@H_301_6@
1、客户向接待员请求下单,点套餐(可乐@H_301_6@+@H_301_6@薯条@H_301_6@+@H_301_6@汉堡)。@H_301_6@@H_301_6@@H_301_6@
2、客户在柜台前等待。@H_301_6@@H_301_6@@H_301_6@
3、接待员从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。@H_301_6@@H_301_6@@H_301_6@
4、客户拿东西走人。@H_301_6@@H_301_6@@H_301_6@
5、轮到下一个客户向接待员发起新一轮点餐请求。@H_301_6@@H_301_6@@H_301_6@
注意到,只要步骤3@H_301_6@准备套餐所含食物的过程足够快(且架子上往往提前有储备),这样效率是很高的。@H_301_6@@H_301_6@@H_301_6@
=====================@H_301_6@
场景2@H_301_6@、餐厅忙时点餐的情景:@H_301_6@@H_301_6@@H_301_6@
1、@H_301_6@客户们在几个柜台前排长队,每个柜台前都有一个接待员。@H_301_6@@H_301_6@
2、@H_301_6@此时每个接待员只负责下单,由于下单后食物不能马上就绪,接待员会对客户说,“请您在旁边稍等”,此时客户等在柜台前,一边盯住自己的餐盘;而接待员立即给队列中下一位客户提供下单服务。@H_301_6@@H_301_6@
3、@H_301_6@有几名专职的配餐员负责从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。@H_301_6@@H_301_6@
4、@H_301_6@收到通知的客户拿东西走人,没收到通知的继续等在柜台前。@H_301_6@@H_301_6@
5、@H_301_6@有时候,某种食物如薯条恰好卖完了,这就需要等较长时间。配餐员会对客户说,“请您去座位上等,一会儿有薯条了,我们会通知您”。或者有些餐厅的做法是,给客户发一个号码牌。@H_301_6@@H_301_6@
6、@H_301_6@客户回到座位上,把号码牌摆在显眼的地方。@H_301_6@@H_301_6@
7、@H_301_6@薯条就绪后,配餐员根据号码牌分发到客户的座位上。@H_301_6@@H_301_6@
=====================@H_301_6@@H_301_6@
接下来回到程序世界:点餐的客户就是需要读取数据进行处理的handler@H_301_6@,而食物就是待读取的数据。以柜台作为分水岭,场景@H_301_6@1@H_301_6@说明了,@H_301_6@@H_301_6@如果待读取的数据能瞬间到达、就绪,用同步读取的方式效率是最高的@H_301_6@,用户在柜台前几乎感受不到等待。然而,互联网场景中,径由网络发送给服务器的数据往往是逐块、无序、缓慢地抵达(可乐、汉堡和薯条被逐个放在餐盘这个缓冲区里),且处理线程不得不等数据读完整了才能开始处理(用户也往往等套餐齐了才离开柜台)。那么:@H_301_6@@H_301_6@
l@H_301_6@同步阻塞模式即大量客户挤在柜台前等待,且什么事儿也不做。@H_301_6@@H_301_6@
l@H_301_6@同步非阻塞模式即客户们下单后到座位上等待,并不时去柜台看看套餐有否就绪。@H_301_6@@H_301_6@
l@H_301_6@异步模式即客户们回到座位上等待,套餐就绪后由接待员负责通知客户来拿。@H_301_6@@H_301_6@
@H_301_6@
如果不能预知等待数据就绪需要多久,始终选择上述任一一种模式,肯定会是低效的。@H_301_6@@H_301_6@
那么在客户众多的情况下(典型的互联网场景下,一台服务器伺候众多用户),有如下折衷方案:@H_301_6@@H_301_6@
多个柜台同时开放,每个柜台有一名接待员(事件分离器@H_301_6@/@H_301_6@事件选择器,监听事件到达)负责招待一群客户。由于人太多,不能全挤在柜台前,下单后客户都回到座位上等。当某个客户套餐中的部分食物就绪,如只有薯条就绪时,接待员可以:@H_301_6@@H_301_6@
l@H_301_6@立即通知客户来取走(@H_301_6@标准@H_301_6@Reactor模式@H_301_6@@H_301_6@)。客户往往要来来回回跑多次才能取得完整的套餐。@H_301_6@@H_301_6@
l@H_301_6@帮客户把套餐配齐,再通知客户来取走(@H_301_6@模拟的Proactor@H_301_6@模式@H_301_6@@H_301_6@,即模拟异步@H_301_6@)。配餐的事通常由配餐员这个角色来处理,如果接待员很清闲,客户比较少,他完全可以胜任配餐员的角色;如果客户非常多,帮一大堆客户监听食物就绪事件就会忙得够呛,配餐的事可以交给专职人员。(注意场景@H_301_6@1和场景@H_301_6@2@H_301_6@中谁在负责配餐工作的微妙变化@H_301_6@@H_301_6@)@H_301_6@@H_301_6@
进一步地,如果有这样一家餐厅,拥有全自动配餐设备——意味着接待员帮用户下单后,配餐过程由机器代劳,@H_301_6@OK,我们称之为@H_301_6@@H_301_6@Proactor@H_301_6@模式@H_301_6@。@H_301_6@@H_301_6@
@H_301_6@
附:@H_301_6@《两种高性能I/O设计模式的比较》@H_301_6@我@H_301_6@按自己的理解改动了多处@H_301_6@译@H_301_6@文@H_301_6@,@H_301_6@有问题请大家指出@H_301_6@。@H_301_6@
@H_301_6@
这是05@H_301_6@年的老文章,网上应该有人早就翻译过了,我翻译它仅仅为了学习@H_301_6@@H_301_6@Reactor/Proactor@H_301_6@两种IO@H_301_6@设计模式,顺便作翻译练习。@H_301_6@@H_301_6@@H_301_6@
@H_207_404@AlexanderLibman、@H_301_6@VladimirGilbourd@H_301_6@@H_301_6@@H_301_6@
http://www.artima.com/articles/io_design_patterns.html@H_301_6@
这篇文章探讨并比较两种高性能IO@H_301_6@设计模式@H_301_6@.@H_301_6@除了介绍现有的解决方案@H_301_6@,@H_301_6@还提出了一种更具伸缩性@H_301_6@,@H_301_6@只需要维护一份代码并且跨平台的解决方案@H_301_6@(@H_301_6@含代码示例@H_301_6@),@H_301_6@以及其在不同平台上的微调@H_301_6@.@H_301_6@此文还比较了@H_301_6@java,c#,c++@H_301_6@对各自现有以及提到的解决方案的实现性能@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
系统I/O@H_301_6@可分为阻塞型@H_301_6@,@H_301_6@非阻塞同步型以及非阻塞异步型@H_301_6@[@H_301_6@@H_301_6@1,@H_301_6@2].阻塞型@H_301_6@I/O@H_301_6@意味着控制权只到调用操作结束了才会回到调用者手里@H_301_6@.@H_301_6@结果调用者被阻塞了@H_301_6@,@H_301_6@这段时间做不了任何其它事情@H_301_6@.@H_301_6@更郁闷的是@H_301_6@,@H_301_6@在等待@H_301_6@IO@H_301_6@结果的时间里@H_301_6@,@H_301_6@调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿@H_301_6@read()@H_301_6@操作来说吧@H_301_6@,@H_301_6@调用此函数的代码会一直僵在此处,直至它所读的@H_301_6@socket@H_301_6@缓存中有数据到来@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;@H_301_6@要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试试看吧。比如@H_301_6@read()@H_301_6@操作@H_301_6@,@H_301_6@如果当前@H_301_6@socket@H_301_6@无数据可读,则立即返回@H_301_6@@H_301_6@EWOULBLOCK/EAGAIN@H_301_6@,告诉调用者:"@H_301_6@数据还没准备好,你稍后再试@H_301_6@".@H_301_6@@H_301_6@@H_301_6@
在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(如通过回调函数)。拿Windows@H_301_6@的@H_301_6@@H_301_6@ReadFile()@H_301_6@或者POSIX@H_301_6@的@H_301_6@@H_301_6@aio_read()@H_301_6@来说,@H_301_6@调用它之后,函数立即返回,操作系统在后台同时开始读操作。@H_301_6@@H_301_6@@H_301_6@
在以上三种IO@H_301_6@形式中,非阻塞异步是性能最高、伸缩性最好的。@H_301_6@@H_301_6@@H_301_6@
这篇文章探讨不同的I/O@H_301_6@利用机制并提供一种跨平台的设计模式@H_301_6@(@H_301_6@解决方案@H_301_6@).@H_301_6@希望此文可以给于@H_301_6@TCP@H_301_6@高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较@H_301_6@Java,C++@H_301_6@各自对探讨方案的实现以及性能@H_301_6@.@H_301_6@我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式@H_301_6@I/O@H_301_6@实在是缺少可伸缩性,性能也达不到高性能服务器的要求。@H_301_6@@H_301_6@@H_301_6@
两种IO@H_301_6@多路复用方案@H_301_6@:ReactorandProactor@H_301_6@@H_301_6@@H_301_6@
一般情况下,I/O@H_301_6@复用机制需要@H_301_6@@H_301_6@事件分离器@H_301_6@(eventdemultiplexor[@H_301_6@3]).事件分离器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊@H_301_6@:@H_301_6@@H_301_6@“@H_301_6@谁的什么东西送了,@H_301_6@快来拿吧@H_301_6@@H_301_6@!”@H_301_6@开发人员在开始的时候需要在分离器那里注册感兴趣的事件,并提供相应的处理者(eventhandlers)@H_301_6@或者是回调函数@H_301_6@;@H_301_6@事件分离器在适当的时候会将请求的事件分发给这些@H_301_6@handler@H_301_6@或者回调函数@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
涉及到事件分离器的两种模式称为:ReactorandProactor[@H_301_6@@H_301_6@1].Reactor模式基于同步@H_301_6@I/O@H_301_6@,而@H_301_6@Proactor@H_301_6@模式@H_301_6@@H_301_6@则基于@H_301_6@异步I/O.@H_301_6@在@H_301_6@Reactor@H_301_6@模式中,@H_301_6@@H_301_6@事件分离器会@H_301_6@等待@H_301_6@一个就绪事件,例如当文件描述符或@H_301_6@socket已对读写操作准备好,@H_301_6@@H_301_6@事件分离器@H_301_6@就把这个事件传给事先注册的事件处理函数或回调函数,由后者来做实际的读写操作。@H_301_6@@H_301_6@
而在Proactor@H_301_6@模式中,事件处理者@H_301_6@(@H_301_6@或者代由@H_301_6@@H_301_6@事件分离器@H_301_6@发起)@H_301_6@直接发起一个异步读写操作@H_301_6@(@H_301_6@相当于请求@H_301_6@)@H_301_6@,而实际的工作是由操作系统来完成的。@H_301_6@@H_301_6@提供给操作系统@H_301_6@的参数包括用于@H_301_6@存放@H_301_6@读@H_301_6@写@H_301_6@数据@H_301_6@的用户自定义@H_301_6@缓@H_301_6@冲@H_301_6@区,以及这个请求完后的回调函数等信息。@H_301_6@事件分离器@H_301_6@得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调@H_301_6@函数@H_301_6@。举例来说,在Windows@H_301_6@上@H_301_6@@H_301_6@,@H_301_6@事件处理者@H_301_6@可以提交@H_301_6@一个异步IO@H_301_6@操作@H_301_6@(@H_301_6@@H_301_6@微软称为@H_301_6@overlapped技术@H_301_6@)@H_301_6@,@H_301_6@@H_301_6@事件分离器则等待@H_301_6@IOCompletion事件@H_301_6@[@H_301_6@@H_301_6@1]@H_301_6@表示操作完成@H_301_6@.这种异步模式的典型实现是基于操作系统底层异步@H_301_6@API@H_301_6@的,所以我们可称之为@H_301_6@“@H_301_6@系统级别@H_301_6@”@H_301_6@的或者@H_301_6@“@H_301_6@真正意义上@H_301_6@”@H_301_6@的异步,因为具体的读写是由操作系统代劳的。@H_301_6@@H_301_6@@H_301_6@
举另外个例子来更好地理解Reactor@H_301_6@与@H_301_6@Proactor@H_301_6@两种模式的区别。这里我们只关注@H_301_6@read@H_301_6@操作,因为@H_301_6@write@H_301_6@操作也是差不多的。下面是@H_301_6@Reactor@H_301_6@的做法:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@某个事件处理者宣称它对某个socket@H_301_6@上的读@H_301_6@@H_301_6@就绪@H_301_6@事件很感兴趣@H_301_6@(表示@H_301_6@socket的@H_301_6@@H_301_6@缓冲区可读)@H_301_6@;@H_301_6@@H_301_6@
·@H_301_6@事件分离器@H_301_6@等着这个事件的发生;@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@事件发生@H_301_6@时会唤醒@H_301_6@事件分离器,@H_301_6@由分离器@H_301_6@通知先前那个事件处理者;@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@事件处理者收到消息,于是去@H_301_6@目标@H_301_6@socket@H_301_6@的缓冲区@H_301_6@上读数据了@H_301_6@,这一次读完后,会把控制权还给调度器(@H_301_6@dispatcher@H_301_6@)@H_301_6@,@H_301_6@如果数据没完(缓冲区内可能只来了部分数据),@H_301_6@它@H_301_6@可以再次@H_301_6@宣称对这个socket@H_301_6@上的读事件感兴趣,一直重复上面的步骤@H_301_6@@H_301_6@直到把数据读完@H_301_6@;@H_301_6@@H_301_6@
下面再来看看真正意义的异步模式Proactor@H_301_6@是如何做的:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@事件处理者@H_301_6@提交@H_301_6@一个@H_301_6@异步读@H_301_6@操作(@H_301_6@当然,操作系统必须支持异步@H_301_6@@H_301_6@I/O@H_301_6@).这个时候,事件处理者根本不关心@H_301_6@@H_301_6@I/O@H_301_6@读@H_301_6@就绪@H_301_6@事件,它只管@H_301_6@提交@H_301_6@这么个请求,@H_301_6@然后等着接收@H_301_6@这个@H_301_6@读@H_301_6@操作的完成事件@H_301_6@。@H_301_6@@H_301_6@
·@H_301_6@事件分离器@H_301_6@等着这个读事件的完成;@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@当@H_301_6@事件分离器@H_301_6@默默等待完成@H_301_6@事件@H_301_6@到来的同时,操作系统@H_301_6@会在一个内核线程上执行这个读操作@H_301_6@,它@H_301_6@分几次@H_301_6@从目标@H_301_6@socket的缓冲区上@H_301_6@@H_301_6@读取数据,@H_301_6@再统一转储到@H_301_6@用户@H_301_6@自定义@H_301_6@缓@H_301_6@冲@H_301_6@区中,最后通知@H_301_6@事件分离器@H_301_6@,@H_301_6@读操作完成@H_301_6@;@H_301_6@@H_301_6@
·@H_301_6@事件@H_301_6@分离器@H_301_6@通知之前的事件处理者@H_301_6@。@H_301_6@@H_301_6@
·@H_301_6@事件处理者这时会发现想要读的数据已经@H_301_6@完整地@H_301_6@放在@H_301_6@用户自定义@H_301_6@缓@H_301_6@冲@H_301_6@区中,@H_301_6@如有需要(目标可以无限大,需要读一点处理一点)@H_301_6@,事件处理者还@H_301_6@可以继续提交@H_301_6@一个@H_301_6@新的异步读@H_301_6@操作@H_301_6@,再将控制权交还给事件分离器。@H_301_6@@H_301_6@
开源C++@H_301_6@开发框架@H_301_6@ACE[@H_301_6@@H_301_6@3](DouglasSchmidt,etal.开发@H_301_6@)@H_301_6@提供了大量平台独立的底层并发支持类@H_301_6@(@H_301_6@线程、互斥量等@H_301_6@).@H_301_6@同时在更高一层它也提供了独立的几组@H_301_6@C++@H_301_6@类,用于实现@H_301_6@Reactor@H_301_6@及@H_301_6@Proactor@H_301_6@模式。尽管它们都是平台独立的单元,但他们都提供了不同的接口@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
ACEProactor在@H_301_6@MS-Windows@H_301_6@上无论是性能还在健壮性都更胜一筹,这主要是由于@H_301_6@Windows@H_301_6@提供了一系列高效的底层异步@H_301_6@API.[@H_301_6@@H_301_6@4,255); font-size:9.5pt; font-family:Verdana">5@H_301_6@].@H_301_6@@H_301_6@
(这段可能过时了点吧@H_301_6@)@H_301_6@不幸的是@H_301_6@,@H_301_6@并不是所有操作系统都为底层异步提供健壮的支持。举例来说@H_301_6@,@H_301_6@许多@H_301_6@Unix@H_301_6@系统就有麻烦@H_301_6@.@H_301_6@因此@H_301_6@,ACEReactor@H_301_6@可能是@H_301_6@Unix@H_301_6@系统上更合适的解决方案@H_301_6@.@H_301_6@正因为系统底层的支持力度不一,为了在各系统上有更好的性能@H_301_6@,@H_301_6@开发者不得不维护独立的好几份代码@H_301_6@:@H_301_6@为@H_301_6@Windows@H_301_6@准备的@H_301_6@ACEProactor@H_301_6@以及为@H_301_6@Unix@H_301_6@系列提供的@H_301_6@ACEReactor.@H_301_6@@H_301_6@@H_301_6@
就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor@H_301_6@和@H_301_6@Proactor@H_301_6@设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。@H_301_6@@H_301_6@@H_301_6@
在文章这一段时,我们将尝试提供一种融合了Proactor@H_301_6@和@H_301_6@Reactor@H_301_6@两种模式的解决方案@H_301_6@.@H_301_6@为了演示这个方案,我们将@H_301_6@Reactor@H_301_6@稍做调整,模拟成异步的@H_301_6@Proactor(@H_301_6@主要是在事件分离器里完成本该事件处理者做的@H_301_6@@H_301_6@分多次@H_301_6@读@H_301_6@写@H_301_6@socket缓冲区的@H_301_6@@H_301_6@工作,@H_301_6@我们称这种方法为@H_301_6@"@H_301_6@@H_301_6@模拟异步@H_301_6@")。下面的示例可以看看@H_301_6@read@H_301_6@操作是如何完成的@H_301_6@:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@事件处理者宣称对@H_301_6@IO@H_301_6@事件@H_301_6@(@H_301_6@读@H_301_6@就绪)@H_301_6@感兴趣,@H_301_6@并提供@H_301_6@@H_301_6@给事件分离器@H_301_6@用于存储结果的@H_301_6@用户自定义@H_301_6@缓@H_301_6@冲@H_301_6@区@H_301_6@地址@H_301_6@、@H_301_6@和数据@H_301_6@长度等参数;@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@调度器(@H_301_6@dispatcher@H_301_6@)@H_301_6@等待@H_301_6@事件@H_301_6@(比如通过@H_301_6@select()@H_301_6@@H_301_6@方法@H_301_6@);@H_301_6@@H_301_6@
·@H_301_6@当有事件到来(@H_301_6@即可读@H_301_6@)@H_301_6@,@H_301_6@@H_301_6@调度器@H_301_6@被唤醒,@H_301_6@@H_301_6@它负责@H_301_6@执行@H_301_6@一个@H_301_6@非阻塞的读操作(@H_301_6@前面事件处理者已经给了足够的信息了@H_301_6@)@H_301_6@。读完后,它去通知事件处理者。@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了.@H_301_6@@H_301_6@@H_301_6@
我们看到,通过@H_301_6@给分离器的@H_301_6@I/O模块@H_301_6@@H_301_6@(也就上面的@H_301_6@@H_301_6@调度器@H_301_6@)@H_301_6@增@H_301_6@加一些功能,可以让Reactor@H_301_6@模式转换为@H_301_6@Proactor@H_301_6@模式。@H_301_6@@H_301_6@且转换后的这些@H_301_6@操作,@H_301_6@@H_301_6@总工作量上@H_301_6@其实是和Reactor@H_301_6@@H_301_6@模式@H_301_6@完全一致的。我们只是把@H_301_6@任务重新@H_301_6@分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情:@H_301_6@@H_301_6@
标准@H_301_6@/@H_301_6@经典的Reactor@H_301_6@模式@H_301_6@:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤1)@H_301_6@等待事件@H_301_6@(Reactor@H_301_6@的工作@H_301_6@)@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤2)@H_301_6@发@H_301_6@"@H_301_6@已经可读@H_301_6@"@H_301_6@事件发给事先注册的事件处理者或者回调@H_301_6@(Reactor@H_301_6@的工作@H_301_6@)@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤3)@H_301_6@读数据@H_301_6@(@H_301_6@@H_301_6@用户定义的处理者的工作@H_301_6@)@H_301_6@@H_301_6@
·@H_301_6@步骤4)@H_301_6@处理数据@H_301_6@(@H_301_6@@H_301_6@用户定义的处理者的工作@H_301_6@)@H_301_6@@H_301_6@
模拟的Proactor@H_301_6@模式@H_301_6@:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤1)@H_301_6@等待事件@H_301_6@(Proactor@H_301_6@的工作@H_301_6@)@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤2)@H_301_6@读数据@H_301_6@(@H_301_6@@H_301_6@现在是@H_301_6@Proactor的工作@H_301_6@@H_301_6@了@H_301_6@)@H_301_6@@H_301_6@
·@H_301_6@步骤3)@H_301_6@@H_301_6@把“读取完毕”事件调度(@H_301_6@dispatch@H_301_6@)给处理者@H_301_6@(Proactor的工作@H_301_6@)@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@步骤4)@H_301_6@处理数据@H_301_6@(@H_301_6@@H_301_6@用户定义的处理者的工作@H_301_6@)@H_301_6@@H_301_6@
在没有底层异步I/OAPI@H_301_6@支持的操作系统,这种方法可以帮我们隐藏掉@H_301_6@socket@H_301_6@接口的差异@H_301_6@,@H_301_6@提供一个完全可用的统一@H_301_6@@H_301_6@的@H_301_6@"异步接口@H_301_6@"@H_301_6@@H_301_6@。这样我们就可以开发真正平台独立的通用接口了。@H_301_6@@H_301_6@
我们提出的TProactor@H_301_6@方案已经由@H_301_6@TerabitP/L[@H_301_6@@H_301_6@6]公司实现了@H_301_6@.@H_301_6@它有两种实现@H_301_6@:C++@H_301_6@的和@H_301_6@Java@H_301_6@的@H_301_6@.C++@H_301_6@版本使用了@H_301_6@ACE@H_301_6@平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。@H_301_6@@H_301_6@@H_301_6@
TProactor中最重要的组件要数@H_301_6@Engine@H_301_6@和@H_301_6@WaitStrategy@H_301_6@了@H_301_6@.Engine@H_301_6@用于维护异步操作的生命周期;而@H_301_6@WaitStrategy@H_301_6@用于管理并发策略@H_301_6@.WaitStrategy@H_301_6@和@H_301_6@Engine@H_301_6@一般是成对出现的@H_301_6@,@H_301_6@两者间提供了良好的匹配接口@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
Engines和等待策略被设计成高度可组合的@H_301_6@(@H_301_6@完整的实现列表请参照附录@H_301_6@1)@H_301_6@。@H_301_6@TProactor@H_301_6@是高度可配置的方案,通过使用异步内核@H_301_6@API@H_301_6@和同步@H_301_6@UnixAPI(@H_301_6@@H_301_6@select()@H_301_6@,@H_301_6@poll()@H_301_6@,/dev/poll(Solaris5.8+),@H_301_6@port_get@H_301_6@(Solaris5.10),RealTime(RT)signals(Linux2.4+),epoll(Linux2.6),k-queue(FreeBSD)),它内部实现了三种引擎@H_301_6@(POSIXAIO,SUNAIOandEmulatedAIO)@H_301_6@并隐藏了六类等待策略。@H_301_6@TProactor@H_301_6@实现了和标准的@H_301_6@ACEProactor@H_301_6@一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。@H_301_6@@H_301_6@@H_301_6@
Engines和@H_301_6@WaitStrategies@H_301_6@可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层@H_301_6@API@H_301_6@,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。@H_301_6@@H_301_6@@H_301_6@
举例来说,对于运行在SunSolaris@H_301_6@上的@H_301_6@HTTP@H_301_6@服务器,如果需要支持大量的连接数,@H_301_6@/dev/poll@H_301_6@或者@H_301_6@port_get()@H_301_6@之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本@H_301_6@select()@H_301_6@的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准@H_301_6@ACEReactor/Proactor@H_301_6@模式所无法提供的(见附录@H_301_6@2@H_301_6@)。@H_301_6@@H_301_6@@H_301_6@
在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor@H_301_6@相较标签的@H_301_6@ACEReactor@H_301_6@在@H_301_6@Unix/Linux@H_301_6@系统上有大约@H_301_6@10-35%@H_301_6@性能提升@H_301_6@,@H_301_6@而在@H_301_6@Windows@H_301_6@上差不多@H_301_6@(@H_301_6@测试了吞吐量及响应时间@H_301_6@)@H_301_6@。@H_301_6@@H_301_6@@H_301_6@
性能比较(JAVA/C++/C#).@H_301_6@@H_301_6@@H_301_6@
除了C++,@H_301_6@我们也在@H_301_6@Java@H_301_6@中实现了@H_301_6@TProactor.JDK1.4@H_301_6@中@H_301_6@,Java@H_301_6@仅提供了同步方法@H_301_6@,@H_301_6@像@H_301_6@C@H_301_6@中的@H_301_6@select()[@H_301_6@@H_301_6@7,255); font-size:9.5pt; font-family:Verdana">8@H_301_6@].JavaTProactor基于@H_301_6@Java@H_301_6@的非阻塞功能@H_301_6@(java.nio@H_301_6@包@H_301_6@),@H_301_6@类似于@H_301_6@C++@H_301_6@的@H_301_6@TProactor@H_301_6@使用了@H_301_6@select()@H_301_6@引擎@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
图1@H_301_6@、@H_301_6@2@H_301_6@显示了以@H_301_6@bits/sec@H_301_6@为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的@H_301_6@echo@H_301_6@服务器:标准@H_301_6@ACEReactor@H_301_6@实现@H_301_6@(@H_301_6@基于@H_301_6@RedHatLinux9.0)@H_301_6@、@H_301_6@TProactorC++/Java@H_301_6@实现@H_301_6@(MicrosoftWindows@H_301_6@平台及@H_301_6@RedHatv9.0),@H_301_6@以及@H_301_6@C#@H_301_6@实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。@H_301_6@@H_301_6@@H_301_6@
这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。@H_301_6@@H_301_6@
@H_301_6@
图1.WindowsXP/P42.6GHzHyperThreading/512MBRAM.@H_301_6@@H_301_6@@H_301_6@
@H_301_6@
图2.LinuxRedHat2.4.20-smp/P42.6GHzHyperThreading/512MBRAM.@H_301_6@@H_301_6@@H_301_6@
下面是TProactorJava@H_301_6@实现的@H_301_6@echo@H_301_6@服务器代码框架。总的来说,开发者只需要实现两个接口:一是@H_301_6@OpRead@H_301_6@,提供存放读结果的缓存;二是@H_301_6@OpWrite@H_301_6@,提供存储待写数据的缓存区。同时,开发者需要通过回调@H_301_6@onReadComplated()@H_301_6@和@H_301_6@onWriteCompleted()@H_301_6@实现协议相关的业务代码。这些回调会在合适的时候被调用@H_301_6@.@H_301_6@@H_301_6@@H_301_6@
classEchoServerProtocolimplementsAsynchHandler@H_301_6@@H_301_6@
AsynchChannelachannel=null;@H_301_6@@H_301_6@
EchoServerProtocol(Demultiplexorm,SelectableChannelchannel)@H_301_6@@H_301_6@
throwsException@H_301_6@@H_301_6@
this.achannel=newAsynchChannel(m,this,channel);@H_301_6@@H_301_6@
publicvoidstart()throwsException@H_301_6@@H_301_6@
//calledafterconstruction@H_301_6@@H_301_6@
System.out.println(Thread.currentThread().getName()+@H_301_6@@H_301_6@
":EchoServerprotocolstarted");@H_301_6@@H_301_6@
achannel.read(buffer);@H_301_6@@H_301_6@
publicvoidonReadCompleted(OpReadopRead)throwsException@H_301_6@@H_301_6@
if(opRead.getError()!=null)@H_301_6@@H_301_6@
//handleerror,doclean-upifneeded@H_301_6@@H_301_6@
System.out.println("EchoServer::readCompleted:"+@H_301_6@@H_301_6@
opRead.getError().toString());@H_301_6@@H_301_6@
achannel.close();@H_301_6@@H_301_6@
if(opRead.getBytesCompleted()<=0)@H_301_6@@H_301_6@
System.out.println("EchoServer::readCompleted:Peerclosed"@H_301_6@@H_301_6@
+opRead.getBytesCompleted();@H_301_6@@H_301_6@
ByteBufferbuffer=opRead.getBuffer();@H_301_6@@H_301_6@
achannel.write(buffer);@H_301_6@@H_301_6@
publicvoidonWriteCompleted(OpWriteopWrite)@H_301_6@@H_301_6@
//logicallysimilartoonReadCompleted@H_301_6@@H_301_6@
@H_301_6@
TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录@H_301_6@2@H_301_6@中提到的问题都被很好地隐藏在内部实现中了。@H_301_6@@H_301_6@@H_301_6@
从上面的图中我们可以看出C++@H_301_6@仍旧是编写高性能服务器最佳选择,虽然@H_301_6@Java@H_301_6@已紧随其后。然而因为@H_301_6@Java@H_301_6@本身实现上的问题,其在@H_301_6@Windows@H_301_6@上表现不佳@H_301_6@(@H_301_6@这已经应该成为历史了吧@H_301_6@)@H_301_6@。@H_301_6@@H_301_6@@H_301_6@
需要注意的是,以上针对Java@H_301_6@的测试,都是以裸数据的形式测试的,未涉及到数据的处理@H_301_6@(@H_301_6@影响性能@H_301_6@)@H_301_6@。@H_301_6@@H_301_6@@H_301_6@
纵观AIO@H_301_6@在@H_301_6@Linux@H_301_6@上的快速发展@H_301_6@[@H_301_6@@H_301_6@9],我们可以预计@H_301_6@Linux@H_301_6@内核@H_301_6@API@H_301_6@将会提供大量更加强健的异步@H_301_6@API,@H_301_6@如此一来以后基于此而实现的新的@H_301_6@Engine/@H_301_6@等待策略将能轻松地解决能用性方面的问题,并且这也能让标准@H_301_6@ACEProactor@H_301_6@接口受益。@H_301_6@@H_301_6@@H_301_6@
附录I@H_301_6@@H_301_6@@H_301_6@
TProactor中实现的@H_301_6@Engines@H_301_6@和等待策略@H_301_6@@H_301_6@@H_301_6@
@H_207_404@ POSIX_AIO(trueasync)@H_301_6@
@H_301_6@aio_read()/aio_write()@H_301_6@@H_301_6@
aio_suspend()@H_301_6@
@H_301_6@WaitingforRTsignal@H_301_6@
@H_301_6@Callbackfunction@H_301_6@@H_301_6@
POSIXcomplainedUNIX(notrobust)@H_301_6@
@H_301_6@POSIX(notrobust)@H_301_6@
@H_301_6@SGIIRIX,LINUX(notrobust)@H_301_6@@H_301_6@
SUN_AIO(trueasync)@H_301_6@
@H_301_6@aio_read()/aio_write()@H_301_6@@H_301_6@
EmulatedAsync@H_301_6@
@H_301_6@Non-blockingread()/write()@H_301_6@@H_301_6@
select()@H_301_6@
@H_301_6@poll()@H_301_6@
@H_301_6@/dev/poll@H_301_6@
@H_301_6@LinuxRTsignals@H_301_6@
@H_301_6@Kqueue@H_301_6@@H_301_6@
genericPOSIX@H_301_6@
@H_301_6@MostlyallPOSIXimplementations@H_301_6@
@H_301_6@SUN@H_301_6@
@H_301_6@Linux@H_301_6@
@H_301_6@FreeBSD@H_301_6@@H_301_6@
附录II@H_301_6@@H_301_6@@H_301_6@
所有@H_301_6@同步等待策略@H_301_6@可划分为两组:@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@边缘触发@H_301_6@edge-triggered(@H_301_6@如@H_301_6@Linux实时信号@H_301_6@)-@H_301_6@@H_301_6@socket就绪后信号只发一次@H_301_6@@H_301_6@@H_301_6@
·@H_301_6@水平触发@H_301_6@level-triggered(@H_301_6@如@H_301_6@select(),poll(),/dev/poll)- @H_301_6@始终有效@H_301_6@@H_301_6@@H_301_6@
两组的逻辑@H_301_6@:@H_301_6@@H_301_6@
·@H_301_6@边缘触发组@H_301_6@:@H_301_6@执行@H_301_6@I/O@H_301_6@操作后@H_301_6@,@H_301_6@事件分离器会收不到@H_301_6@后续socket@H_301_6@就绪事件的通知@H_301_6@@H_301_6@.@H_301_6@@H_301_6@
·@H_301_6@水平触发组@H_301_6@:@H_301_6@当事件分离器侦测到某@H_301_6@socket的@H_301_6@@H_301_6@就绪状态,它负责回调对应的事件处理者。@H_301_6@@H_301_6@但在触发回调前,应该先从监听的@H_301_6@socket描述符列表中移除之,@H_301_6@@H_301_6@否则相同的事件可能被派发两次。@H_301_6@@H_301_6@
·@H_301_6@显然要解决上述这些问题给开发带来了额外的复杂度,而@H_301_6@TProactor@H_301_6@已经帮大家处理掉。@H_301_6@@H_301_6@
@H_301_6@
译注:@H_301_6@
关于边缘(下蓝色)和水平(上红色)触发,一图胜千言@H_301_6@
@H_301_6@
@H_301_6@
@H_301_6@
边缘触发@H_301_6@仅当状态发生变化的时候才获得通知,@H_301_6@这里所谓的状态的变化并不包括缓冲区中还有未处理的数据@H_301_6@,@H_301_6@也就是说@H_301_6@,@H_301_6@如果要采用@H_301_6@@H_301_6@边缘触发@H_301_6@模式,@H_301_6@需要一直@H_301_6@read/write@H_301_6@直到出错为止@H_301_6@,@H_301_6@很多人反映为什么采用@H_301_6@@H_301_6@边缘触发@H_301_6@模式只接收了一部分数据就再也得不到通知了,@H_301_6@大多因为这样@H_301_6@;@H_301_6@而@H_301_6@LT@H_301_6@模式是只要有数据没有处理就会一直通知下去的@H_301_6@.@H_301_6@@H_301_6@看来前者适用于数据到达时多而密,后者适用于数据到达时少而稀。@H_301_6@@H_301_6@
[1]DouglasC.Schmidt,StephenD.Huston"C++NetworkProgramming."2002,Addison-WesleyISBN0-201-60464-7@H_301_6@@H_301_6@
[2]W.RichardStevens"UNIXNetworkProgramming"vol.1and2,1999,PrenticeHill,ISBN0-13-490012-X@H_301_6@@H_301_6@
[3]DouglasC.Schmidt,MichaelStal,HansRohnert,FrankBuschmann"Pattern-OrientedSoftwareArchitecture:PatternsforConcurrentandNetworkedObjects,Volume2"Wiley&Sons,NY2000@H_301_6@@H_301_6@
[4]INFO:SocketOverlappedI/OVersusBlocking/Non-blockingMode.Q181611.MicrosoftKnowledgeBaseArticles.@H_301_6@@H_301_6@
[5]MicrosoftMSDN.I/OCompletionPorts.@H_301_6@
@H_301_6@http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/i_o_completion_ports.asp@H_301_6@
[6]TProactor(ACEcompatibleProactor).@H_301_6@
@H_301_6@www.terabit.com.au@H_301_6@
[7]JavaDocjava.nio.channels@H_301_6@
@H_301_6@http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html@H_301_6@
[8]JavaDocJava.nio.channels.spiClassSelectorProvider@H_301_6@
@H_301_6@http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html@H_301_6@
[9]LinuxAIOdevelopment@H_301_6@
@H_301_6@http://lse.sourceforge.net/io/aio.html,and@H_301_6@
@H_301_6@http://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf@H_301_6@
更多IanBarile"I/OMultiplexing&ScalableSocketServers",2004February,DDJ@H_301_6@@H_301_6@
@H_301_6@@H_301_6@
Furtherreadingoneventhandling@H_301_6@
@H_301_6@-http://www.cs.wustl.edu/~schmidt/ACE-papers.html@H_301_6@
TheAdaptiveCommunicationEnvironment@H_301_6@
@H_301_6@http://www.cs.wustl.edu/~schmidt/ACE.html@H_301_6@
TerabitSolutions@H_301_6@
@H_301_6@http://terabit.com.au/solutions.php@H_301_6@
AlexLibmanhasbeenprogrammingfor15years.Duringthepast5yearshismainareaofinterestispattern-orientedmultiplatformnetworkedprogrammingusingC++andJava.HeisbigfanandcontributorofACE.@H_301_6@@H_301_6@
VladGilbourdworksasacomputerconsultant,butwishestospendmoretimelisteningjazz:)Asahobby,hestartedandruns@H_301_6@www.corporatenews.com.auwebsite.@H_301_6@