純JS+MVC 打造Web實時聊天室


一、Web聊天室應用  

    要實現一個聊天系統,有很多方法可以用flash,html5里的WebSocket但是這兩種辦法因為要么要裝插件,要么瀏覽器不支持,所以目前用的比較多的還是用純js通過長輪詢來實現.就我所知目前WebQQ,還有新浪微薄右下角的聊天系統都是采用這一方法來實現.

   相比普通掃描式的輪詢,這種實現最主要的優點就是無用的請求會特別少我這里是2.5分鍾一次,也就是說沒有任何消息時每2.5分鍾請求一次,騰訊好像是1分鍾一次,這個看自己需求自己訂吧,時間長點服務器壓力小點,但穩定性差點,長期運行后自己去調節找一個好的平衡點吧.

輪詢

二、具體實現

  • 前端用Jquery ajax來實現
    View Code
     1         $(function () {
     2             window.polling = {
     3                 ///長連接地址
     4                 connectionUrl: "/channel/polling",
     5                 ///發送方式 
     6                 method: "POST",
     7                 ///事件載體
     8                 event_host: $("body"),
     9                 ///連接失敗時重連接時間
    10                 period: 1000 * 20,
    11                 ///連接超時時間
    12                 timeOut: 180 * 1000,
    13                 v: 0,
    14                 ///連接ID
    15                 id: "",
    16                 error_num: 0,
    17                 Reconnect: function () {
    18                     polling.v++;
    19                     $.ajax({
    20                         url: polling.connectionUrl,
    21                         type: polling.method,
    22                         data: { id: polling.id, v: polling.v },
    23                         dataType: "json",
    24                         timeout: polling.timeOut,
    25                         success: function (json) {
    26                             polling.id = json.id;
    27                             ///版本號相同才回發服務器
    28                             if (json.v == polling.v)
    29                                 polling.Reconnect();
    30                             ///無消息返回時不處理
    31                             if (json.result == "-1")
    32                                 return;
    33                             $.each(json.datas, function (i, ajaxData) {
    34                                 ajaxData.data.type = ajaxData.t;
    35                                 polling.event_host.triggerHandler("sys_msg", [ajaxData.data]);
    36                             });
    37                         }, ///出錯時重連
    38                         error: function () {
    39                             if (polling.error_num < 5) {
    40                                 setTimeout(polling.Reconnect, 1000 * 2);
    41                                 polling.error_num++;
    42                                 return;
    43                             }
    44                             ///20秒后重新連接
    45                             setTimeout(polling.Reconnect, polling.period);
    46                         }, ///釋放資源
    47                         complete: function (XHR, TS) { XHR = null }
    48                     });
    49                 }
    50             }
    51             polling.Reconnect();
    52             /*-----------------------------------------------------------------------------*/
    53             ///新消息事件訂閱
    54             $("body").bind("sys_msg", function (event, json) {
    55                 if (json.type != "1")
    56                     return;
    57                 $("#new_msg").append($("<p>" + json.content + "</p>"));
    58             });
    59             /*-----------------------------------------------------------------------------*/
    60             ///發送消息事件綁定
    61             $("#sendMsg").click(function () {
    62                 var self = $(this);
    63                 $.post("/home/addnewmsg", $("#msg").serialize(), null, "json")
    64             });
    65         });

     

 

注意:因為網絡經常會出現這樣那樣的問題所以保持連接時,如果連接時間超過自己設定的時間未響應,就應該要主動終結此次請求,而且每次請求都帶上一個序號以保證消息序列,對於序號不對的請求應該予以丟棄.這里后面事件采用的是一種事件訂閱的方式,方便每個不同頁面訂閱自己的事件而做出不同的處理方式

 

 

  根據消息類型不同,各自己處理自己想要消息

 

  • 后端用MVC來當服務器

控制器

 

View Code
 1  [SessionState(SessionStateBehavior.ReadOnly)]
 2     public class ChannelController : AsyncController
 3     {
 4 
 5         [HttpPost,AsyncTimeout(1000*60*4)]
 6         public void PollingAsync(int? id,int v)
 7         {
 8             AsyncManager.OutstandingOperations.Increment();
 9             AsyncManager.Parameters["Version"] = v;
10             PollingMannger.AddConnection(id, AsyncManager);
11         }
12 
13 
14         public ActionResult PollingCompleted()
15         {
16             try
17             {
18                 (AsyncManager.Parameters["time"] as Timer).Dispose();
19                 AsyncManager.Parameters["Finish"] = 1;
20                 var v = AsyncManager.Parameters["Version"];
21                 var id = AsyncManager.Parameters["id"];
22                 if (!AsyncManager.Parameters.ContainsKey("Datas"))
23                     return Json(new { result = "-1", v, id });
24                 var datas = AsyncManager.Parameters["Datas"] as List<PollingMannger.ClientData>;
25                 return Json(new { result = "-200", v, id, datas });
26             }
27             catch (Exception e)
28             {
29                 return Json(new { result = "-500" });
30             }
31         }
32     }

 

 注意:1.首先我們應該讓Controller繼承自AsyncController這樣才可以實現異步,提高服務器的吞吐量.如果是webform那就自己實現一下 IHttpAsyncHandler這個接口.

        2.這里讓我頭疼的問題就是因為這個長連接一直沒響應,所以導致其他所有請求阻塞着,最后找來找去發現原來是Session的原因所以要在Controller上加上一個標記(怎么不能加在Action上呢,很郁悶!),加了這個后你不能對Session有寫操作,不然會有異常的,請求阻塞的原因是Session上有個鎖造成的.

 

  連接管理輔助類

View Code
  1 /// <summary>
  2     /// 連接數據管理類
  3     /// </summary>
  4     public class PollingMannger
  5     {
  6 
  7         [Serializable]
  8         public class ClientData
  9         {
 10             #region 消息類型
 11 
 12 
 13             /// <summary>
 14             /// 新消息
 15             /// </summary>
 16             public const int MsgNewInformation = 1;
 17 
 18 
 19             #endregion
 20 
 21 
 22 
 23             /// <summary>
 24             /// 消息類型
 25             /// 傳送的數據
 26             /// </summary>
 27             /// <param name="type"></param>
 28             /// <param name="data"></param>
 29             public ClientData(int type, object data)
 30             {
 31                 this.t = type;
 32                 this.data = data;
 33             }
 34 
 35             /// <summary>
 36             /// 消息類型
 37             /// t=>Type
 38             /// </summary>
 39             public int t
 40             {
 41                 get;
 42                 private set;
 43             }
 44 
 45 
 46 
 47             /// <summary>
 48             /// 傳送數據
 49             /// data=>Data
 50             /// </summary>
 51             public object data
 52             {
 53                 get;
 54                 private set;
 55             }
 56 
 57 
 58         }
 59 
 60 
 61         /// <summary>
 62         /// 發送信息委托
 63         /// </summary>
 64         /// <param name="to"></param>
 65         /// <param name="data"></param>
 66         public delegate void SendMessage(ClientData data);
 67 
 68         /// <summary>
 69         /// 連接管理定時器
 70         /// </summary>
 71         static Timer ManngerTime;
 72 
 73         public static SendMessage Send = new SendMessage(SendTo);
 74 
 75         /// <summary>
 76         /// 在線用戶集合
 77         /// Dictionary 多線程出現高CPU問題
 78         /// 問題描述: http://blogs.msdn.com/b/tess/archive/2009/12/21/high-cpu-in-net-app-using-a-static-generic-dictionary.aspx
 79         /// </summary>
 80         //static Dictionary<int, PollingMannger> Online { get; set; }
 81         static Hashtable Online { get; set; }
 82         /// <summary>
 83         /// 連接自動超時時間
 84         /// </summary>
 85         static TimeSpan TimeOut = TimeSpan.FromSeconds(60 * 2.5);
 86 
 87         /// <summary>
 88         /// 最多連接數
 89         /// </summary>
 90         static int MaxConnection = 1000000;
 91 
 92         /// <summary>
 93         /// 連接ID隨機數
 94         /// </summary>
 95         static Random radm = new Random(1);
 96 
 97         /// <summary>
 98         /// 連接對象
 99         /// </summary>
100         /// <param name="connection"></param>
101         static void RemoveConnection(PollingMannger connection)
102         {
103             if (connection == null)
104                 return;
105             Online.Remove(connection.Id);
106             PollingMannger.SendTo(new ClientData(ClientData.MsgNewInformation, new { id = connection.Id }));
107         }
108 
109         /// <summary>
110         /// 將一個連接從集合中移除
111         /// </summary>
112         /// <param name="id">連接唯一標識ID</param>
113         public static void RemoveConnection(int id, string userId)
114         {
115             try
116             {
117                 if (Online.ContainsKey(id))
118                 {
119                     var connection = Online[id] as PollingMannger;
120                     RemoveConnection(connection);
121                 }
122             }
123             catch (Exception e)
124             {
125                 ///多線程同時操作時有可能會不存在
126             }
127         }
128 
129         /// <summary>
130         /// 添加或者激活一個新連接
131         /// </summary>
132         static public void AddConnection(int? id, AsyncManager asyncMannger)
133         {
134             if (id.HasValue && Online.ContainsKey(id.Value))
135             {
136                 (Online[id.Value] as PollingMannger).Active(asyncMannger);
137                 return;
138             }
139             PollingMannger newConnection = new PollingMannger(asyncMannger);
140             ///通知別人我上線了
141             PollingMannger.SendTo(new ClientData(ClientData.MsgNewInformation, new { id = newConnection.Id, content = newConnection.Id + "上線了!" }));
142         }
143 
144         /// <summary>
145         /// 異步發送消息
146         /// End
147         /// </summary>
148         /// <param name="asyncResult"></param>
149         public static void EndSend(IAsyncResult asyncResult)
150         {
151             try
152             {
153                 Send.EndInvoke(asyncResult);
154             }
155             catch (Exception e)
156             {
157 
158             }
159         }
160 
161 
162         /// <summary>
163         /// 給所有人發送信息
164         /// </summary>
165         /// <param name="data">接收的數據</param>
166         static void SendTo(ClientData data)
167         {
168             PollingMannger[] tempOnlines = new PollingMannger[Online.Values.Count + 10];
169             Online.Values.CopyTo(tempOnlines, 0);
170             var len = tempOnlines.Length;
171             for (int i = 0; i < len; i++)
172             {
173                 try
174                 {
175                     PollingMannger polling = tempOnlines[i];
176                     if (polling != null)
177                         polling.AddStack(data);
178                 }
179                 catch (Exception e)
180                 {
181                     break;
182                 }
183             }
184         }
185 
186         /// <summary>
187         /// 異步發送消息
188         /// Begin
189         /// </summary>
190         /// <param name="to"></param>
191         /// <param name="data"></param>
192         /// <param name="callBack"></param>
193         /// <param name="object"></param>
194         /// <returns></returns>
195         public static IAsyncResult BeginSend(ClientData data, AsyncCallback callBack, object @object)
196         {
197             try
198             {
199                 return Send.BeginInvoke(data, callBack, @object);
200             }
201             catch (Exception e)
202             {
203                 return null;
204             }
205         }
206 
207         /// <summary>
208         ///分配一個連接ID
209         /// </summary>
210         /// <returns></returns>
211         static int GetNewId()
212         {
213             var tempId = radm.Next(MaxConnection);
214             while (Online.ContainsKey(tempId))
215                 tempId = radm.Next(MaxConnection);
216             return tempId;
217         }
218 
219         /// <summary>
220         /// 用戶是否在線
221         /// </summary>
222         /// <param name="uid">要判斷的用戶</param>
223         /// <returns></returns>
224         public static bool IsOline(string uid)
225         {
226             PollingMannger[] tempOnlines = new PollingMannger[Online.Values.Count + 10];
227             Online.Values.CopyTo(tempOnlines, 0);
228             for (int i = 0; i < tempOnlines.Length; i++)
229             {
230                 try
231                 {
232                     var tempOnline = tempOnlines[i];
233                     if (tempOnline != null)
234                         return true;
235                 }
236                 catch (Exception e)
237                 {
238                     break;
239                 }
240             }
241             return false;
242         }
243 
244         /// <summary>
245         /// 靜態變量初始化
246         /// </summary>
247         static PollingMannger()
248         {
249             //Online = new Dictionary<int, PollingMannger>();
250             Online = new Hashtable();
251             ///連接最大過期時間(也就是超過這個時間就會被清除)
252             TimeSpan maxTimeOut = TimeSpan.FromMinutes(5);
253             ///每隔5分鍾進行一次連接清理
254             ManngerTime = new Timer(o =>
255             {
256                 PollingMannger[] pollings = new PollingMannger[Online.Values.Count + 10];
257                 Online.Values.CopyTo(pollings, 0);
258                 int len = pollings.Length;
259                 DateTime currentTime = DateTime.Now;
260                 for (int i = 0; i < len; i++)
261                 {
262                     try
263                     {
264                         var tempPolling = pollings[i];
265                         if (tempPolling == null)
266                             continue;
267                         ///移除長時沒有用的連接
268                         if ((currentTime - tempPolling.LastActiveTime).TotalMinutes > maxTimeOut.TotalMinutes)
269                         {
270                             RemoveConnection(tempPolling);
271                             ///如果這個連接還沒響應就先響應掉
272                             if (!tempPolling.AsyncMannger.Parameters.ContainsKey("Finish"))
273                                 tempPolling.AsyncMannger.Finish();
274                         }
275 
276                     }
277                     catch (Exception e)
278                     {
279 
280                     }
281                 }
282             }, null, maxTimeOut, TimeSpan.FromMinutes(10));
283         }
284 
285         public PollingMannger(AsyncManager asyncManager)
286         {
287             this.TaskQueue = new Queue<ClientData>();
288             this.LastActiveTime = DateTime.Now;
289             this.Id = GetNewId();
290             this.AsyncMannger = asyncManager;
291             asyncManager.Parameters["id"] = Id;
292             ///將自己添加入連接集合
293             Online.Add(Id, this);
294         }
295 
296         /// <summary>
297         /// 心跳激活
298         /// </summary>
299         /// <param name="asyncManager"></param>
300         public void Active(AsyncManager asyncManager)
301         {
302             asyncManager.Parameters["id"] = this.Id;
303             this.LastActiveTime = DateTime.Now;
304             this.AsyncMannger = asyncManager;
305             DequeueTask();
306         }
307 
308         /// <summary>
309         /// 任務隊列
310         /// </summary>
311         Queue<ClientData> TaskQueue { get; set; }
312 
313         /// <summary>
314         /// 最后激活時間
315         /// </summary>
316         public DateTime LastActiveTime { get; set; }
317 
318         /// <summary>
319         /// 連接唯一編號 
320         /// </summary>
321         public int Id { get; set; }
322 
323 
324         AsyncManager asyncMannger;
325 
326         /// <summary>
327         /// 當前連接的上下文
328         /// </summary>
329         public AsyncManager AsyncMannger
330         {
331             get { return asyncMannger; }
332             set
333             {
334                 var mySession = this;
335                 asyncMannger = value;
336                 var tempMannger = value;
337                 Timer tempTime = null;
338                 tempTime = new Timer(o =>
339                 {
340                     if (!tempMannger.Parameters.ContainsKey("Finish"))
341                         tempMannger.Finish();
342                 }, null, TimeOut, TimeSpan.FromSeconds(0));
343                 tempMannger.Parameters["time"] = tempTime;
344             }
345         }
346 
347 
348         /// <summary>
349         /// 添加一要運送的數據
350         /// 注意:要可序列化
351         /// </summary>
352         /// <param name="type">消息類型</param>
353         /// <param name="data">要傳送的數據</param>
354         void AddStack(ClientData clientData)
355         {
356             this.TaskQueue.Enqueue(clientData);
357             if (!this.asyncMannger.Parameters.ContainsKey("Finish"))
358                 DequeueTask();
359         }
360 
361         /// <summary>
362         ///完成隊列中的任務
363         /// </summary>
364         void DequeueTask()
365         {
366             if (this.TaskQueue.Count > 0)
367             {
368                 List<ClientData> datas = new List<ClientData>();
369                 while (this.TaskQueue.Count > 0)
370                     datas.Add(this.TaskQueue.Dequeue());
371                 this.asyncMannger.Parameters["Datas"] = datas;
372                 this.asyncMannger.Finish();
373             }
374         }
375 
376     }

 

 

  注意:這里用了一個隊列來存儲需要發送的消息,當每次請求回到服務器時先檢查隊列中有沒有要運送的消息,如果有就將消息發給瀏覽器,沒有就一直等待,直到超時時間到期時自動響應一個空消息給瀏覽器,瀏覽器再回發,形成一個循環.

 

效果

 

 


最后想跟大家探討一下多線程的鎖問題:

這里面一個靜態的連接管理對像 

我一開始用Dictionary因為我沒加鎖,所以在多線程同時調Add和ContainsKey(id)時出現高CPU現像(CPU 100%),

抓了個Dump后面找朋友用windbg幫忙分析一下,Dictionary的add里有個循環所以CPU 100%

代碼如下:

描述(http://blogs.msdn.com/b/tess/archive/2009/12/21/high-cpu-in-net-app-using-a-static-generic-dictionary.aspx),后來換成Hashtable好點目前還沒出現問題

因為多線程這里多線程同時操作這個集合時容易出現問題,加鎖的話又會降低效率,所想請各位高手指點一下,如何高效的操作這些集合,我這里沒有加鎖,復制一個副本出來遍歷,只是將其中出現的異常將其屏蔽掉了.

所以希望高手們指點指點,大家平常多線程操作集合時是怎么高效操作的.

demo打包下載:  點擊下載 

用2或者1個瀏覽器打2個開首就可以實現對聊.


免責聲明!

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



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