纯JS+MVC 打造Web实时聊天室
来源:互联网 发布:大数据分析技术 编辑:程序博客网 时间:2024/05/20 16:36
纯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 ///连接ID15 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 try17 {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 try116 {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 /// End147 /// </summary>148 /// <param name="asyncResult"></param>149 public static void EndSend(IAsyncResult asyncResult)150 {151 try152 {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 try174 {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 /// Begin189 /// </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 try198 {199 return Send.BeginInvoke(data, callBack, @object);200 }201 catch (Exception e)202 {203 return null;204 }205 }206 207 /// <summary>208 ///分配一个连接ID209 /// </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 try231 {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 try263 {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 AsyncMannger330 {331 get { return asyncMannger; }332 set333 {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个开首就可以实现对聊.
- 纯JS+MVC 打造Web实时聊天室
- 纯JS打造循环间隔滚动公告栏
- 纯css+js打造返回顶部代码
- 使用Node.js+Socket.IO搭建WebSocket实时应用(聊天室)
- Node.js websocket 使用 socket.io库实现实时聊天室
- Node.js websocket 使用 socket.io库实现实时聊天室
- Node.js websocket 使用 socket.io库实现实时聊天室
- Node.js + express + socket 实现在线实时多人聊天室
- 用Node.js编写多人实时在线聊天室
- 从零开始用node.js搭建web聊天室
- 用node.js搭建web聊天室
- 纯手工打造HTTPSERVER(02WEB JOB)
- Web聊天室
- Spring4 Web MVC纯注解启动,无web.xml
- [NodeJS]Node.js 打造实时多人游戏框架
- js聊天室
- node.js+socket.io 实现一个web聊天室
- 打造纯绿色软件
- spring 文件上传
- Server2008R2:由于没有远程桌面授权服务器可以提供许可证,.....错误的解决
- MySQL Replication, 主从和双主配置
- SQL中round()函数、Ucase()、Lcase()、as的用法
- 键盘遮挡
- 纯JS+MVC 打造Web实时聊天室
- 第二次计划
- 机房收费系统----状态图
- Leetcode -- The Skyline Problem
- django.forms-Widget和Media间的联系
- matlab linprog函数的使用
- android 签名和混淆打包
- C++文件读写
- @Autowired 注释与@Qualifier 注释