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代碼在收到一個長連接反饋后馬上又建立一個長連接,這是關鍵所在。

  源代碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM