纯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来当服务器

控制器

 

 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             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上有个锁造成的.

 

  连接管理辅助类

 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             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个开首就可以实现对聊.

0 0
原创粉丝点击