利用jquery Ajax和.Net IHttpAsyncHandler实现网站的即时提示
项目做完有一段时间了,一直想写个博客总结一下,之前也没写过有质量的博客.一是怕写出来被各位大牛笑话,二也是因为怕自己只了解了一点皮毛就发出来误导了别人,所以一直没怎么写过博客,但是看很多大牛都鼓励程序员写博客,一来可以回顾一下自己做的项目中的重点,二也可以发现很多自己以前没发现的问题.所以自己也试试写一下吧,一直没有总结的习惯,也想改改.文笔不好,经验欠缺,各位轻喷.
-----------------------------------------------------分割线-----------------------------------------------
因为项目的需要,主管要求我做一个登录后即时提醒的功能,即数据有变化的时候立即通知用户.然后我就开始百度,Google各种关键字搜索.最后知道有几种方式可以实现这种需求.即轮询和长连接.另外还有微软提供的一个开源的框架signalr(目前楼主本人就知道这些).
因为HTTP的无状态性,无连接性.导致web程序和服务器之间的数据传输只能是:浏览器向服务器发送一个请求,服务器再响应请求,然后返回要请求的数据.即浏览器和服务器的关系是请求--响应的关系,这种关系的好处就不说了(我也知道的不多 - -!),但是服务器却不能主动向浏览器发送数据,因为它是无状态的.那如果有这种需求了怎么办呢?聪明的人有很多,聪明人想出来解决的办法也挺多.前人栽树后人乘凉,咱们就先开始试试哪种方案最适合项目需求的.
1.signalr
园子里的已经有过介绍signalr的文章:SignalR 项目介绍 是张善友老师写的
我是通过在 Asp.NET MVC 中使用 SignalR 实现推送功能这篇文章了解到具体的使用方法,没有深入点的研究,它适用于做web即时聊天方面的.
楼主的项目则是要实现类似监视数据库的功能,所以不考虑这个方法,有兴趣的朋友可以去了解一下.
2.轮询
所谓轮询就是客户端不停的向服务器发送异步的请求,当发现数据库有变化时再通知浏览器做处理.这种方法实现起来简单,但是想想也知道,由于是不停的向服务器发送请求,对服务器来说是压力山大,要是同时打开的网页太多了话,有可能造成服务器崩溃.
3.长连接
前两种方法都不是LZ想要的,看来LZ就只能祭出那一招了:长连接.
楼主是百度GOOGLE党,就摘一段网友的话来解释长连接:客户端向服务器发送一个请求,服务器接收请求并hlod住这个连接,直到有数据或请求超时才返回客户端,客户端紧接着再发送一次请求,如此循环直到页面关闭,这也解释了为什么它叫长连接.比如这张图:
这张图的前两个请求超时我都设置为1分钟,返回后再立即发送一个请求.
好了,既然只剩下最后一招了,那就的把最后一招耍好,
首先是客户端要发送一个异步的请求:
/*客户端发出的异步请求*/ function asyncRequest() { $.ajax({ type: "POST", url: "asyncResult.asyn", data: "time=60", //请求的超时时间 success: function (data) { if (data != "") { /*执行操作,比如弹出提示*/ } asyncRequest(); //得到服务器响应后继续发一个请求 }, error: function () { asyncRequest(); //服务器抛出错误后继续发送一个请求 } }); }
服务器接收这个异步请求的方法也要实现异步操作,要不然会阻塞正常的请求,所以要实现IHttpAsyncHandler这个接口,实现服务器的异步计算.
public class asyncResponse : IHttpAsyncHandler { public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { myAsyncResult result = new myAsyncResult(context, cb, extraData); asyncRequestMgr.add(result); asyncRequestMgr.send(); return result; } public void EndProcessRequest(IAsyncResult result) { asyncRequestMgr.resultStr = ""; //异步结束时清空结果 } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { } }
asyncResponse类用来接收所有的异步请求,并交给静态类asyncRequestMgr来根据请求计算结果:
public static class asyncRequestMgr { public static string resultStr = ""; private static myAsyncResult asyncResult; /// <summary> /// 把一个异步的请求对象保存到静态对象中供操作 /// </summary> /// <param name="result"></param> public static void add(myAsyncResult result) { asyncResult = result; } /// <summary> /// /// </summary> public static void send() { string time = asyncResult.contex.Request.Form["time"]; getResult(time); asyncResult.send(resultStr); //发送数据到客户端 } /// <summary> /// 得到结果或返回空值 /// </summary> private static void getResult(string time) { int i = int.Parse(time), temp = 0; while (temp < i) { Thread.Sleep(1000); //这个类继承自IHttpAsyncHandler,是由线程池中取出一个线程来执行本类,所以这里让线程Sleep(1000)不会影响到UI线程 /* *这里再查询数据库,得到数据后保存至变量resultStr,再break出循环, */ temp++;
} } }
然后由myAsyncResult类来发送结果:
public class myAsyncResult : IAsyncResult { public HttpContext contex; public AsyncCallback cb; public object extraData; /// <summary> /// 初始化数据 /// </summary> /// <param name="contex"></param> /// <param name="cb"></param> /// <param name="extraData"></param> public myAsyncResult(HttpContext contex, AsyncCallback cb, object extraData) { this.contex = contex; this.cb = cb; this.extraData = extraData; } /// <summary> /// 返回客户端请求的数据 /// </summary> public void send(string resultStr) { this.contex.Response.Write(resultStr); } }
这样一个异步请求就算完成了,也实现了监视数据库的目的,但是如果客户不小心在后台查询数据库的时候按了刷新怎么办呢?这样建立起来的连接就会断开,而且由于我的前台是页面加载的时候开始异步请求,那一刷新一下又会再发送一次请求,而后台第一次的查询还在继续.这样后台就会有两次请求一起执行,一起查询数据库.再如果数据库的变化被第一次的请求查询到,但是第一次的请求因为客户刷新页面,连接已经断开,那用户也就不能得到数据变化的通知了.再再如果用户不小心无(手)意(贱)一直按着F5不放,那前台就会一直刷新一直请求,后台的N个请求同时查数据库.再再再如果有10个用户同时按F5不放,那就是10*N个请求同时查数据库,最后服务器只能不堪重负崩溃掉,如果这样怎么办呢?由于LZ平时MSDN看的少,确实苦恼了一阵子,最后突然发现HttpContext.Response有个属性:IsClientConnected,这个属性帮了大忙了,它返回一个BOOL值,表示当前请求是否在连接状态。有了这个属性就好办了,在getResult方法中加上判断,如果IsClientConnected==false的话,立即抛出一个异常,再把查询的结果保存到resultStr变量中,这样线程就不会继续执行下去.
修改后的getResult方法:
/// <summary> /// 得到结果或返回空值 /// </summary> private static void getResult(string time) { int i = int.Parse(time), temp = 0; try {
while (temp < i)
{
if (!asyncResult.contex.Response.IsClientConnected) throw new Exception(); Thread.Sleep(1000); //这个类继承自IHttpAsyncHandler,是由线程池中取出一个线程来执行本类,所以这里让线程Sleep(1000)不会影响到UI线程 /* *这里再查询数据库,得到数据后保存至变量resultStr,再break出循环, */
temp++; } } catch (Exception) { /*这里把异常的线程中的结果保存至resultStr中*/ throw; } }
然后在send方法执行前判断resultStr是不是空的,如果不是空的就不用查询数据库,直接发送resultStr:
/// <summary> /// /// </summary> public static void send() { if (resultStr == "") { string time = asyncResult.contex.Request.Form["time"]; getResult(time); } asyncResult.send(resultStr); //发送数据到客户端 }
这样无论按多久的F5,只要服务器判断哪个请求的连接状态为false就抛出异常,保持最多只让一个请求来查询数据库,现在就算再怎么无()意()按F5也不怕啦!
----------------------------------------分割线-------------------------------------
第一次发自认为是技术贴的帖子,如果大家觉得我哪里理解有误请及时指出来,避免误导他人.