一般需求推送服務時,都會去第三方拿推送組件,如”極光“,”百度“,”小米"什么的,自己用.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(); } } }

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; } } } }

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); } } } } }
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(""); }); });
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); } } }
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個圖片