一、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來當服務器
控制器

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上有個鎖造成的.
連接管理輔助類

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個開首就可以實現對聊.