Asp.Net实现Http长连接推送
话说最新帮一个朋友搞智能家居方面的东西,做一个云平台。主要作用手机在局域网外环境时对手机客户端和智能网关中命令的互相转发。
目前已经有了一个稳定的Socket版本,但是考虑到以后的扩展和性能指标要改成Http长连接形式,这确实是一个很逗逼的方案。
下面普及一下Http长连接的概念,所谓的Http长连接其实不是指像Socket那样的建立一个连接client端和server端来回传递数据。Http长连接指的是客户端发送给服务器端的Http请求不会马上得到服务器的应答,而是当满足一定条件时服务器才“主动”将数据返回给客户端,这时一次Http请求才算结束。实际应用中为客户端在结束了一个长连接后往往要再次建立一个长连接,也就是客户端到服务器端总是维持一个打开的下行Http通道。
搞过Socket的同学都知道,Socket通讯中除了有自己的协议以外还要有心跳的命令,以此来保证客户端和服务器端连接的状态。这些本文都不去深究,主要还是说长连接的这个小框架。
代码是我们最好的伙伴,下面我们结合代码说说这个简单的东西。
Asp.Net4.0中加入了很多异步特性,其中IHttpAsyncHandler配合IAsyncResult可以很好的解决本文的需求。首先我们定义一个类实现IAsyncResult这个接口
using System; using System.Collections.Generic; using System.Linq; using System.Web; using log4net; namespace SM.BIZKeepAliveHttp { /// <summary> /// 一个异步会话,会话会被临时缓存 /// </summary> public class HKAsyncRequest : IAsyncResult { private static readonly ILog logger = LogManager.GetLogger(typeof(HKAsyncRequest)); public HKAsyncRequest(HttpContext context, AsyncCallback cb, object extraData) { this.Context = context; this.CallBack = cb; this.ExtraData = extraData; } public HttpContext Context { get; set; } public object ExtraData { get; set; } public AsyncCallback CallBack { get; set; } public bool IsCompleted { get; set; } public object AsyncState { get; set; } public System.Threading.WaitHandle AsyncWaitHandle { get; set; } public bool CompletedSynchronously { get { return false; } } public void Send(string response) { if (String.IsNullOrEmpty(response)) return; try { this.Context.Response.ContentType = "text/plain"; this.Context.Response.Write(response); if (this.CallBack != null) { this.CallBack(this); } } catch (Exception ex) { logger.Error("输出到客户端发生错误:" + ex.Message); } finally { IsCompleted = true; } } public void Send(byte[] b,int offset,int length){ string str = Func.ByteArrayToHexString(b); this.Send(str); } } }
这个类没有什么难的,主要是保存外部传进来的HttpContext、AsyncCallBack和ExtraData,HttpContext用来向Response中写回应,AsyncCallBack用来结束当前Http长连接请求,ExtraData自己该干什么干什么我没有用它。这里需要注意的是这个类中的CompletedSynchronously属性要返回false,不然客户端收不到数据。而且各个属性也别随便返回null,不然在写入Response时会报空指针的错误。
下面我们看看另一个接口的实现。在项目中新建一个一般处理程序(.ashx)文件。实现IAsyncHandler接口:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using log4net; using Newtonsoft.Json.Linq; namespace SM.BIZKeepAliveHttp { public class Data : IHttpAsyncHandler { public static readonly string DATAFIELD = "data"; private static readonly ILog logger = LogManager.GetLogger(typeof(Data)); public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { context.Response.Cache.SetCacheability(HttpCacheability.NoCache); string value = context.Request.Params.Get(DATAFIELD); //这里传过来的是SessionId,不是数据,数据不做重复Parse //用sessionId去缓存中找对应的会话,并填充异步AsyncResult HKAsyncRequest result = new HKAsyncRequest(context, cb, extraData); string error = null; if (String.IsNullOrEmpty(value)) { error = "500 SessionId is null"; context.Response.StatusCode = 500; logger.Error(error); result.Send(error); return result; } List<AliveClient> acs = AsyncManager.Sessions.FindAll(x => x.SessionId.Equals(value)); if (acs == null || acs.Count == 0) { error = "404 SessionId:" + value + " has no connection."; context.Response.StatusCode = 404; logger.Debug(error); result.Send(error); return result; } AliveClient ac = acs.First(); ac.Result = result; //执行命令 CommondFactory.ExecuteCommond(ac); return result; } public void EndProcessRequest(IAsyncResult result) { } public void ProcessRequest(HttpContext context) { } public bool IsReusable { get { return false; } } } }
这个类中主要实现的方法只有一个,那就是BeginProcessRequest,其他方法不用写任何代码。这个方法主要作用是建立一个IAsyncResult实例后保存起来,便于以后服务器端有了数据或是满足了特定情况把数据返回给客户端。所以我在代码里面建立了一个静态List的缓存保存这些IAsyncResult实现。当然这就是Asp.Net实现Http长连接的核心所在了。
其它的就不多说了,大家可以看源代码,看代码时大家会发现我实际建立了两个.ashx文件,这和我这个项目的逻辑有关,因为协议规定客户端发送一条数据后服务器端马上要做出回应,所以我用一个传统的ashx作回应,回应前这个传统的ashx(connection.ashx)先分析数据把分析后的数据模型保存起来,同时给客户端一个SessionId。客户端收到回应后用这个SessionId发起长连接请求,服务器端就不再重复分析数据了,而是将之前的数据从缓存中取出使用,为了调试方便我把这个SessionId写死了。同时我用Quartz.Net建立了两个任务,一个CleanJob.cs实际是作为清理任务,定时清理掉缓存中的无效或已完成请求5分钟跑一次。还有一个任务是HeartJob.cs主要是用来模拟服务器端推送的逻辑,30秒跑一次。
用到Quartz.Net是因为我个人认为在Asp.net中直接启动BackgroundWorker的方式不是很好,还是调度引擎的线程模型更可靠。具体启动调度引擎的代码在Global.asax里面。
附件中是我剥离出来的代码,删除了业务部分只做测试用。测试界面为index.aspx,在文本框中写点东西点提交,先收到服务器的回应后每个30秒收到服务器的回应弹出alert窗口。这里要提的就是客户端js代码在收到一个长连接反馈后马上又建立一个长连接,这是关键所在。