Web网站数据”实时”更新设计
请注意这个实时打上了双引号,没有绝对的实时,只是时间的颗粒不一样罢了(1ms,1s,1m)。
服务器数据有更新可以快速通知客户端。Web 基于取得模式,而服务器建立大量的和客户端连接来提供数据实时更新反而拉低服务器的使用效能。
请下载DEMO 点击下载
一、现有方案归纳有两类。
-
服务器真实推送 - 基于浏览器外部控件数据实时更新。
IE ActiveX(flash)控件,还有其他浏览器比如Firefox插件。这种基于浏览器外部插件的,由于移植性差。主要要在一些浏览器安全上得到应用。比如在线支付(支付宝),自动登陆(QQ)。和一些内网控制(电网内部控制管理)。 Flash 其实也是浏览器的一种插件后台通过建立Socket 来与客户端实时数据更新。这点比较有优势的是Flash 插件几乎每台机器上都有安装,移植性没有问题。但同样对防火墙穿透能力差,而且需要消耗服务器大量的。
-
基于XMLHttpRequest 定时取的解决方案.
Ajax 通过定时去询问服务器是否有数据更新,似乎是一个通用的解决方案,如:1元xx 类的网站,因为抢购模式需要实现更新商品的剩余份数。比如要获取服务器当前参与人数,获取最新购买人数,发送的私信,好友消息,每一种类型数据都设定一个时间如:1秒到数据库取一次数据。而大多数请求的链接是无效的,而且过多的请求会导致浏览器无响应。
二、为什么我们不能两者结合,选择折中的方案呢?
客户端脚本需要一个可以告知的程序,告诉我们服务器中有数据更新了,然后执行自己注册好的程序到服务器取对应的数据。
我们只需要服务器通知提供这样的数据结构:
{ //用户消息更新时间 msg: '20141192003261234', //网站购买记录更新时间 buy: '20141192003534567' }
获取客户端获取当前的数据后 和自己当前浏览器中存储的用户消息更新时间进行对比。如果msg更新时间与服务器给的时间完全一致,我们就没有必要到服务器中去用户个人消息了,反之服务器中消息更新,开始执行我们预定的程序来获取服务器,用户收到的消息。显示给用户,并且设置一下当前消息的更新时间。
我们的需要的就这么简单——需要一个通知程序通知我们
三、我们需要做什么,会遇到那些问题。
让服务器通知客户端程序数据更新显然不是很划算的事情我们上面讨论过了,所以我们需要在客户端设置一个循环往复的定时程序到服务器里面去取注册好的类型(用户消息,网站成交数量,购买人次,商品剩余数量)的更新的时间,通过对于注册对于数据类型的时间来判断要不要执行我们注册好的方法。
问题1:定时程序必须有序执行Ajax方法(前一个ajax完毕后才能发送第二次Ajax),服务器获取数据是一个耗时操作。
Ajax异步递归。
问题2:并不是每一个页面都需要知道数据的变动情况
不同的页面注册不同的监听信息。
问题3:如何动态开始和阻止定时程序,并且保持程序只能运行一个定时实例。
设置互斥量.
问题4:服务器如何存储这些数据的更新状态(公共的:成交量,个体的:用户消息)。
需要一个存储介质,存储对应类型信息的更新时间
问题5:如何在异常中恢复。确保定时程序能正常运行。
Try ... Catch... $.ajax error.
四、代码实现
客户端定时程序:
//客户端监听对象 var listener = { tid: 0, keys: "", //任务存储对象 taskType: {}, //注册一个任务 appendTaskType: function (key, type) { if (typeof (type) == "function" && typeof (key) == "string" && /^[a-z0-9]+$/.test(key)) { //添加一个任务 this.taskType[key] = { //任务执行函数 fun: type, //变化量 ts: '' }; var a = []; for (var k in this.taskType) { a.push(k); } this.keys = a.join('.'); } }, //开始运行监听 start: function () { //如果定时器正在运行则返回 if (this.tid != 0) return; fn(); //私有定时执行方法 function fn() { $.getJSON("/api/listener", { keys: listener.keys }, function (d) { //获取定时器所有的注册类型 for (var key in listener.taskType) { //获取注册类型对象 var O = listener.taskType[key]; //判断当前对象是否存在和当前的值是否和之前的变化值一样,是否真正执行 if (d[key] && d[key] != O.ts) { //更改现有状态 O.ts = d[key]; //执行注册函数 O.fun(O); } } //设置ID listener.tid = setTimeout(fn, 1000); }) } }, //关闭监听 stop: function () { //清除定时器 clearTimeout(this.tid); //归零 this.tid = 0; } } //注册对页面的监听 listener.appendTaskType("msg", function () { $.getJSON("/api/getlist", { key: 'msg' }, function (data) { var b = $("#msgList"); b.hide(); b.empty(); var html = ''; for (var i = 0; i < data.length; i++) { html += "<li>【" + data[i].user + "】说:" + data[i].msg + "</li>" } b.html(html); b.slideDown(300); }) }); //注册对购买记录的监听 listener.appendTaskType("buy", function () { $.getJSON("/api/getlist", { key: 'buy' }, function (data) { var b = $("#buyList"); b.hide(); b.empty(); var html = ''; for (var i = 0; i < data.length; i++) { html += "<li>【" + data[i].user + "】购买了:" + data[i].msg + "</li>" } b.html(html); b.slideDown(300); }) }) //开始执行监听任务 listener.start();
服务器代码:
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Web; using System.Web.Mvc; namespace MvcApplication1.Controllers { public class ApiController : Controller { // // GET: /Api/ /// <summary> /// 根据缓存Key获取当前缓存最后更新的标示 /// </summary> /// <param name="cacheKey">缓存Key</param> /// <returns>最后更新的时间</returns> public string getlast(string cacheKey) { List<data> ls = HttpRuntime.Cache.Get(cacheKey) as List<data>; if (ls != null) return ls.First().time; return "0"; } /// <summary> /// 服务器定时方法,这个方法是检测,数据有没有进行更新 /// </summary> /// <returns></returns> public ContentResult Listener(string keys) { if (string.IsNullOrWhiteSpace(keys)) return Content("{}"); System.Text.StringBuilder builder = new System.Text.StringBuilder("{"); //客户端需要知道用户信息是否变动 if (keys.Contains("msg")) { builder.AppendFormat("\"msg\":\"{0}\",", getlast("msg")); } //客户端需要知道购买列表是否变动 if (keys.Contains("buy")) { builder.AppendFormat("\"buy\":\"{0}\",", getlast("buy")); } //类推各种监听....... //移除最后“,” if (builder.Length > 1) { builder.Remove(builder.Length - 1, 1); } builder.Append("}"); return Content(builder.ToString()); } public ContentResult msg(data ms) { InsertCache(ms, "msg"); return Content("ok"); } public ContentResult buy(data buy) { InsertCache(buy, "buy"); return Content("ok"); } private void InsertCache(data d, string cacheKey) { //使用时间设置最后更新量 d.time = DateTime.Now.ToString("yyyyMMddhhmmssffff"); //获取存储的值 List<data> ls = HttpRuntime.Cache.Get(cacheKey) as List<data>; //判断是否为空 if (ls == null) { ls = new List<data>(); HttpRuntime.Cache.Insert(cacheKey, ls); } //添加到集合 ls.Insert(0, d); //移除大于这个数 if (ls.Count > 10) ls.RemoveRange(10, ls.Count - 10); } public JsonResult GetList(string key) { if (string.IsNullOrEmpty(key)) return Json(null, JsonRequestBehavior.AllowGet); return Json(HttpRuntime.Cache.Get(key) as List<data>, JsonRequestBehavior.AllowGet); } public JsonResult GetCache() { return Json(new { p1 = HttpRuntime.Cache.EffectivePercentagePhysicalMemoryLimit, p2 = HttpRuntime.Cache.EffectivePrivateBytesLimit }, JsonRequestBehavior.AllowGet); } } //定义一个数据存储介质 public class data { public string user { get; set; } public string msg { get; set; } public string time { get; set; } } }
请下载DEMO 点击下载