asp.net导步处理实战之类似QQ的简易网页聊天
自己四个月前曾初步研究了Asp.net导步处理模型并写了一遍学习总结:asp.net异步处理机制研究 ,由于一直没有应用的机会,不久就抛之脑后了。前天一朋友说需要实现一个类似QQ聊天的网页聊天工具,我立马就想到了它。经过几个小时的奋战,终于做出一个简易的聊天Demo,效果图如下:
左右两图代表单独打开的两个浏览器界面,当右面的用户选中一个在线用户,在输入框架填入信息并发送时,左侧的用户就能立马收到信息。
一.概要
1.前台
前台代码里最重要的函数当数wait如下:
function wait() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": $("#hdCurrentUserId").val() }, function (result) { if (result.Content) { $("#divContent").append("<div>" + result.Sender.Name + " 说:" + result.Content + "</div>") } if (result.List) { refreshOnlineMember(result.List, $("#hdCurrentUserId").val()); } wait(); }) }
一目了然,这是一个递归的函数,向服务器发送请求后就一直处于连接状态,当服务器回应后,跟据返回信息更新界面信息,最后再次调用自己。
2.后台
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { MessageAsyncResult result = new MessageAsyncResult(context, cb); if (string.IsNullOrEmpty(context.Request.Params["receiverId"])) { MessageCenter.Active(new Guid(context.Request.Params["senderId"]), result); } else { MessageCenter.SendMessage(new Guid(context.Request.Params["senderId"]), new Guid(context.Request.Params["receiverId"]), context.Request.Params["content"], result); } return result; } public void EndProcessRequest(IAsyncResult result) { MessageAsyncResult messageResult = result as MessageAsyncResult; messageResult.HttpContext.Response.ContentType = "application/json"; messageResult.HttpContext.Response.Write(JsonConvert.SerializeObject(new { Sender = messageResult.Sender, Content = messageResult.Content, List = MessageCenter.GetOnlineMember() })); }
后台代码严格按照异步模型进行编写,实现了IHttpAsyncHandler接口,在Begin函数里调用相关处理函数,在End函数里向客户端返回信息。
3.回调对象
public class MessageAsyncResult : IAsyncResult { //+静态部分 //静态内部类区 //静态字段区 //静态属性区 //静态构造函数区,按参数由少到多排列 //静态方法区 //+动态部分 //动态内部类区 //动态字段区 private bool _isComplete = false; private AsyncCallback _asyncCallback; //动态属性区 public Member Sender { get; private set; } public HttpContext HttpContext { get; set; } public string Content { get; set; } //动态构造函数区,按参数由少到多排列 public MessageAsyncResult(HttpContext context, AsyncCallback asyncCallback) { HttpContext = context; _asyncCallback = asyncCallback; } //动态方法区 //-本类方法区 public void SendMessage(Member sender, string content) { Sender = sender; Content = content; _isComplete = true; if (_asyncCallback != null) { _asyncCallback(this); } } //-重写基类方法区,从直接超类开始,层层追溯至Object //-重写接口方法区,按其出现的先后顺序,依次实现 #region IAsyncResult public object AsyncState { get { return null; } } public System.Threading.WaitHandle AsyncWaitHandle { get { return null; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get { return _isComplete; } } #endregion //+析构部分 //析构方法区 }
此函数编写毫无新意,起了两个作用:1作为中介者传递对象,如HttpContext, Member等,2调用asyncCallback委托触发结束事件。
4.处理程序
private static Dictionary<Guid, MessageAsyncResult> _members = new Dictionary<Guid, MessageAsyncResult>(); public static void Active(Guid id, MessageAsyncResult result) { DataSource.Members.Find(m => m.Id == id).LastActiveTime = DateTime.Now; DataSource.Members.Find(m => m.Id == id).IsLogin = true; _members[id] = result; } public static void SendMessage(Guid senderId, Guid receiverId, string message, MessageAsyncResult result) { DataSource.Members.Find(m => m.Id == senderId).LastActiveTime = DateTime.Now; DataSource.Members.Find(m => m.Id == senderId).IsLogin = true; if (_members.Keys.Contains(receiverId)) { _members[senderId].SendMessage(DataSource.Members.Find(m => m.Id == senderId), string.Empty); result.SendMessage(DataSource.Members.Find(m => m.Id == senderId), string.Empty); _members[receiverId].SendMessage(DataSource.Members.Find(m => m.Id == senderId), message); } }
这里有个_member对象,保存了用户与回调对象的键值对,另外还有两个方法,激活登录状态,发送信息。
二.思路与要点
要明白上面代码的含义与下面论述的意思,要求读者了解Asp.net的异步处理模型。
1.客户端限制
首先A用户登录,这里服务端就有个HttpApplication对象为其服务,并生成一个IAsyncResult对象,里面包括了一个AsyncCallback委托对象。我们简称HA1,RA1,CA1对象;B用户登录,又有一系列的对象为其服务,简称HB1,RB1,CB1对象。这时,B对象向A对象发送消息,当B对象点击发送按钮后,一个新的请求发送到服务端。注意,这是一个新的请求,所以会有一个新的HttpApplication对象为其服务,我称其为HB2对象,生成一个新的IAsyncResult对象,我称其为RB2对象,但是AsyncCallback委托对象却还是原来那一个。
然后通过缓存中的键值对找到之前A用户对应的HA1,RA1,CA1对象,在RA1里找到CA1对象,执行之。信息发回客户端,处理之后又会有一个新的请求自动发送过来,这时又会建立起新的为A用户服务的对象HA2,RA2,CA2。
通过上面的描述,可以发现服务器什么时候回应,是由什么时候有人给自己发信息决定的。
目前,B用户共有两组对象为其服务:HB1,RB1,CB1与HB2,RB2,CB1。现在就有两个选择:保持之,主动返回给客户端然后由客户端重新建立。我的第一个困惑来原由此。如果选择第一种方案,可以想像如果B一直向外发信息而从不接收信息,服务端就会有越来越多的HB*,RB*对象被其占用而不被释放。我编写代码时一开始考虑的也是第一种方案,就会发现一个奇怪的问题,B用户IE下连续发送9次或FF下连续发送5次信息后,服务端就再也不会接收B发来的信息了。之前一直找不到原因,后来一想,这会不会是客户端单域并发连接数的限制,因为如果服务端对象一直被占用而不返回,在客户端看来请求就没有完成。由于页面加载时会自动发起一次请求,那么算起来IE下就是10次而FF下就是6次了,这正好符合各自并发连接数的默认值。如下图:
可以看到,左图中当说过“5”之后,浏览器已经有6个未返回的连接了,达到了单域最大连接数的设定值。这时如果再点发送,浏览器与服务器都将没有响应。
2.改进后的通讯模型
这时就要选择上面所说的第二种方案。我现在再把思路梳理一遍。A用户建立连接,生成HA1,RA1,CA1对象;B用户建立连接,生成HB1,RB1,CB1对象;B用户向A用户发送信息,生成HB2,RB2,CB1对象;这时要做的事件有两件,第一件RA1对象被调用,CA1对象被执行,回调HA1对像的End方法向客户端发送信息,本次请求处理完成。A客户端接收信息作处理后,重新发起请求,生成HA2,RA2,CA2对象。第二件,将新建立的HB2,RB2,CB1对象执行掉,具体就是RB2对象被调用,CB1对象被执行,回调HB2对象的End方法向客户端发送信息,结束本次请求,将之前的HB1,RB1,CB1对象执行掉,具体就是RB1对象被调用,CB1对象被执行,回调HB1对象的End方法向客户端发送信息,B客户端接收信息作处理后,重新发起请求,生成HB3,RB3,CB2对象。如下图
上面演示了A,B先后登录,然后B连续发两次信息给A,一共有三次交互过程。弯曲的箭头表达了请求与响应的对应关系,黑色箭头表达了用户之间的触发关系。这样,所有用户与服务器在大部分时间内就只保持了一个连接。
上面的讲解也解答了在《基于ASP.NET的comet简单实现》一文中为什么要单独执行这句代码的原因。
asyncResult.Send(null);
来看一下在客户端的具体编程方式。
$(function () { $.post("LoginHandler.ashx", null, function (result) { if (result.User) { $("#spanCurrentUser").text(result.User.Name); $("#hdCurrentUserId").val(result.User.Id); refreshOnlineMember(result.List, result.User.Id); wait(); } }) }) function sendMessage() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": senderId, "receiverId": receiverId, "content": content }); } function wait() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": $("#hdCurrentUserId").val() }, function (result) { if (result.Content) { $("#divContent").append("<div>" + result.Sender.Name + " 说:" + result.Content + "</div>") } if (result.List) { refreshOnlineMember(result.List, $("#hdCurrentUserId").val()); } wait(); }) }
可以看到,登录成功后,会执行wait方法。这个方法最大的特点就是回调完成后自我调用重新发起新请求。页面加载完成后A,B用户会自动登录,成功后各自都已进入wait方法的等待回调。
B用户向A用户发信息,也就是在没有等到wait回调时发起新的请求,这时A用户的连接数是1个,B是2个,可以看到调用的是sendMessage方法。这个方法的最大特点是没有回调,也就是说当服务端返回后不会自动发起新请求。这样就明白了。A用户的唯一的连接由wait发起,B用户触发服务端返回A请求后,会自动发起新请求。B用户有两个连接,一个由wait方法发起,服务端返回后会重新建立请求,一个是sendMessage方法发起,服务端返回后就结束了。这样A,B用户在大部分的时间内与服务端的连接数都是1个。
3.遗留问题
(1).并发问题
如果B向A发了信息,服务端在已结束A请求与新的A请求之间收到新的发给A的信息,这时信息就会丢失,或者A所对应的回调对像已不存在,服务端发生异常。更稳妥的方式是让数据库完成大部份功能,而仅让这种异步编程模型完成消息的实时性,每次请求时都访问数据库有无最新信息。
(2).连接超时
显然,任何浏览器的连接都不是无限时等待的。这时应该在服务端应记录最后活跃时间并定时检查所有正在连接的请求,如果发现超出一定的时间,如1分钟,则主动将此返回给客户端并让其重新建立请求。
(3).下线
这个跟连接超时的原理差不多,当请求返回给客户端而客户端并未建立新的请求时,就可认为其已下线。
4.功能增强
这个基本就是向QQ看齐了,如群发,隐身等。在上面的基础上扩展起来不算太难
三.花絮
1.IIS7.5下扩展请求类型
《基于ASP.NET的comet简单实现》的范例在VS下可正常运行,但在IIS里却行不通,报404错误。原因是作者把处理请求的路径扩展名设为.asyn,而实际上不存在这个文件,web.conbfig里的httpHandlers配置节将其映射到自定义类的配置也未起到作用。在iis7.5的经典模式下,需要在IIS里的Handler Mappings模块里单独进行配置,让IIS将此扩展名的请求让asp.net来处理,而不是简单的返回一个404错误。具体来讲就是增加一个脚本映射,处理扩展名为.asyn,处理程序为aspnet_isapi.dll即可。
2.IIS7.5下挂载asp.net 4.0网站
IIS7.5默认情况下不一定能够处理4.0网站。在Handler Mappings模块里,如果没有4.0的处理程序,那么挂载4.0网站后,请求它时会返回500服务器内部错误。如果先安装的VS2010后打开的IIS可能就有此问题。解决的方法是手工注册IIS的4.0环境,在命令行里运行下面的命令即可
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -i
四.源代码
参考的文章