用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

时间:2024-05-24 22:33:50

ASP。NET 计时器

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

之前在“反向Ajax,第1部分:Comet介绍”(英文版)文章中学习了“基于 Multipart XMLHttpRequest 的 Comet”的知识,然后用 ASP.NET MVC 实现了一个,详见用 ASP.NET MVC 实现基于 Multipart XMLHttpRequest 的 Comet

今天继续学习了基于 XMLHttpRequest long polling 的 Comet,又用 ASP.NET MVC 实现了一个,在这篇文章中分享一下。

先了解一下什么是XMLHttpRequest long polling?

这是一种推荐的实现Comet的做法,打开一个到服务器端的Ajax请求然后等待响应。服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。然后客户端就会使用这一响应并打开一个新的到服务器端的长生存期的Ajax请求。

This is a recommended method to implement Comet is to open an Ajax request to the server and wait for the response. The server requires specific features on the server side to allow the request to be suspended. As soon as an event occurs, the server sends back the response in the suspended request and closes it. The client then consumes the response and opens a new long-lived Ajax request to the server.

我个人的理解是,看起来就像在Web环境中客户端能订阅服务端的事件,服务器端通过事件去通知客户端。如果服务器端用 ASP.NET 来实现,可以利用 .NET 的事件驱动机制,很有意思,下面的示例代码将展示这一点。

先看Web前端js代码:

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
jQuery(function ($) {     function long_polling() {         $.getJSON('/comet/LongPolling', function (data) {             if (data.d) {                 $('#logs').append(data.d + "<br/>");             }              long_polling();         });     }     long_polling(); });
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

js代码很简单,就是一个递归调用(调用在callback时进行的),通过jQuery的$.getJSON发起Ajax请求,'/comet/LongPolling' 表示请求的服务端 CometController 的 LongPolling Action 的网址。这里我们可以看出实现 Comet 的难点不在 Web 前端,而是在服务器端。

接下来重点看 Web 服务器 ASP.NET MVC Controller 的代码。

首先要注意的是为了响应 XMLHttpRequest long polling 请求,我们需要实现一个异步控制器(AsyncController),如果您对 AsyncController 不熟悉,建议阅读MSDN上的文章 Using an Asynchronous Controller in ASP.NET MVC

先上 Controller 的实现代码:

(注:该控制器实现的功能是每隔5秒钟向客户端发送服务器当时间)

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class CometController : AsyncController {     //LongPolling Action 1 - 处理客户端发起的请求     public void LongPollingAsync()     {         //计时器,5秒种触发一次Elapsed事件         System.Timers.Timer timer = new System.Timers.Timer(5000);         //告诉ASP.NET接下来将进行一个异步操作         AsyncManager.OutstandingOperations.Increment();         //订阅计时器的Elapsed事件         timer.Elapsed += (sender, e) =>             {                 //保存将要传递给LongPollingCompleted的参数                 AsyncManager.Parameters["now"] = e.SignalTime;                 //告诉ASP.NET异步操作已完成,进行LongPollingCompleted方法的调用                 AsyncManager.OutstandingOperations.Decrement();             };         //启动计时器         timer.Start();     }    //LongPolling Action 2 - 异步处理完成,向客户端发送响应     public ActionResult LongPollingCompleted(DateTime now)     {         return Json(new { d = now.ToString("MM-dd HH:mm:ss ") +              "-- Welcome to cnblogs.com!" },              JsonRequestBehavior.AllowGet);     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

实现异步控制器需要继承 System.Web.Mvc.AsyncController,并将 Action 分解为两个,比如 Action 叫 LongPolling,则分解为 LongPollingAsync 与 LongPollingCompleted 。LongPollingAsync 接受客户端请求,并发起异步操作;异步操作完成,调用LongPollingCompleted。

AsyncManager.OutstandingOperations.Increment(); 告诉ASP.NET接下来将进行一个异步操作。

AsyncManager.OutstandingOperations.Decrement(); 告诉ASP.NET异步操作完成,请调用LongPollingCompleted()方法。

示例代码中的异步操作就是将服务器当前时间作为参数传递给 LongPollingCompleted() 方法,LongPollingCompleted() 获取服务器当前时间并传递给客户端,客户端收到后将之显示出来,将继续发起 Ajax 请求 ... 这样周而复始,实现了基于 XMLHttpRequest long polling 的 Comet。

示例代码运行结果如下:

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

小结

以前觉得 Comet 是很高深的东西,自己动手做了之后,发觉原来没那么难。所以,重要的是动手去做

如果不能在实际项目中去做,那就写一篇博客吧!

代码下载

=========================================================================================================

在前文中,介绍了.NET下的多种异步的形式,在WEB程序中,天生就是多线程的,因此使用异步应该更为谨慎。本文将着重展开ASP.NET中的异步。

【注意】本文中提到的异步指的是服务器端异步,而非客户端异步(Ajax)。

对于HTTP的请求响应模型,服务器无法主动通知或回调客户端,当客户端发起一个请求后,必须保持连接等待服务器的返回结果,才能继续处理,因此,对于客户端来说,请求与响应是无法异步进行,也就是说无论服务器如何处理请求,对于客户端来说没有任何差别。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

那么ASP.NET异步指的又是什么,解决了什么问题呢?

在解释ASP.NET异步前,先来考察下ASP.NET线程模型。

ASP.NET线程模型

我们知道,一个WEB服务可以同时服务器多个用户,我们可以想象一下,WEB程序应该运行于多线程环境中,对于运行WEB程序的线程,我们可以称之为WEB线程,那么,先来看看WEB线程长什么样子吧。

我们可以用一个HttpHandler输出一些内容。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class Handler : IHttpHandler {    public void ProcessRequest(HttpContext context)     {         context.Response.ContentType = "text/plain";         var thread = Thread.CurrentThread;         context.Response.Write(             string.Format("Name:{0}\r\nManagedThreadId:{1}\r\nIsBackground:{2}\r\nIsThreadPoolThread:{3}",                  thread.Name,                 thread.ManagedThreadId,                 thread.IsBackground,                 thread.IsThreadPoolThread)             );     }    public bool IsReusable     {         get {return true;}     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

你可以看到类似于这样的结果:

Name:

ManagedThreadId:57

IsBackground:True

IsThreadPoolThread:True

这里可以看到,WEB线程是一个没有名称的线程池中的线程,如果刷新这个页面,还有机会看到 ManagedThreadId 在不断变化,并且可能重复出现。说明WEB程序有机会运行于线程池中的不同线程。

为了模拟多用户并发访问的情况,我们需要对这个处理程序添加人为的延时,并输出线程相关信息与开始结束时间,再通过客户端程序同时发起多个请求,查看返回的内容,分析请求的处理情况。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public void ProcessRequest(HttpContext context) {     DateTime begin = DateTime.Now;     int t1, t2, t3;     ThreadPool.GetAvailableThreads(out t1, out t3);     ThreadPool.GetMaxThreads(out t2, out t3);     Thread.Sleep(TimeSpan.FromSeconds(10));     DateTime end = DateTime.Now;     context.Response.ContentType = "text/plain";     var thread = Thread.CurrentThread;     context.Response.Write(         string.Format("TId:{0}\tApp:{1}\tBegin:{2:mm:ss,ffff}\tEnd:{3:mm:ss,ffff}\tTPool:{4}",              thread.ManagedThreadId,             context.ApplicationInstance.GetHashCode(),             begin,             end,             t2 - t1             )         ); }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

我们用一个命令行程序来发起请求,并显示结果。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
static void Main() {     var url = new Uri("http://localhost:8012/Handler.ashx");     var num = 50;     for (int i = 0; i < num; i++)     {         var request = WebRequest.Create(url);         request.GetResponseAsync().ContinueWith(t =>         {             var stream = t.Result.GetResponseStream();             using (TextReader tr = new StreamReader(stream))             {                 Console.WriteLine(tr.ReadToEnd());             }         });     }     Console.ReadLine(); }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

这里,我们同时发起了50个请求,然后观察响应的情况。

【注意】后面的结果会因为操作系统、IIS版本、管道模式、.NET版本、配置项 的不同而不同,以下结果为在Windows Server 2008 R2 + IIS7.5 + .NET 4.5 beta(.NET 4 runtime) + 默认配置 中测试的结果,在没有特别说明的情况下,均为重启IIS后第一次运行的情况。
这个程序在我的电脑运行结果是这样的:

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
TId:6   App:35898671    Begin:55:30,3176        End:55:40,3182  TPool:2 TId:5   App:22288629    Begin:55:30,3176        End:55:40,3212  TPool:2 TId:7   App:12549444    Begin:55:31,0426        End:55:41,0432  TPool:3 TId:8   App:22008501    Begin:55:31,5747        End:55:41,5752  TPool:4 TId:9   App:37121646    Begin:55:32,1067        End:55:42,1073  TPool:5 TId:10  App:33156464    Begin:55:32,6387        End:55:42,6393  TPool:6 TId:11  App:7995840     Begin:55:33,1707        End:55:43,1713  TPool:7 TId:12  App:36610825    Begin:55:33,7028        End:55:43,7033  TPool:8 TId:13  App:20554616    Begin:55:34,2048        End:55:44,2054  TPool:9 TId:14  App:15510466    Begin:55:35,2069        End:55:45,2074  TPool:10 TId:15  App:23324256    Begin:55:36,2049        End:55:46,2055  TPool:11 TId:16  App:34250480    Begin:55:37,2050        End:55:47,2055  TPool:12 TId:17  App:58408916    Begin:55:38,2050        End:55:48,2056  TPool:13 TId:18  App:2348279     Begin:55:39,2051        End:55:49,2057  TPool:14 TId:19  App:61669314    Begin:55:40,2051        End:55:50,2057  TPool:15 TId:6   App:35898671    Begin:55:40,3212        End:55:50,3217  TPool:15 TId:5   App:22288629    Begin:55:40,3232        End:55:50,3237  TPool:15 TId:7   App:12549444    Begin:55:41,0432        End:55:51,0438  TPool:15 TId:8   App:22008501    Begin:55:41,5752        End:55:51,5758  TPool:15 TId:9   App:37121646    Begin:55:42,1073        End:55:52,1078  TPool:15 TId:10  App:33156464    Begin:55:42,6393        End:55:52,6399  TPool:15 TId:11  App:7995840     Begin:55:43,1713        End:55:53,1719  TPool:15 TId:12  App:36610825    Begin:55:43,7043        End:55:53,7049  TPool:15 TId:13  App:20554616    Begin:55:44,2054        End:55:54,2059  TPool:15 TId:20  App:36865354    Begin:55:45,2074        End:55:55,2080  TPool:16 TId:14  App:15510466    Begin:55:45,2084        End:55:55,2090  TPool:16 TId:21  App:3196068     Begin:55:46,2055        End:55:56,2061  TPool:17 TId:15  App:23324256    Begin:55:46,2065        End:55:56,2071  TPool:17 TId:22  App:4186222     Begin:55:47,2055        End:55:57,2061  TPool:18 TId:16  App:34250480    Begin:55:47,2065        End:55:57,2071  TPool:18 TId:23  App:764807      Begin:55:48,2046        End:55:58,2052  TPool:19 TId:17  App:58408916    Begin:55:48,2056        End:55:58,2062  TPool:19 TId:24  App:10479095    Begin:55:49,2047        End:55:59,2052  TPool:20 TId:18  App:2348279     Begin:55:49,2057        End:55:59,2062  TPool:20 TId:25  App:4684807     Begin:55:50,2047        End:56:00,2053  TPool:21 TId:19  App:61669314    Begin:55:50,2057        End:56:00,2063  TPool:21 TId:6   App:35898671    Begin:55:50,3227        End:56:00,3233  TPool:21 TId:5   App:22288629    Begin:55:50,3237        End:56:00,3243  TPool:21 TId:7   App:12549444    Begin:55:51,0438        End:56:01,0443  TPool:21 TId:8   App:22008501    Begin:55:51,5758        End:56:01,5764  TPool:21 TId:9   App:37121646    Begin:55:52,1078        End:56:02,1084  TPool:21 TId:10  App:33156464    Begin:55:52,6399        End:56:02,6404  TPool:21 TId:11  App:7995840     Begin:55:53,1719        End:56:03,1725  TPool:21 TId:26  App:41662089    Begin:55:53,7049        End:56:03,7055  TPool:22 TId:12  App:36610825    Begin:55:53,7059        End:56:03,7065  TPool:22 TId:13  App:20554616    Begin:55:54,2069        End:56:04,2075  TPool:22 TId:27  App:46338128    Begin:55:55,2070        End:56:05,2076  TPool:23 TId:14  App:15510466    Begin:55:55,2090        End:56:05,2096  TPool:23 TId:20  App:36865354    Begin:55:55,2090        End:56:05,2096  TPool:23 TId:28  App:28975576    Begin:55:56,2051        End:56:06,2056  TPool:24
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

从这个结果大概可以看出,开始两个请求几乎同时开始处理,因为线程池最小线程数为2(可配置),紧接着后面的请求会每隔半秒钟开始一个,因为如果池中的线程都忙,会等待半秒(.NET版本不同而不同),如果还是没有线程释放则开启新的线程,直到达到最大线程数(可配置)。未能在线程池中处理的请求将被放入请求队列,当一个线程释放后,下一个请求紧接着开始在该线程处理。

最终50个请求共产生24个线程,总用时约35.9秒。

光看数据不够形象,用简单的代码把数据转换成图形吧,下面是100个请求的处理过程。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

我们可以看到,当WEB线程长时间被占用时,请求会由于线程池而阻塞,同时产生大量的线程,最终响应时间变长。

作为对比,我们列出处理时间10毫秒的数据。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
TId:6   App:44665200    Begin:41:07,9932        End:41:08,0032  TPool:2 TId:5   App:37489757    Begin:41:07,9932        End:41:08,0032  TPool:2 TId:5   App:44665200    Begin:41:08,0042        End:41:08,0142  TPool:2 TId:6   App:37489757    Begin:41:08,0052        End:41:08,0152  TPool:2 TId:5   App:44665200    Begin:41:08,0142        End:41:08,0242  TPool:2 TId:6   App:37489757    Begin:41:08,0152        End:41:08,0252  TPool:2 TId:5   App:44665200    Begin:41:08,0242        End:41:08,0342  TPool:2 TId:6   App:37489757    Begin:41:08,0252        End:41:08,0352  TPool:2 TId:5   App:44665200    Begin:41:08,0342        End:41:08,0442  TPool:2 TId:6   App:37489757    Begin:41:08,0352        End:41:08,0452  TPool:2 TId:5   App:44665200    Begin:41:08,0442        End:41:08,0542  TPool:2 TId:6   App:37489757    Begin:41:08,0452        End:41:08,0552  TPool:2 TId:5   App:44665200    Begin:41:08,0542        End:41:08,0642  TPool:2 TId:6   App:37489757    Begin:41:08,0552        End:41:08,0652  TPool:2 TId:5   App:44665200    Begin:41:08,0642        End:41:08,0742  TPool:2 TId:6   App:37489757    Begin:41:08,0652        End:41:08,0752  TPool:2 TId:5   App:44665200    Begin:41:08,0742        End:41:08,0842  TPool:2 TId:6   App:37489757    Begin:41:08,0752        End:41:08,0852  TPool:2 TId:5   App:44665200    Begin:41:08,0842        End:41:08,0942  TPool:2 TId:6   App:37489757    Begin:41:08,0852        End:41:08,0952  TPool:2 TId:5   App:44665200    Begin:41:08,0942        End:41:08,1042  TPool:2 TId:6   App:37489757    Begin:41:08,0952        End:41:08,1052  TPool:2 TId:5   App:44665200    Begin:41:08,1042        End:41:08,1142  TPool:2 TId:6   App:37489757    Begin:41:08,1052        End:41:08,1152  TPool:2 TId:5   App:44665200    Begin:41:08,1142        End:41:08,1242  TPool:2 TId:6   App:37489757    Begin:41:08,1152        End:41:08,1252  TPool:2 TId:5   App:44665200    Begin:41:08,1242        End:41:08,1342  TPool:2 TId:6   App:37489757    Begin:41:08,1252        End:41:08,1352  TPool:2 TId:5   App:44665200    Begin:41:08,1342        End:41:08,1442  TPool:2 TId:6   App:37489757    Begin:41:08,1352        End:41:08,1452  TPool:2 TId:5   App:44665200    Begin:41:08,1442        End:41:08,1542  TPool:2 TId:6   App:37489757    Begin:41:08,1452        End:41:08,1552  TPool:2 TId:5   App:44665200    Begin:41:08,1542        End:41:08,1642  TPool:2 TId:6   App:37489757    Begin:41:08,1552        End:41:08,1652  TPool:2 TId:5   App:44665200    Begin:41:08,1642        End:41:08,1742  TPool:2 TId:6   App:37489757    Begin:41:08,1652        End:41:08,1752  TPool:2 TId:5   App:44665200    Begin:41:08,1742        End:41:08,1842  TPool:3 TId:7   App:12547953    Begin:41:08,1752        End:41:08,1852  TPool:3 TId:6   App:37489757    Begin:41:08,1762        End:41:08,1862  TPool:3 TId:5   App:44665200    Begin:41:08,1842        End:41:08,1942  TPool:3 TId:7   App:12547953    Begin:41:08,1852        End:41:08,1952  TPool:3 TId:6   App:37489757    Begin:41:08,1862        End:41:08,1962  TPool:3 TId:5   App:44665200    Begin:41:08,1942        End:41:08,2042  TPool:3 TId:7   App:12547953    Begin:41:08,1952        End:41:08,2092  TPool:3 TId:6   App:37489757    Begin:41:08,1962        End:41:08,2102  TPool:3 TId:5   App:44665200    Begin:41:08,2052        End:41:08,2152  TPool:3 TId:7   App:12547953    Begin:41:08,2092        End:41:08,2192  TPool:3 TId:6   App:37489757    Begin:41:08,2102        End:41:08,2202  TPool:3 TId:5   App:44665200    Begin:41:08,2152        End:41:08,2252  TPool:3 TId:7   App:12547953    Begin:41:08,2192        End:41:08,2292  TPool:3
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

共产生线程3个,总用时0.236秒。

根据以上的数据,我们可以得出结论,要提高系统响应时间与并发处理数,应尽可能减少WEB线程的等待。

【略】请各位自行查验当一次并发全部处理完毕后再次测试的处理情况。

【略】请各位自行查验当处理程序中使用线程池处理等待任务的处理情况。

如何减少WEB线程的等待呢,那就应该尽早的结果ProcessRequest方法,前一篇中讲到,对于一些需要等待完成的任务,可以使用异步方法来做,于是我们可以在ProcessRequest中调用异步方法,但问题是当ProcessRequest结束后,请求处理也即将结束,一但请求结束,将没有办法在这一次请求中返回结果给客户端,但是此时,异步任务还没有完成,当异步任务完成时,也许再也没有办法将结果传给客户端了。(难道用轮询?囧)

我们需要的方案是,处理请求时可以暂停处理(不是暂停线程),并保持客户端连接,在需要时,向客户端输出结果,并结束请求。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

在这个模型中,可以看到,对于WebServerRuntime来说,我们的请求处理程序就是一个异步方法,而对于客户端来说,却并不知道后面的处理情况。无论在WebServerRuntime或是我们的处理程序,都没有直接占用线程,一切由何时SetComplete决定。同时可以看到,这种模式需要WebServerRuntime的紧密配合,提供调用异步方法的接口。在ASP.NET中,这个接口就是IHttpAsyncHandler。

异步ASP.NET处理程序

首先,我们来实现第一个异步处理程序,在适当的时候触发结束,在开始和结束时输出一些信息。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class Handler : IHttpHandler, IHttpAsyncHandler {     public void ProcessRequest(HttpContext context)     {         //异步处理器不执行该方法     }    public bool IsReusable     {         //设置允许重用对象         get { return false; }     }          //请求开始时由ASP.NET调用此方法     public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)     {         context.Response.ContentType = "text/xml";         context.Response.Write("App:");         context.Response.Write(context.ApplicationInstance.GetHashCode());         context.Response.Write("\tBegin:");         context.Response.Write(DateTime.Now.ToString("mm:ss,ffff"));         //输出当前线程         context.Response.Write("\tThreadId:");         context.Response.Write(Thread.CurrentThread.ManagedThreadId);         //构建异步结果并返回         var result = new WebAsyncResult(cb, context);         //用一个定时器来模拟异步触发完成         Timer timer = null;         timer = new Timer(o =>         {             result.SetComplete();             timer.Dispose();         }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));         return result;     }    //异步结束时,由ASP.NET调用此方法     public void EndProcessRequest(IAsyncResult result)     {         WebAsyncResult webresult = (WebAsyncResult)result;         webresult.Context.Response.Write("\tEnd:");         webresult.Context.Response.Write(DateTime.Now.ToString("mm:ss,ffff"));         //输出当前线程         webresult.Context.Response.Write("\tThreadId:");         webresult.Context.Response.Write(Thread.CurrentThread.ManagedThreadId);     }    //WEB异步方法结果     class WebAsyncResult : IAsyncResult     {         private AsyncCallback _callback;        public WebAsyncResult(AsyncCallback cb, HttpContext context)         {             Context = context;             _callback = cb;         }        //当异步完成时调用该方法         public void SetComplete()         {             IsCompleted = true;             if (_callback != null)             {                 _callback(this);             }         }        public HttpContext Context         {             get;             private set;         }        public object AsyncState         {             get { return null; }         }        //由于ASP.NET不会等待WEB异步方法,所以不使用此对象         public WaitHandle AsyncWaitHandle         {             get { throw new NotImplementedException(); }         }        public bool CompletedSynchronously         {             get { return false; }         }        public bool IsCompleted         {             get;             private set;         }     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

在这里,我们实现了一个简单的AsyncResult,由于ASP.NET通过回调方法获取异步完成,不会等待异步,所以不需要WaitHandle。在开始请求时,建立一个AsyncResult后直接返回,当异步完成时,调用AsyncResult的SetComplete方法,调用回调方法,再由ASP.NET调用异步结束。此时整个请求即完成。

当我们访问这个地址,可以得到类似于下面的结果:

App:11240144 Begin:37:24,2676 ThreadId:6 End:37:29,2619 ThreadId:6

可以看到开始和结束在同一个线程中运行。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

当有多个并发请求时,线程池将忙碌起来,开始与结束处理也奖有机会运行于不同的线程上。50个请求并发时的处理数据:

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
App:52307948    Begin:39:47,8128        ThreadId:6      End:39:52,8231  ThreadId:5 App:58766839    Begin:39:47,8358        ThreadId:5      End:39:52,8321  ThreadId:7 App:23825510    Begin:39:47,8348        ThreadId:5      End:39:52,8321  ThreadId:7 App:30480920    Begin:39:47,8348        ThreadId:5      End:39:52,8321  ThreadId:7 App:62301924    Begin:39:47,8348        ThreadId:6      End:39:52,8321  ThreadId:6 App:28062782    Begin:39:47,8338        ThreadId:5      End:39:52,8321  ThreadId:6 App:41488021    Begin:39:47,8338        ThreadId:6      End:39:52,8321  ThreadId:7 App:15315213    Begin:39:47,8338        ThreadId:6      End:39:52,8321  ThreadId:6 App:17228638    Begin:39:47,8328        ThreadId:5      End:39:52,8321  ThreadId:7 App:51438283    Begin:39:47,8328        ThreadId:6      End:39:52,8321  ThreadId:6 App:32901400    Begin:39:47,8328        ThreadId:5      End:39:52,8321  ThreadId:7 App:61925337    Begin:39:47,8358        ThreadId:6      End:39:52,8321  ThreadId:6 App:24914721    Begin:39:47,8318        ThreadId:6      End:39:52,8321  ThreadId:6 App:26314214    Begin:39:47,8318        ThreadId:6      End:39:52,8321  ThreadId:6 App:51004322    Begin:39:47,8358        ThreadId:6      End:39:52,8321  ThreadId:6 App:51484875    Begin:39:47,8308        ThreadId:5      End:39:52,8321  ThreadId:7 App:19420176    Begin:39:47,8308        ThreadId:6      End:39:52,8321  ThreadId:6 App:16868352    Begin:39:47,8298        ThreadId:6      End:39:52,8321  ThreadId:7 App:61115195    Begin:39:47,8298        ThreadId:5      End:39:52,8321  ThreadId:6 App:63062333    Begin:39:47,8288        ThreadId:6      End:39:52,8321  ThreadId:6 App:53447344    Begin:39:47,8298        ThreadId:5      End:39:52,8321  ThreadId:7 App:31665793    Begin:39:47,8288        ThreadId:5      End:39:52,8321  ThreadId:7 App:2174563     Begin:39:47,8288        ThreadId:6      End:39:52,8321  ThreadId:6 App:12053474    Begin:39:47,8318        ThreadId:5      End:39:52,8321  ThreadId:7 App:41728762    Begin:39:47,8278        ThreadId:6      End:39:52,8321  ThreadId:6 App:6385742     Begin:39:47,8278        ThreadId:5      End:39:52,8321  ThreadId:7 App:13009416    Begin:39:47,8268        ThreadId:6      End:39:52,8321  ThreadId:6 App:43205102    Begin:39:47,8268        ThreadId:5      End:39:52,8321  ThreadId:7 App:14333193    Begin:39:47,8268        ThreadId:6      End:39:52,8321  ThreadId:6 App:2808346     Begin:39:47,8258        ThreadId:6      End:39:52,8321  ThreadId:6 App:37489757    Begin:39:47,8128        ThreadId:5      End:39:52,8231  ThreadId:6 App:34106743    Begin:39:47,8258        ThreadId:5      End:39:52,8321  ThreadId:7 App:30180123    Begin:39:47,8248        ThreadId:6      End:39:52,8321  ThreadId:6 App:44313942    Begin:39:47,8248        ThreadId:5      End:39:52,8321  ThreadId:7 App:12611187    Begin:39:47,8248        ThreadId:6      End:39:52,8321  ThreadId:6 App:7141266     Begin:39:47,8238        ThreadId:5      End:39:52,8321  ThreadId:7 App:25425822    Begin:39:47,8278        ThreadId:5      End:39:52,8321  ThreadId:7 App:51288387    Begin:39:47,8238        ThreadId:5      End:39:52,8321  ThreadId:7 App:66166301    Begin:39:47,8228        ThreadId:6      End:39:52,8321  ThreadId:6 App:34678979    Begin:39:47,8228        ThreadId:6      End:39:52,8321  ThreadId:7 App:10104599    Begin:39:47,8218        ThreadId:5      End:39:52,8321  ThreadId:6 App:47362231    Begin:39:47,8258        ThreadId:5      End:39:52,8321  ThreadId:7 App:40535505    Begin:39:47,8218        ThreadId:6      End:39:52,8321  ThreadId:7 App:20726372    Begin:39:47,8368        ThreadId:5      End:39:52,8321  ThreadId:5 App:2730334     Begin:39:47,8368        ThreadId:6      End:39:52,8321  ThreadId:6 App:59884855    Begin:39:47,8368        ThreadId:5      End:39:52,8321  ThreadId:7 App:39774547    Begin:39:47,8238        ThreadId:6      End:39:52,8321  ThreadId:6 App:12070837    Begin:39:47,8378        ThreadId:6      End:39:52,8491  ThreadId:7 App:64828693    Begin:39:47,8218        ThreadId:5      End:39:52,8331  ThreadId:6 App:14509978    Begin:39:47,9308        ThreadId:6      End:39:52,9281  ThreadId:5
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

可以看到,从始至终只由3个线程处理所有的请求,总共时间约5.12秒。

为简化分析,我们用下面的图来示意异步处理程序的并发处理过程。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

这样,我们就可以通过异步的方式,将WEB线程撤底释放出来。由WEB线程进行请求的接收与结束处理,耗时的操作与等待都进行异步处理。这样少量的WEB线程就可以承受大量的并发请求,WEB线程将不再成为系统的瓶颈。

在大并发的异步模式下,和前面的数据相比较,可以看到HttpApplication的对象数量随并发处理数提高而提高,随之带来的一系列数据结构,如HttpHandler缓存,是需要考虑的内存开销。同时,在异步模式下,请求的完成需要编程的方式来控制,在触发完成前,客户端连接、HttpContext对象都保持活动状态,客户端也一直保持等待,直到超时。因此,异步模式下需要更细致的资源操作。

我们来看ASP.NET异步 的典型应用场景。

场景一:处理过程中有需要等待的任务,并且可以使用异步完成的。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
//同步方法 public void ProcessRequest(HttpContext context) {     FileStream fs = new FileStream("", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous);     fs.CopyTo(context.Response.OutputStream); }      //异步方法开始 public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {     FileStream fs = new FileStream("D:\\a.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous);     var task = fs.CopyToAsync(context.Response.OutputStream);     task.GetAwaiter().OnCompleted(() => cb(task));     return task; }//异步方法结束 public void EndProcessRequest(IAsyncResult result) { }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

这个处理程序读取服务器的文件并输出到客户端。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
//同步方法 public void ProcessRequest(HttpContext context) {     var url = context.Request.QueryString["url"];     var request = (HttpWebRequest)WebRequest.Create(url);     var response = request.GetResponse();     var stream = response.GetResponseStream();     stream.CopyTo(context.Response.OutputStream); }      //异步方法开始 public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {     //构建异步结果并返回     var result = new WebAsyncResult(cb, context);    var url = context.Request.QueryString["url"];     var request = (HttpWebRequest)WebRequest.Create(url);     var responseTask = request.GetResponseAsync();     responseTask.GetAwaiter().OnCompleted(() =>     {         var stream = responseTask.Result.GetResponseStream();         stream.CopyToAsync(context.Response.OutputStream).GetAwaiter().OnCompleted(() =>         {             result.SetComplete();         });     });    return result; }//异步方法结束 public void EndProcessRequest(IAsyncResult result) { }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

这是一个简单的代理,服务器获取WEB资源后写回。

在这类程序中,我们提供的异步处理程序调用了IOCP异步方法,使得大量节省了WEB线程的占用,相比同步处理程序来说,并发量会得到相当大的提升。

【注意】前面提到,由于WEB线程属于线程池线程,因此,如果在线程池中加入任务,将同样会影响并发处理数。而在异步处理程序中,由线程池来完成异步将得不到任何本质上的提升,因此在异步处理程序中禁止操作线程池(ThreadPool.QueueUserWorkItem、delegate.BeginInvoke,Task.Run等)。如果确定需要使用多线程来处理大量的计算,需要自己开启线程或实现自己的线程池。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {     return new Action(() =>     {         Thread.Sleep(1000);         context.Response.Write("OK");     }).BeginInvoke(cb, extraData); }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

上面的代码将无法达到异步的效果。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

虽然等待工作交由另一线程去操作,但是该线程与WEB线程性质相同,同样会导致其他请求阻塞。

【思考】如果我们的程序中的确需要有大量的计算,那么可以考虑将这些计算提取到独立的应用服务器中,然后通过网络IOCP异步调用,达到WEB服务器的高吞吐量与系统的平行扩展性。

典型应用场景二:长连接消息推送。

一般来说,在WEB中获取服务器消息,采用轮询的方式,这种方式不可避免会有延时,当我们需要即时消息的推送时(如WEBIM),需要用到长连接。

长连接方式,由客户端发起请求,服务器端接收后暂停处理并保持连接,当需要发送消息给客户端时,输出内容并结束处理,客户端得到消息或者超时后,再次发起连接。如此达到在HTTP协议上服务器消息即时推送到客户端的目的。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

在这种情况下,我们希望服务器尽可能长时间保持连接,如果采用同步处理程序,则连接数受到服务器线程数的限制,而异步处理程序则可以很好的解决这个问题。异步处理程序开始时,收集相关信息,并放入集合后返回异步结果。当需要向这个客户端发送消息时,从客户端集合中找到需要发送的目标,发送完成即可。

首先,我们需要对客户端进行标识,这个标识往往采用sessionid来做,本例中简单起见,通过客户端传递参数获取。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class WebAsyncResult : IAsyncResult {     private AsyncCallback _callback;    public WebAsyncResult(AsyncCallback cb, HttpContext context, string clientID)     {         Context = context;         ClientID = clientID;         _callback = cb;     }    //当异步完成时调用该方法     public void SetComplete()     {         IsCompleted = true;         if (_callback != null)         {             _callback(this);         }     }
    //存储客户端标识     public string ClientID     {         get;         private set;     }    public HttpContext Context     {         get;         private set;     }    public object AsyncState     {         get { return null; }     }    //由于ASP.NET不会等待WEB异步方法,所以不使用此对象     public WaitHandle AsyncWaitHandle     {         get { throw new NotImplementedException(); }     }    public bool CompletedSynchronously     {         get { return false; }     }    public bool IsCompleted     {         get;         private set;     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

我们需要一个集合来保存连接中的客户端,提供一个向这些客户端发送消息的方法。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class WebAsyncResultCollection : List<WebAsyncResult>, ICollection<WebAsyncResult> {     private static WebAsyncResultCollection _instance = new WebAsyncResultCollection();    public static WebAsyncResultCollection Instance     {         get { return WebAsyncResultCollection._instance; }     }    public bool SendMessage(string clientID, string message)     {         var result = this.FirstOrDefault(r => r.ClientID == clientID);         if (result != null)         {             Remove(result);             bool sendsuccess = false;             if (result.Context.Response.IsClientConnected)             {                 sendsuccess = true;                 result.Context.Response.Write(message);             }             result.SetComplete();             return sendsuccess;         }         return false;     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

对于异步处理程序的开始方法,我们收集信息并放入集合。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {     var clientID = context.Request.QueryString["id"];     WebAsyncResultCollection.Instance.SendMessage(clientID, "ClearClientID");     WebAsyncResult result = new WebAsyncResult(cb, context, clientID);     WebAsyncResultCollection.Instance.Add(result);     return result; }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

【不完善】由于客户端收到一次消息后结束请求,由客户端再次发起请求,中间会有部分时间间隙,在这间隙中向该客户端发送的消息将丢失,解决方案是维护另一个用户是否在线的表,如果用户不在线,则处理离线消息,如果在线,并且正在连接中,则按上述处理,如果不在连接中,则缓存在服务器,当客户端再次连接时,首先检查缓存的消息,如果有未接消息,则获取消息并立即返回。

发送消息的处理程序。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class SendMessage : IHttpHandler {    public void ProcessRequest(HttpContext context)     {         var clientID = context.Request.QueryString["clientID"];         var message = context.Request.QueryString["message"];         WebAsyncResultCollection.Instance.SendMessage(clientID, message);     }    public bool IsReusable     {         get         {             return true;         }     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

可以在任何需要的位置向客户端发送消息。

【不完善】我们需要定时刷新客户端集合,对于长时间未处理的客户端进行超时结束处理。

通过异步处理程序构建的长连接消息推送机制,单台服务器可以轻松支持上万个并发连接。

异步Action

在ASP.NET MVC 4中,添加了对异步Action的支持。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

在ASP.NET MVC4中,整个处理过程都是异步的。

在图中可以看到,最右边的ActionDescriptor将决定如何调用我们的Action方法,而如何调用是由具体的Action方法形式决定,ASP.NET MVC会根据不同的方法形式创建不同的ActionDescriptor实例,从而调用不同的处理过程。对于传统的方法,则使用ReflectedActionDescriptor,他实现Execute方法,调用我们的Action,并在AsyncControllerActionInvoker包装成同步调用。而异步调用在ASP.NET MVC 4 中有两种模式。

异步Action模式一:AsyncController/XXXAsync/XXXCompleted

我们可以使一个Controller继承自AsyncController,按照约定同时提供两个方法,分别命名为XXXAsync/XXXCompleted,ASP.NET MVC则会将他们包装成ReflectedAsyncActionDescriptor。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class DefaultController : AsyncController {     public void DoAsync()     {         //注册一次异步         AsyncManager.OutstandingOperations.Increment();         Timer timer = null;         timer = new Timer(o =>         {             //一次异步完成             AsyncManager.OutstandingOperations.Decrement();             timer.Dispose();         },null, 5000, 5000);     }    public ActionResult DoCompleted()     {         return Content("OK");     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

由于没有IAsyncResult,我们需要通过AsyncManager来告诉ASP.NET MVC何时完成异步,我们可以在方法内部在启用异步时调用AsyncManager.OutstandingOperations.Increment()告诉ASP.NET MVC开始了一次异步,完成异步时调用AsyncManager.OutstandingOperations.Decrement()告诉ASP.NET MVC完成了一次异步,当所有异步完成,AsyncManager会自动触发异步完成事件,调用回调方法,最终调用我们的XXXComplete方法。我们也可以用AsyncManager.Finish()也触发所有异步完成。当不使用任何AsyncManager时,则不启用异步。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

可以看到整个异步过程由ASP.NET完成,在适当的时候会调用我们的方法。异步的开始、结束动作与及如何触发完成都在我们的代码中体现。

异步Action模式二:Task Action

对于Action,如果返回的类型是 Task,ASP.NET MVC则会将他们包装成TaskAsyncActionDescriptor。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet
public class DefaultController : Controller {     public async Task<FileResult> Download()     {         using (FileStream fs = new FileStream("D:\\a.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous))         {             byte[] data = new byte[fs.Length];             await fs.ReadAsync(data, 0, data.Length);             return new FileContentResult(data, "application/octet-stream");         }     } }
用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

我只需要需提供一个返回类型为Task的方法即可,我里我们采用async/await语法构建一个异步方法,在方法内部调用其他的异步方法。

用 ASP.NET MVC 实现基于 XMLHttpRequest long polling(长轮询) 的 Comet

相比之前的模式,简单了一些,特别是我们的Controller中,只有一个方法,异步的操作都交由Task完成。对于可以返回Task的方法来说(如通过async/await包装多个异步方法),就显得十分方便。