UML软件工程组织

 

 

使用会话状态避免不必要的 Ajax 通信量
 
2008-01-02 作者:David Mertz 出处:IBM
 
本文内容包括:
在可行的情况下,以 REST 的方式创建 Web 应用程序 —— 包括基于 Ajax 的应用程序 —— 将避免大量 bug。然而,具象状态传输(Representational State Transfer,REST)的一个缺陷就是使用类似的 XMLHttpRequests 发送复制数据。本技巧将展示如何通过适度使用会话 cookie 将服务器端状态维持至最低水平,从而显著减少客户机-服务器通信量,同时仍然允许不使用 cookie 的操作。

引言

HTTP 是一种无状态协议,这个简单事实既是它的优点也是它的缺点。它意味着发往 HTTP 服务器资源的每个请求都是等幂 的,这就是说相同的请求在每次调用时应返回相同的结果。等幂是 REST 的基本思想:无论何时发起,相同的请求 — 可能是客户机信息的编码 — 应该返回相同的数据。

理论上讲,Ajax 应用程序通常比 REST 具有更加丰富的状态。Web 应用程序中的某个字段或区域将反映某些服务器数据的当前状态,同时客户机通过 JavaScript 轮询定期查询当前状态(可以使用多种方法使它更加倾向于推的方式,但是对本技巧来说并无必要)。然而,Web 应用程序多少都期望服务器能够跟踪在下次轮询事件需要了解的内容:客户机得到了哪些数据、没有得到哪些数据、已经进行了哪些交互,等等。

对 Ajax 应用程序使用 REST 技术的一个常见方法是为所有针对最新数据的查询安排一个全局惟一的 URI。例如,某个查询可能包含一个 UUID,使用 URL 编码的参数或隐藏的表单变量;比方说,一个 XMLHttpRequest 对象可能会对如下资源执行 GET 操作:

http://myserver.example.com?uuid=4b879324-8ec0-4120-bba6-890eb0aa3fc0

在下一次轮询事件中,即使只过了一秒的时间,也会打开一个不同的 URI。

处理复杂的等幂性

“相同数据” 的含义理解起来较为困难。在现实中,相同 URI 不可能总是返回一致的数据。毕竟,当对内容进行更正后,即使静态的 Web 页面也会发生变化(比如,修复已发表文章中的输入错误)。所谓等幂,就是指所涉及的修改不应该直接影响 GET 请求本身。因此,对持续变化的资源使用这种方式是一种非常合理的方法:

http://myserver.example.com/latest_data/

问题仅仅在于组成 “latest_data” 的内容不光指是否、以及何时由何人对数据进行检索。服务器可以很好地使用 REST,同时仍然可以反映 “事物的状态”。

获得最新数据

在开发一个 Web 应用程序时,我和我的同事 Miki Tebeka 就遇到了这种情况,这个应用程序频繁使用一个 JavaScript XMLHttpRequest() 对象从服务器轮询最新的数据。我在本文中提供的 Python 服务器示例来源于 Miki 自己创建的内部模块,但是在其基础上进行了简化和改进。

我们希望解决两个问题。其中之一是,如果自前一个请求发出以后内容没有发生任何改变,则避免发送任何实质性消息。第二个问题是避免过度使用数据库或计算性资源,防止生成重复数据。

事实上,尽管没有充分利用这种正确的解决方案,HTTP 协议中的 “Not Modified” 问题已经得以解决。我们可能而且应该做的仅仅是返回一个 HTTP 304 状态码。我们需要使用 Ajax 代码检查 304 状态,如果存在的话,那么也不要根据轮询发送的数据修改客户机应用程序状态。

服务器资源问题可以这样解决:将以前的数据缓存,然后将最新添加的数据集成进来。通常,这种解决方案只适用于 “最新数据” 由相对离散的数据项构成的情况,而不适合用于整个数据集互相依赖的情况。通过使用一个客户机 cookie,我们可以跟踪客户机会话的缓存状态。如清单 1 所示:

清单 1. 启用会话的服务器代码:server.cgi
 
from datetime import datetime
session = ClientSession()
old_stuff = session.get("data", [])   # Retrieve cached data
last_query = session.get("last", None)
prune_data(old_stuff, last_query)     # Age out really-old data
new_stuff = get_new_stuff()           # Look for brand-new data

if not new_stuff:
    print "Status: 304"               # "Not Modified" status
else
    print session.cookie              # New or existing cookie
    print "Content-Type: text/plain"
    print
    all_stuff = old_stuff + new_stuff
    session["data"] = all_stuff
    session["last"] = datetime.now().isoformat()
    print encode_data(all_stuff)      # XML, or JSON, or...
session.save()

ClientSession 类耍了一点小聪明,但仅此而已。基本上,我们仅需跟踪具有与缓存的 old_stuff 对应的 cookie 的所有客户机:

清单 2. 维护会话
 
from os import environ
from Cookie import SimpleCookie
from random import shuffle
from string import letters
from cPickle import load, dump

COOKIE_NAME = "my.server.process"

class ClientSession(dict):
    def __init__(self):
        self.cookie = SimpleCookie()
        self.cookie.load(environ.get("HTTP_COOKIE",""))

        if COOKIE_NAME not in cookie:
            # Real UUID would be better
            lets = list(letters)
            shuffle(lets)
            self.cookie[COOKIE_NAME] = "".join(lets[:15])

        self.id = self.cookie[COOKIE_NAME].value
        try:
            session = load(open("session."+self.id, "rb"))
            self.update(session)
        except:       # If nothing cached, just do not update
            pass

    def save(self):
        fh = open("session."+self.id, "wb")
        dump(self.copy(), fh, protocol=-1)  # Save the dictionary
        fh.close()

生成 Ajax 调用

缓存服务器就绪后,JavaScript 对数据进行轮询就变得十分简单了。我们所需做的就是执行清单 3 的代码:

清单 3. 轮询服务器获取最新数据
 
var r = new XMLHttpRequest();
r.onreadystatechange=function() {
    if (r.readyState==4) {
        if (r.status==200) {  // "OK status"
            displayData(r.responseText);
        }
        else if (r.status==304) {
            // "Not Modified": No change to display
        }
        else {
            alertProblem(r);
        }
    }
}
r.open("GET",'http://myserver.example.com/latest_data/',true)
r.send(null);

我们的示例较简单,因此没有明确说明 displayData() 和 alertProblem() 的实现。一般来说,前者需要以某种方式解析或处理接收到的响应;具体细节取决于是否使用了 JSON、XML 或其他格式发送数据以及实际应用程序的要求。

此外,本文的示例仅展示了如何进行单次轮询。在长期运行的应用程序中,您很可能需要在 setTimeout() 或 setInterval() 回调期间反复生成请求。或者,根据具体的应用程序,可能在发生特定的应用程序操作或事件后进行轮询。

结束语

本技巧提供了一些使用 Python 编写的服务器代码,但相同的设计基本上可用于所有编写 CGI 或其他服务器进程的语言。总体思想十分简单:使用客户机 cookie(如果可用的话)确定被缓存的数据,如果自上一次轮询事件以后没有产生新的数据,则发送一个 304 状态。无论使用什么样的服务器编程语言,您的程序应该大致相同。

虽然我没有过多地介绍错误捕捉(error catching),但是此处的设计非常健壮,可以返回到没有使用 cookie 的正确行为。如果客户机没有提供有关的会话 cookie — 可能因为它不支持 cookie 或者是新会话的第一次轮询 — old_stuff 则是一个空的列表,并且返回的所有数据将成为 new_stuff 的一部分。另外一个值得添加的功能是一条特殊的客户机消息,它将发送所有当前的会话状态:既可用于应用程序调试,也可用于清除不一致的状态,客户机应检查是否出错。刷新缓存时会损失一些服务器加载数据和一些带宽;这并不违背基本的等幂性。

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • Wikipedia 提供了优秀的 REST 原理介绍
  • developerWorks SOA 和 Web 服务专区 提供了关于会话状态和 REST 原理的信息。
  • Ajax 功能越来越多地被构建到 Web 应用程序中,您可能希望查看 developerWorks Ajax 资源中心,其中提供了大量工具、代码、培训和资源,可帮助您立即将 Ajax 构建到应用程序中。
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号