Asp.net 构建可扩展的的Comet Web 应用(一)
说明
这篇文章用来提供在asp.net中使用comet的一种理论上的解决方案。它包含了Comet技术在服务端的实现以及怎样去解决可扩展的问题。我将在不久以后发表一般文章,使用我接下来要讲到的Comet 线程池技术演示一个小游戏,来提供客户端的代码。它可能会给你在真实的环境下解决问题带来一些思路。
简介
在过去的六个月里,我一直都在投入精力开发一个在线的象棋应用程序。它能够让玩家注册、登陆,并且像在真实世界中对弈一样。其中,我不得不克服的一个障碍就是,怎样在服务端和客户端实现一个类似在真实世界中的通信。要克服这个障碍,以下一系列的因素需要考虑:
(1) 可扩展性 – 我想让它在一个负载均衡的环境中工作,并且不需要占用巨大的服务器资源。
(2) 兼容性 – 我希望它能够在许多不同特性的浏览器中工作,希望不需要任何的浏览器插件。
(3) 性能 – 我希望在一位玩家到任何可通信的对手之间提供尽可能快的响应。这可以让我更有效得控制时间和提供一个更友好的用户体验。
(4) 简单 – 我想实现通信层儿不想涉及第三方服务应用程序。总得来说,它应该仅仅工作在宿主环境中,例如www.discountasp.net
我评估了以上所有我列出的选项。我构建的第一个原型是使用标准的ajax作为解决方案。它从服务端“拉取”数据。这造成了太长得延时并且太多次数的通信。因此,我很快地从可行性方案中把它移除掉了。我又调研了其他的通信方案。例如使用一个隐藏的Flash小程序进行socket通信。这需要一个浏览器插件,所以也不是我想要的答案。然后我发现了Comet,并且觉得这就是我想要的方案。于是,我做了一些调研,并且构建了一个原型。
Comet的技术原理
Comet在客户端(web浏览器,使用XmlHttpRequest)和服务端使用一个持续的连接。这个持续连接在一段预定义的时间内(例如5秒钟)对服务端保持着打开状态,并且将会客户端两种响应:要么超时信息,要么是服务端应用程序的某些部分的逻辑想要发送的信息。一旦客户端接收到信息,它将可以被实现在客户端的任何应用程序的逻辑处理。这个持续的连接然后会被重新打开,并且重复以上过程。
这种方案解决了性能的问题。它意味着无论什么时候一条消息只要需要被发送,它就可以被发送会客户端。并且如果这个持续的连接被打开着,客户端接收它只需要很短的延时,几乎是瞬间的。
另一个连接被用来发送信息给服务器。这个连接并不是“持续”的。并且通常情况下处理完之后立即返回。从这个象棋游戏的一端看来,这个持续的连接一直都在等待对手棋子的移动。而那个非持续的连接则只是发送我的移动。
![](http://hi.csdn.net/attachment/201108/31/0_1314797264Gx2M.gif)
一个 Comet请求返回超时的顺序图
一个Comet返回消息的顺序图
真实环境中使用Comet
到现在为止,一切从纸面上看起来都很美好。我们有一种方案能够提供发送信息给一个浏览器的功能,在真实的环境并且不需要插件。但是在实践的过程当中,会遇到更多的麻烦。许多文章都在描述这样一个事实——采用一个持续的连接多么得”Hack”。我不太倾向于同意这样的观点。
Comet在某些浏览器中运行,确实会遇到一些问题(主要是因为HTTP协议规定,每个浏览器只同时支持两个连接,并且限制在同一主机上)。Http协议的这个限制被用来为通常的在低带宽连接下的浏览提供更好地性能,这在运行Comet程序的时候导致了性能问题(有一些解决方法)。这个问题,仅仅只需要被IE关注(我猜想IE直到8.0的版本,才严格执行了标准)。FireFox 2运行更多的连接,并且将他们管理得更好。另外,FireFox 3甚至允许更多的连接,这意味着Comet风格的应用程序的前景是光明的。
第二个问题来着这种技术的可扩展性。这也是这篇文章尝试补救的问题。这个问题源于现阶段各个平台缺乏对Comet这种风格协议的很好的支持,这导致了使用了持久连接的应用程序在未来可能不会具有很好的扩展性。我想说,这不是Comet技术思想本身的失败,而是对Comet服务器特殊实现的失败。
很多其他的开发者已经将那些坐在我们平台之前的服务器放到了一起。这就允许我们将Comet的请求方式从web 服务器中分离开来。并且通过管理他们自己的持续连接可以实现扩展。我在这篇文章中阐明的就是你在哪些情况下不应该在asp.net中使用Comet,并且给出了可能的解决方案。
开始测试Comet
在asp.net中使用持续连接最主要的缺陷是,在asp.net中每一个连接都占用asp.net一个工作线程(那些连接可能持续着五秒,并且一直打开着)。因此,每一个客户端连接都将在asp.net线程池中占用一个线程。最终,无法卸载,服务器将停止响应。
为了展示这种情况,我举一个非常简单的例子。它使用很简单的持续连接向asp.net发起请求。用一个handler占用请求,并在返回客户端之前使其保持打开状态5秒钟。
![](http://hi.csdn.net/attachment/201108/31/0_1314797830i7wa.gif)
这个handler非常的简单,它持有请求的执行5秒钟。然后返回。这个简单的Comet请求将最终因为请求超时返回到客户端。
我还写了一个控制台程序。使用WebRequest来调用CometSyncHandler。结果和预期的一样,每一个客户端占用了一个asp.net工作线程,最终在40或者更多的连接下,网站开始招架不住,页面渐渐响应非常缓慢。
下面的截屏可以看到发生了什么:
![](http://hi.csdn.net/attachment/201108/31/0_1314798055BE03.gif)
可以清楚地看到,它不适合任何真实的应用程序。所以我做了一些“挖掘”,并且设计了一个解决方案。
IhttpAsyncHandler
这是方案的第一部分,这个小“魔法”在当我们向一个handler发出一个请求时。
,允许我们在服务器上异步地运行代码。如果你对IasyncHttpHandler不是很熟悉,那就阅读我下面的解释,来了解它如何工作:
IhttpAsyncHandler 开放两个主要的需要被实现的方法。它们是:BeginProcessRequest和EndProcessRequest。通常的做法是:我们在请求开始的时候的处理逻辑放在BeginProcessRequest中,然后我们执行一系列的异步方法,例如数据库查询或者.net的异步方法。当这些异步方法执行完成之后,然后响应客户端的处理放在EndProcessRequest。
下面的顺序图,展示了它如何工作:
![](http://hi.csdn.net/attachment/201108/31/0_1314798198pgG8.gif)
CometThreadPool
上面的顺序图介绍了一个自定义的线程池用来处理Comet请求。需要它的原因是因为我们不想asp.net为每一个这样的请求开启一个它自己的线程,知道它需要等待一个Comet请求的超时。
这段线程池技术的实现技术位于网站的CometAsync文件夹中。它包含了如下的文件:
CometAsyncHandler – 这是一个IhttpAsyncHandler接口的实现。
CometAsyncResult – 这是IasyncResult接口的自定义实现。它包含了一个Comet异步操作的状态。
CometThreadPool – 这是一个静态类用来管理Comet线程池。
CometWaitRequest – 这是一个代表从客户端请求的对象。它们被排列自定义线程池中等待被处理。
CometWaitThread –这是一个线程用来处理来自队列中的CometWaitRequest对象。
这个实现在第一次创建一系列的后台CometWaitThread对象。每一个这些对象都包含一个单独的线程,用来处理CometWaitRequest。在我们的web应用程序中,我们将在Application_Start实例化一个线程池。
![](http://hi.csdn.net/attachment/201108/31/0_1314798789pHNs.gif)
这个被创建的五个线程一直被闲置在后台,直到等待CometWaitRequest实例的到来,以为这些实例提供服务。
然后CometAsyncHandler等待来自客户端的请求。它的职责是将这些请求排列在线程池中。
![](http://hi.csdn.net/attachment/201108/31/0_1314798973nuuD.gif)
为了我们能完全的跟踪哪些线程是存在的,BeginProcessRequest输出了一些debug信息。然后创建了CometAsyncResult类的一个实例来跟踪HttpContext,并且向asp.net返回并说明它已经开始了一个异步处理。在返回之前,它调用了BeginWaitRequest,用来把请求加入线程池。
![](http://hi.csdn.net/attachment/201108/31/0_1314799164F1Uu.gif)
这段代码创建了一个CometWaitRequest类的新的实例并且把它排列到线程池中。
这段代码逻辑中,挑选了一个CometWaitThread并基于循环方法来分配CometWaitRequest(例如,如果线程1接收之前的一个请求,线程2将接收第二个)。
CometWaitThread类
![](http://hi.csdn.net/attachment/201108/31/0_1314799322i8MT.gif)
请求被加入到一个被选中的线程的用来存放CometWaitRequest对象的列表中。
这一时刻,CometAsyncHandler已经将asp,net线程返回到线程池中。并且正在等待CometWaitThread完成异步处理,然后它就可以完成客户端的请求。CometWaitThread的代码看起来像下面这样:
![](http://hi.csdn.net/attachment/201108/31/0_1314799809M7Hy.gif)
QueueCometWaitRequest_WaitCallback是这个线程的入口点。它在Application_Start方法返回的时候开始执行。它执行了一个循环,并等待一个CometWaitRequest对象加入到它的队列中。一旦一个客户端请求CometAsyncHandlerhandler,它就会出现在队列中。
它顺序处理队列中的每一个对象,在每一次循环中。例如,如果这里有三个请求。它将检查请求1,2,3 然后继续循环并且处理继续处理请求1,2,3 。这能够确保每一个请求都被尽可能快地处理而不是等到它的5秒超时完成。
循环检查是否CometWaitRequest已经是否排列在队列中的时间已经超过了它预定的5秒超时时间。否则,它会检查是否有一个事件正在等待被返回给客户端。如果,这两种情况都不是,它就完成了CometWaitRequest的处理,返回所需的响应对象。然后从队列中将其移除。
![](http://hi.csdn.net/attachment/201108/31/0_1314800033c3rt.gif)
QueueCometWaitRequest_Finished方法完成异步操作,通过调用CometAsyncResult 对象的SetCompleted方法。然后调用CometAsyncResult上的回调方法。它指向CometAsyncHandler对象的EndProcessRequest方法。接下来的代码会被执行。
![](http://hi.csdn.net/attachment/201108/31/0_1314800289256a.gif)
该方法以序列化任意的我们设置到request's HttpContext输出流的对象来响应客户端。
有一个需要被提及的事情是,无论那些线程最终对请求做什么处理,当它到达BeginProcessRequest方法时,它是一个正在被执行的asp.net工作线程。并且当CometWaitThread完成时,要么是一个超时信息要么是一个返回信息,EndProcessRequest方法是被CometThreadPool线程池中的一个线程执行的。这意味着,asp.net只是使用了它线程池中的一个线程来初始化Comet请求。其余的5秒不是被asp.net线程处理。
我们在执行时,屏幕的截图中可以看到:
![](http://hi.csdn.net/attachment/201108/31/0_131480036495PB.gif)
从这点上来看,值得一提的是来自网站的输出是非常棒的。考虑到这里有200个持续连接,可以看到任务管理器重的CPU/内存数据也是非常正常(并且还同时在这台机子上运行着客户端)。
为了检查一切都工作地非常正常,我为每一个请求/响应对做了一个计数器,来确保每一个请求都有一个响应。下面的截图显示了运行着200个客户端五分钟的测试输出。
![](http://hi.csdn.net/attachment/201108/31/0_1314800659L6bL.gif)
它显示了所有的请求都完成地非常得成功
结论
通过实现一个客户端的线程池,我们能构建一个Comet的解决方法在我们的asp.net服务端代码而不是实现一个自定义的服务器,或者甚至实现任何复杂的消息程序。只是一个简单的线程池来管理对个请求。例如我们用五个线程来管理了所有的200个Comet请求。