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代码:
jQuery(function ($) { function long_polling() { $.getJSON('/comet/LongPolling', function (data) { if (data.d) { $('#logs').append(data.d + "<br/>"); } long_polling(); }); } long_polling(); });
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秒钟向客户端发送服务器当时间)
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); } }
实现异步控制器需要继承 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。
示例代码运行结果如下:
小结
以前觉得 Comet 是很高深的东西,自己动手做了之后,发觉原来没那么难。所以,重要的是动手去做!
如果不能在实际项目中去做,那就写一篇博客吧!
代码下载
在前文中,介绍了.NET下的多种异步的形式,在WEB程序中,天生就是多线程的,因此使用异步应该更为谨慎。本文将着重展开ASP.NET中的异步。 【注意】本文中提到的异步指的是服务器端异步,而非客户端异步(Ajax)。 对于HTTP的请求响应模型,服务器无法主动通知或回调客户端,当客户端发起一个请求后,必须保持连接等待服务器的返回结果,才能继续处理,因此,对于客户端来说,请求与响应是无法异步进行,也就是说无论服务器如何处理请求,对于客户端来说没有任何差别。 那么ASP.NET异步指的又是什么,解决了什么问题呢? 在解释ASP.NET异步前,先来考察下ASP.NET线程模型。 ASP.NET线程模型 我们知道,一个WEB服务可以同时服务器多个用户,我们可以想象一下,WEB程序应该运行于多线程环境中,对于运行WEB程序的线程,我们可以称之为WEB线程,那么,先来看看WEB线程长什么样子吧。 我们可以用一个HttpHandler输出一些内容。 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;} } } 你可以看到类似于这样的结果: Name: ManagedThreadId:57 IsBackground:True IsThreadPoolThread:True 这里可以看到,WEB线程是一个没有名称的线程池中的线程,如果刷新这个页面,还有机会看到 ManagedThreadId 在不断变化,并且可能重复出现。说明WEB程序有机会运行于线程池中的不同线程。 为了模拟多用户并发访问的情况,我们需要对这个处理程序添加人为的延时,并输出线程相关信息与开始结束时间,再通过客户端程序同时发起多个请求,查看返回的内容,分析请求的处理情况。 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 ) ); } 我们用一个命令行程序来发起请求,并显示结果。 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(); } 这里,我们同时发起了50个请求,然后观察响应的情况。 【注意】后面的结果会因为操作系统、IIS版本、管道模式、.NET版本、配置项 的不同而不同,以下结果为在Windows Server 2008 R2 + IIS7.5 + .NET 4.5 beta(.NET 4 runtime) + 默认配置 中测试的结果,在没有特别说明的情况下,均为重启IIS后第一次运行的情况。 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 从这个结果大概可以看出,开始两个请求几乎同时开始处理,因为线程池最小线程数为2(可配置),紧接着后面的请求会每隔半秒钟开始一个,因为如果池中的线程都忙,会等待半秒(.NET版本不同而不同),如果还是没有线程释放则开启新的线程,直到达到最大线程数(可配置)。未能在线程池中处理的请求将被放入请求队列,当一个线程释放后,下一个请求紧接着开始在该线程处理。 最终50个请求共产生24个线程,总用时约35.9秒。 光看数据不够形象,用简单的代码把数据转换成图形吧,下面是100个请求的处理过程。 我们可以看到,当WEB线程长时间被占用时,请求会由于线程池而阻塞,同时产生大量的线程,最终响应时间变长。 作为对比,我们列出处理时间10毫秒的数据。 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 共产生线程3个,总用时0.236秒。 根据以上的数据,我们可以得出结论,要提高系统响应时间与并发处理数,应尽可能减少WEB线程的等待。 【略】请各位自行查验当一次并发全部处理完毕后再次测试的处理情况。 【略】请各位自行查验当处理程序中使用线程池处理等待任务的处理情况。 如何减少WEB线程的等待呢,那就应该尽早的结果ProcessRequest方法,前一篇中讲到,对于一些需要等待完成的任务,可以使用异步方法来做,于是我们可以在ProcessRequest中调用异步方法,但问题是当ProcessRequest结束后,请求处理也即将结束,一但请求结束,将没有办法在这一次请求中返回结果给客户端,但是此时,异步任务还没有完成,当异步任务完成时,也许再也没有办法将结果传给客户端了。(难道用轮询?囧) 我们需要的方案是,处理请求时可以暂停处理(不是暂停线程),并保持客户端连接,在需要时,向客户端输出结果,并结束请求。 在这个模型中,可以看到,对于WebServerRuntime来说,我们的请求处理程序就是一个异步方法,而对于客户端来说,却并不知道后面的处理情况。无论在WebServerRuntime或是我们的处理程序,都没有直接占用线程,一切由何时SetComplete决定。同时可以看到,这种模式需要WebServerRuntime的紧密配合,提供调用异步方法的接口。在ASP.NET中,这个接口就是IHttpAsyncHandler。 异步ASP.NET处理程序 首先,我们来实现第一个异步处理程序,在适当的时候触发结束,在开始和结束时输出一些信息。 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; } } } 在这里,我们实现了一个简单的AsyncResult,由于ASP.NET通过回调方法获取异步完成,不会等待异步,所以不需要WaitHandle。在开始请求时,建立一个AsyncResult后直接返回,当异步完成时,调用AsyncResult的SetComplete方法,调用回调方法,再由ASP.NET调用异步结束。此时整个请求即完成。 当我们访问这个地址,可以得到类似于下面的结果: App:11240144 Begin:37:24,2676 ThreadId:6 End:37:29,2619 ThreadId:6 可以看到开始和结束在同一个线程中运行。 当有多个并发请求时,线程池将忙碌起来,开始与结束处理也奖有机会运行于不同的线程上。50个请求并发时的处理数据: 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 可以看到,从始至终只由3个线程处理所有的请求,总共时间约5.12秒。 为简化分析,我们用下面的图来示意异步处理程序的并发处理过程。 这样,我们就可以通过异步的方式,将WEB线程撤底释放出来。由WEB线程进行请求的接收与结束处理,耗时的操作与等待都进行异步处理。这样少量的WEB线程就可以承受大量的并发请求,WEB线程将不再成为系统的瓶颈。 在大并发的异步模式下,和前面的数据相比较,可以看到HttpApplication的对象数量随并发处理数提高而提高,随之带来的一系列数据结构,如HttpHandler缓存,是需要考虑的内存开销。同时,在异步模式下,请求的完成需要编程的方式来控制,在触发完成前,客户端连接、HttpContext对象都保持活动状态,客户端也一直保持等待,直到超时。因此,异步模式下需要更细致的资源操作。 我们来看ASP.NET异步 的典型应用场景。 场景一:处理过程中有需要等待的任务,并且可以使用异步完成的。 //同步方法 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) { } 这个处理程序读取服务器的文件并输出到客户端。 //同步方法 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) { } 这是一个简单的代理,服务器获取WEB资源后写回。 在这类程序中,我们提供的异步处理程序调用了IOCP异步方法,使得大量节省了WEB线程的占用,相比同步处理程序来说,并发量会得到相当大的提升。 【注意】前面提到,由于WEB线程属于线程池线程,因此,如果在线程池中加入任务,将同样会影响并发处理数。而在异步处理程序中,由线程池来完成异步将得不到任何本质上的提升,因此在异步处理程序中禁止操作线程池(ThreadPool.QueueUserWorkItem、delegate.BeginInvoke,Task.Run等)。如果确定需要使用多线程来处理大量的计算,需要自己开启线程或实现自己的线程池。 public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { return new Action(() => { Thread.Sleep(1000); context.Response.Write("OK"); }).BeginInvoke(cb, extraData); } 上面的代码将无法达到异步的效果。 虽然等待工作交由另一线程去操作,但是该线程与WEB线程性质相同,同样会导致其他请求阻塞。 【思考】如果我们的程序中的确需要有大量的计算,那么可以考虑将这些计算提取到独立的应用服务器中,然后通过网络IOCP异步调用,达到WEB服务器的高吞吐量与系统的平行扩展性。 典型应用场景二:长连接消息推送。 一般来说,在WEB中获取服务器消息,采用轮询的方式,这种方式不可避免会有延时,当我们需要即时消息的推送时(如WEBIM),需要用到长连接。 长连接方式,由客户端发起请求,服务器端接收后暂停处理并保持连接,当需要发送消息给客户端时,输出内容并结束处理,客户端得到消息或者超时后,再次发起连接。如此达到在HTTP协议上服务器消息即时推送到客户端的目的。 在这种情况下,我们希望服务器尽可能长时间保持连接,如果采用同步处理程序,则连接数受到服务器线程数的限制,而异步处理程序则可以很好的解决这个问题。异步处理程序开始时,收集相关信息,并放入集合后返回异步结果。当需要向这个客户端发送消息时,从客户端集合中找到需要发送的目标,发送完成即可。 首先,我们需要对客户端进行标识,这个标识往往采用sessionid来做,本例中简单起见,通过客户端传递参数获取。 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; } } 我们需要一个集合来保存连接中的客户端,提供一个向这些客户端发送消息的方法。 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; } } 对于异步处理程序的开始方法,我们收集信息并放入集合。 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; } 【不完善】由于客户端收到一次消息后结束请求,由客户端再次发起请求,中间会有部分时间间隙,在这间隙中向该客户端发送的消息将丢失,解决方案是维护另一个用户是否在线的表,如果用户不在线,则处理离线消息,如果在线,并且正在连接中,则按上述处理,如果不在连接中,则缓存在服务器,当客户端再次连接时,首先检查缓存的消息,如果有未接消息,则获取消息并立即返回。 发送消息的处理程序。 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; } } } 可以在任何需要的位置向客户端发送消息。 【不完善】我们需要定时刷新客户端集合,对于长时间未处理的客户端进行超时结束处理。 通过异步处理程序构建的长连接消息推送机制,单台服务器可以轻松支持上万个并发连接。 异步Action 在ASP.NET MVC 4中,添加了对异步Action的支持。 在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。 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"); } } 由于没有IAsyncResult,我们需要通过AsyncManager来告诉ASP.NET MVC何时完成异步,我们可以在方法内部在启用异步时调用AsyncManager.OutstandingOperations.Increment()告诉ASP.NET MVC开始了一次异步,完成异步时调用AsyncManager.OutstandingOperations.Decrement()告诉ASP.NET MVC完成了一次异步,当所有异步完成,AsyncManager会自动触发异步完成事件,调用回调方法,最终调用我们的XXXComplete方法。我们也可以用AsyncManager.Finish()也触发所有异步完成。当不使用任何AsyncManager时,则不启用异步。 可以看到整个异步过程由ASP.NET完成,在适当的时候会调用我们的方法。异步的开始、结束动作与及如何触发完成都在我们的代码中体现。 异步Action模式二:Task Action 对于Action,如果返回的类型是 Task,ASP.NET MVC则会将他们包装成TaskAsyncActionDescriptor。 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"); } } } 我只需要需提供一个返回类型为Task的方法即可,我里我们采用async/await语法构建一个异步方法,在方法内部调用其他的异步方法。 相比之前的模式,简单了一些,特别是我们的Controller中,只有一个方法,异步的操作都交由Task完成。对于可以返回Task的方法来说(如通过async/await包装多个异步方法),就显得十分方便。 |