asp.net 中長尾鏈接實現推送 -- comet


 

 一般需求推送服務時,都會去第三方拿推送組件,如”極光“,”百度“,”小米"什么的,自己用.net實現推送服務端需要面對很多問題,比如C10K,但是企業內部使用往往用不了10K的鏈接,有個1K,2K就足夠,這個時候完全可以自己實現一個推送服務,這樣手機應用就不用走外網了。

 

使用.net實現推送服務有幾個選擇

1.是使用WCF 基於TCP的回調-針對.net To .net 端,經過7*24小時測試,2K左右的鏈接能穩定hold住,參考:http://www.cnblogs.com/wdfrog/p/3924718.html


2.自己使用TcpListener或Socket實現一個長連擊服務器,由於推送的信息都很短(長信息只推送信息編號),這樣在一個MTU里面就可以完成傳輸,整體上實現起來也不麻煩 ,最近也做了個,大概加一起就400百來行代碼,2K鏈接已經(實際只要hold住98個用戶就好了)穩定運行了6天了。

3.使用Comet Request, 首先Comet Request 采用Asp.net + IIS,整整網頁就好了,另外由於IIS應用程序池會定期回收,程序寫的爛點也不影響,每天都給你一個新的開始,還有Comet 的實現網上代碼很多,還有開源的SignalR可以用.

 

采用Comet方式的實現

1.服務端,使用IHttpAsyncHandler來Hold住請求,需要根據cookie或QueryString中帶的UserId來區分不同的用戶

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MangoPush.WebComet.Core
{
    public class PushAsyncHandle : IHttpAsyncHandler
    {
        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
        {
            
            var ar = new PushAsyncResult(context, cb, extraData);
            CometRequestMgr.Instance.Add(ar);
            return ar;
        }

        public void EndProcessRequest(IAsyncResult result)
        {
            
        }

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            throw new NotImplementedException();
        }
    }
}
View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

namespace MangoPush.WebComet.Core
{
    public class PushAsyncResult:IAsyncResult
    {
        private static int GId = 0;

        private bool m_IsCompleted = false;
        private AsyncCallback Callback = null;
        public HttpContext Context;
        private Object _AsyncState;
        public DateTime AddTime{get;private set;}
        public int Id { get; set; }
        
        public PushAsyncResult(HttpContext context, AsyncCallback callback, object asyncState)
        {
            Context = context;
            Callback = callback;
            _AsyncState = asyncState;
            AddTime = DateTime.Now; 
            Interlocked.Increment(ref GId);
            int userId = int.TryParse(Context.Request["UserId"], out userId) ? userId : 0;
            Id = userId;
            if (userId == 0)
            {
                Release(JUtil.ToJson(new {Code=-1,Msg="未提供UserId" }));
            }
        }
        public void Release(string msg)
        {
            try
            {
                try
                {
                    Context.Response.Write(msg);
                }
                catch { }
                if (Callback != null)
                {
                    Callback(this);
                }
            }
            catch { }
            finally
            {
                m_IsCompleted = true;
            }
        }
        public object AsyncState
        {
            get { return _AsyncState; }
        }

        public System.Threading.WaitHandle AsyncWaitHandle
        {
            get { return null; }
        }

        public bool CompletedSynchronously
        {
            get { return false; }
        }

        public bool IsCompleted
        {
            get { return m_IsCompleted; }
        }
    }
}
View Code
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Web;

namespace MangoPush.WebComet.Core
{
    public class CometRequestMgr
    {

        public static readonly CometRequestMgr Instance = new CometRequestMgr();
        private ConcurrentDictionary<int, PushAsyncResult> Queue = new ConcurrentDictionary<int, PushAsyncResult>();

        private CometRequestMgr() 
        {
            var timer = new System.Timers.Timer();
            timer.Interval = 1000 * 30;
            timer.AutoReset = false;
            timer.Elapsed += (s, e) =>
            {
         
                try
                {

                    var list = Queue.Select(ent => ent.Value).ToList();
                    #region 清理完成鏈接
                    foreach (var it in list)
                    {
                        if (it.IsCompleted)
                        {
                            try
                            {
                                PushAsyncResult c = null;
                                Queue.TryRemove(it.Id,out c);
                               
                            }
                            catch (Exception ex)
                            {
                              
                                continue;
                            }
                        }
                    }
                    #endregion

                }
                catch (Exception ex)
                {
                    
                }
                finally
                {
                    timer.Start();
                }

            };
            timer.Start();

        }
     
        public void Add(PushAsyncResult ar)
        {
            Queue[ar.Id] = ar;
        }
        public void BroadCastMsg(string msg)
        {
            msg +="," + DateTime.Now.ToString();
            foreach (var c in Queue)
            {
                try
                {
                   
                    c.Value.Release(JUtil.ToJson( new {Code=0,Msg= msg}));
                 
                }
                catch { }
                finally
                {
                    PushAsyncResult outIt=null;
                    Queue.TryRemove(c.Key,out outIt);
                }
            }
        }
    }
}
View Code

2.網頁端,使用ajax方式發起訪問,特別注意的是需要設置timeout,這樣如果斷線或者服務端掛了重啟,可以自動修復

    $(function () {

        function long_polling() {
            var _url = '/pushService.act?userId=' + $("#userId").val();
            console.log(_url);


            var ajaxRequest = $.ajax({
                url: _url,  //請求的URL
                timeout: 1000 * 60 * 3, //超時時間設置,單位毫秒
                type: 'get',  //請求方式,get或post
                data: {},  //請求所傳參數,json格式
                dataType: 'json', //返回的數據格式
                success: function (data) { //請求成功的回調函數

                    console.log(data);
                    if (data.Code == 0) {
                        $('#msg').append(data.Msg + "<br/>");
                    }
                },
                complete: function (XMLHttpRequest, status) { //請求完成后最終執行參數
                    if (status == 'timeout') {//超時,status還有success,error等值的情況
                        ajaxRequest.abort();
                        console.log("超時");
                    }

                    if ($("#btn").val() == "停止") {
                        long_polling();
                    }
                }
            });

        }
        $("#btn").click(function () {

            if ($("#btn").val() == "啟動") {
                long_polling();
                $("#btn").val("停止");
            } else {
                $("#btn").val("啟動");
            }
        });


        $("#btnCls").click(function () {
            $("#msg").text("");
        });

    });
View Code

3.Winform端,采用WebClient發起請求,並且使用AutoResetEvent控制超時重連(相當於心跳包)

 private void Do()
        {
            try
            {
                
                while (Enable)
                {
                    Console.WriteLine("發起請求!");
                    var url = @"http://192.168.9.6:9866/pushService.act?userId=18";
                    using (var wc = new WebClient())
                    {
                        wc.Encoding = Encoding.UTF8;
                        #region 回調處理
                        wc.DownloadStringCompleted += (s, e) => {
                      
                            if (e.Error != null)
                            {
                                Console.WriteLine(e.Error);
                            }
                            else if (e.Cancelled)
                            {
                                Console.WriteLine("Be Cancelled!");
                            }
                            else
                            {
                                Console.WriteLine(e.Result);
                                
                            }
                            autoReset.Set();
                        }; 
                       #endregion      
                        var uri = new Uri(url);
                        wc.DownloadStringAsync(uri);

                        var isOK= autoReset.WaitOne(1000 * 60 * 5);
                        if (!isOK)
                        {
                            wc.CancelAsync();
                        }
                        
                        
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("錯誤" + ex.Message);
            }
            finally
            {
                if (Enable)
                {
                    ThreadPool.QueueUserWorkItem(o => { Do(); }, null);
                }
            }
                
        }
View Code

4.Android端

1.類似WinForm端,目前采用AsyncHttpClient,寫法跟Js差不多,需要設置timeout

2.由於android會回收進程,需要AlarmManager,定期檢查推送服務是否還存活

3.android系統重啟,開關網絡,調整時間,需要訂閱相應廣播,調整AlermManager,觸發平率。

 

總結:

     整個能支持10K,100K,2000K鏈接的,挺不容易,但是一般中小企業使用1K,2K甚至0.1K足矣,3,4百行代碼就完事,至少可以讓員工不用連3G,4G了,NND,提速降價,也不知道降那去了。

最后整2個圖片


免責聲明!

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



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