FreeZone

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  4 随笔 :: 0 文章 :: 2 评论 :: 0 Trackbacks

2005年10月30日 #

在 Web 应用程序开发中,页面重载循环是最大的一个使用障碍,对于 Java™ 开发人员来说也是一个严峻的挑战。在这个系列中,作者 Philip McCarthy 介绍了一种创建动态应用程序体验的开创性方式。Ajax(异步 JavaScript 和 XML)是一种编程技术,它允许为基于 Java 的 Web 应用程序把 Java 技术、XML 和 JavaScript 组合起来,从而打破页面重载的范式。

Ajax(即异步 JavaScript 和 XML)是一种 Web 应用程序开发的手段,它采用客户端脚本与 Web 服务器交换数据。所以,不必采用会中断交互的完整页面刷新,就可以动态地更新 Web 页面。使用 Ajax,可以创建更加丰富、更加动态的 Web 应用程序用户界面,其即时性与可用性甚至能够接近本机桌面应用程序。

Ajax 不是一项技术,而更像是一个 模式 —— 一种识别和描述有用的设计技术的方式。Ajax 是新颖的,因为许多开发人员才刚刚开始知道它,但是所有实现 Ajax 应用程序的组件都已经存在若干年了。它目前受到重视是因为在 2004 和 2005 年出现了一些基于 Ajax 技术的非常棒的动态 Web UI,最著名的就是 Google 的 GMail 和 Maps 应用程序,以及照片共享站点 Flickr。这些用户界面具有足够的开创性,有些开发人员称之为“Web 2.0”,因此对 Ajax 应用程序的兴趣飞速上升。

在这个 系列中,将提供使用 Ajax 开发应用程序需要的全部工具 。在第一篇文章中,将解释 Ajax 背后的概念,演示为基于 Java 的 Web 应用程序创建 Ajax 界面的基本步骤。我将使用代码示例演示让 Ajax 应用程序如此动态的服务器端 Java 代码和客户端 JavaScript。最后,将指出 Ajax 方式的一些不足,以及在创建 Ajax 应用程序时应当考虑的一些更广的可用性和访问性问题。

更好的购物车

可 以用 Ajax 增强传统的 Web 应用程序,通过消除页面装入从而简化交互。为了演示这一点,采用一个简单的购物车示例,在向里面添加项目时,它会动态更新。这项技术如果整合到在线商 店,那么用户可以持续地浏览和向购物车中添加项目,而不必在每次点击之后都等候完整的页面更新。虽然这篇文章中的有些代码特定于购物车示例,但是演示的技 术可以应用于任何 Ajax 应用程序。清单 1 显示了购物车示例使用的有关 HTML 代码,整篇文章中都会使用这个 HTML。


清单1. 购物车示例的有关片断

<!-- Table of products from store's catalog, one row per item -->
<th>Name</th> <th>Description</th> <th>Price</th> <th></th>
...
<tr>
<!-- Item details -->
<td>Hat</td> <td>Stylish bowler hat</td> <td>$19.99</td>
<td>
<!-- Click button to add item to cart via Ajax request -->
<button onclick="addToCart('hat001')">Add to Cart</button>
</td>
</tr>
...

<!-- Representation of shopping cart, updated asynchronously -->
<ul id="cart-contents">

<!-- List-items will be added here for each item in the cart -->

</ul>

<!-- Total cost of items in cart displayed inside span element -->
Total cost: <span id="total">$0.00</span>





Ajax 往返过程

Ajax 交互开始于叫作 XMLHttpRequest 的 JavaScript 对象。顾名思义,它允许客户端脚本执行 HTTP 请求,并解析 XML 服务器响应。Ajax 往返过程的第一步是创建 XMLHttpRequest 的实例。在 XMLHttpRequest 对象上设置请求使用的 HTTP 方法(GETPOST)以及目标 URL。

现在,您还记得 Ajax 的第一个 a 是代表 异步(asynchronous) 吗?在发送 HTTP 请求时,不想让浏览器挂着等候服务器响应。相反,您想让浏览器继续对用户与页面的交互进行响应,并在服务器响应到达时再进行处理。为了实现这个要求,可以在 XMLHttpRequest 上注册一个回调函数,然后异步地分派 XMLHttpRequest。然后控制就会返回浏览器,当服务器响应到达时,会调用回调函数。

在 Java Web 服务器上,请求同其他 HttpServletRequest 一样到达。在解析了请求参数之后,servlet 调用必要的应用程序逻辑,把响应序列化成 XML,并把 XML 写入 HttpServletResponse

回到客户端时,现在调用注册在 XMLHttpRequest 上的回调函数,处理服务器返回的 XML 文档。最后,根据服务器返回的数据,用 JavaScript 操纵页面的 HTML DOM,把用户界面更新。图 1 是 Ajax 往返过程的顺序图。


图 1. Ajax 往返过程
 Ajax 往返过程的顺序图

现在您对 Ajax 往返过程有了一个高层面的认识。下面我将放大其中的每一步骤,进行更详细的观察。如果过程中迷了路,请回头看图 1 —— 由于 Ajax 方式的异步性质,所以顺序并非十分简单。





分派 XMLHttpRequest

我将从 Ajax 序列的起点开始:创建和分派来自浏览器的 XMLHttpRequest。不幸的是,不同的浏览器创建 XMLHttpRequest 的方法各不相同。清单 2 的 JavaScript 函数消除了这些依赖于浏览器的技巧,它可以检测当前浏览器要使用的正确方式,并返回一个可以使用的 XMLHttpRequest。最好是把它当作辅助代码:只要把它拷贝到 JavaScript 库,并在需要 XMLHttpRequest 的时候使用它就可以了。


清单 2. 创建跨浏览器的 XMLHttpRequest

/*
* Returns a new XMLHttpRequest object, or false if this browser
* doesn't support it
*/
function newXMLHttpRequest() {

var xmlreq = false;

if (window.XMLHttpRequest) {

// Create XMLHttpRequest object in non-Microsoft browsers
xmlreq = new XMLHttpRequest();

} else if (window.ActiveXObject) {

// Create XMLHttpRequest via MS ActiveX
try {
// Try to create XMLHttpRequest in later versions
// of Internet Explorer

xmlreq = new ActiveXObject("Msxml2.XMLHTTP");

} catch (e1) {

// Failed to create required ActiveXObject

try {
// Try version supported by older versions
// of Internet Explorer

xmlreq = new ActiveXObject("Microsoft.XMLHTTP");

} catch (e2) {

// Unable to create an XMLHttpRequest with ActiveX
}
}
}

return xmlreq;
}

稍后将讨论处理那些不支持 XMLHttpRequest 的浏览器的技术。目前,示例假设清单 2 的 newXMLHttpRequest 函数总能返回 XMLHttpRequest 实例。

返回示例的购物车场景,想要当用户在目录项目上点击 Add to Cart 时启动 Ajax 交互。名为 addToCart()onclick 处理函数负责通过 Ajax 调用来更新购物车的状态(请参阅 清单 1)。正如清单 3 所示,addToCart() 需要做的第一件事是通过调用清单 2 的 newXMLHttpRequest() 函数得到 XMLHttpRequest 对象。接下来,它注册一个回调函数,用来接收服务器响应(我稍后再详细解释这一步;请参阅 清单 6)。

因为请求会修改服务器上的状态,所以将用 HTTP POST 做这个工作。通过 POST 发送数据要求三个步骤。第一,需要打开与要通信的服务器资源的 POST 连接 —— 在这个示例中,服务器资源是一个映射到 URL cart.do 的 servlet。然后,我在 XMLHttpRequest 上设置一个头,指明请求的内容是表单 编码的数据。最后,用表单编码的数据作为请求体发送请求。

清单 3 把这些步骤放在了一起。


清单 3. 分派 Add to Cart XMLHttpRequest

/*
* Adds an item, identified by its product code, to the shopping cart
* itemCode - product code of the item to add.
*/
function addToCart(itemCode) {

// Obtain an XMLHttpRequest instance
var req = newXMLHttpRequest();

// Set the handler function to receive callback notifications
// from the request object
var handlerFunction = getReadyStateHandler(req, updateCart);
req.onreadystatechange = handlerFunction;

// Open an HTTP POST connection to the shopping cart servlet.
// Third parameter specifies request is asynchronous.
req.open("POST", "cart.do", true);

// Specify that the body of the request contains form data
req.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");

// Send form encoded data stating that I want to add the
// specified item to the cart.
req.send("action=add&item="+itemCode);
}

这就是建立 Ajax 往返过程的第一部分,即创建和分派来自客户机的 HTTP 请求。接下来是用来处理请求的 Java servlet 代码。





servlet 请求处理

用 servlet 处理 XMLHttpRequest,与处理普通的浏览器 HTTP 请求一样。可以用 HttpServletRequest.getParameter() 得到在 POST 请求体中发送的表单编码数据。Ajax 请求被放进与来自应用程序的常规 Web 请求一样的 HttpSession 中。对于示例购物车场景来说,这很有用,因为这可以把购物车状态封装在 JavaBean 中,并在请求之间在会话中维持这个状态。

清单 4 是处理 Ajax 请求、更新购物车的简单 servlet 的一部分。Cart bean 是从用户会话中获得的,并根据请求参数更新它的状态。然后 Cart 被序列化成 XML,XML 又被写入 ServletResponse。重要的是把响应的内容类型设置为 application/xml,否则 XMLHttpRequest 不会把响应内容解析成 XML DOM。


清单 4. 处理 Ajax 请求的 servlet 代码

public void doPost(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {

Cart cart = getCartFromSession(req);

String action = req.getParameter("action");
String item = req.getParameter("item");

if ((action != null)&&(item != null)) {

// Add or remove items from the Cart
if ("add".equals(action)) {
cart.addItem(item);

} else if ("remove".equals(action)) {
cart.removeItems(item);

}
}

// Serialize the Cart's state to XML
String cartXml = cart.toXml();

// Write XML to response.
res.setContentType("application/xml");
res.getWriter().write(cartXml);
}

清单 5 显示了 Cart.toXml() 方法生成的示例 XML。它很简单。请注意 cart 元素的 generated 属性,它是 System.currentTimeMillis() 生成的一个时间戳。


清单 5. Cart 对象的XML 序列化示例

<?xml version="1.0"?>
<cart generated="1123969988414" total="$171.95">
<item code="hat001">
<name>Hat</name>
<quantity>2</quantity>
</item>
<item code="cha001">
<name>Chair</name>
<quantity>1</quantity>
</item>
<item code="dog001">
<name>Dog</name>
<quantity>1</quantity>
</item>
</cart>

如果查看应用程序源代码(可以从 下载 一节得到)中的 Cart.java,可以看到生成 XML 的方式只是把字符串添加在一起。虽然对这个示例来说足够了,但是对于从 Java 代码生成 XML 来说则是最差的方式。这个系列的下一期中介绍一些更好的方式。

现在您已经知道了 CartServlet 响应 XMLHttpRequest 的方式。下一件事就是返回客户端,查看如何用 XML 响应更新页面状态。





用 JavaScript 进行响应处理

XMLHttpRequestreadyState 属性是一个数值,它指出请求生命周期的状态。它从 0(代表“未初始化”)变化到 4(代表“完成”)。每次 readyState 变化时,readystatechange 事件就触发,由 onreadystatechange 属性指定的事件处理函数就被调用。

清单 3 中已经看到了如何调用 getReadyStateHandler() 函数创建事件处理函数。然后把这个事件处理函数分配给 onreadystatechange 属性。getReadyStateHandler() 利用了这样一个事实:函数是 JavaScript 中的一级对象。这意味着函数可以是其他函数的参数,也可以创建和返回其他函数。getReadyStateHandler() 的工作是返回一个函数,检查 XMLHttpRequest 是否已经完成,并把 XML 响应传递给调用者指定的事件处理函数。清单 6 是 getReadyStateHandler() 的代码。


清单 6. getReadyStateHandler() 函数

/*
* Returns a function that waits for the specified XMLHttpRequest
* to complete, then passes its XML response to the given handler function.
* req - The XMLHttpRequest whose state is changing
* responseXmlHandler - Function to pass the XML response to
*/
function getReadyStateHandler(req, responseXmlHandler) {

// Return an anonymous function that listens to the
// XMLHttpRequest instance
return function () {

// If the request's status is "complete"
if (req.readyState == 4) {

// Check that a successful server response was received
if (req.status == 200) {

// Pass the XML payload of the response to the
// handler function
responseXmlHandler(req.responseXML);

} else {

// An HTTP problem has occurred
alert("HTTP error: "+req.status);
}
}
}
}

HTTP 状态码

在清单 6 中,检查 XMLHttpRequeststatus 属性以查看请求是否成功完成。status 包含服务器响应的 HTTP 状态码。在执行简单的 GETPOST 请求时,可以假设任何大于 200 (OK)的码都是错误。如果服务器发送重定向响应(例如 301 或 302),浏览器会透明地进行重定向并从新的位置获取资源;XMLHttpRequest 看不到重定向状态码。而且,浏览器会自动添加 Cache-Control: no-cache 头到所有 XMLHttpRequest,这样客户代码永远也不用处理 304(未经修改)服务器响应。

关于 getReadyStateHandler()

getReadyStateHandler() 是段相对复杂的代码,特别是如果您不习惯阅读 JavaScript 的话。但是通过把这个函数放在 JavaScript 库中,就可以处理 Ajax 服务器响应,而不必处理 XMLHttpRequest 的内部细节。重要的是要理解如何在自己的代码中使用 getReadyStateHandler()

清单 3 中看到了 getReadyStateHandler() 像这样被调用:handlerFunction = getReadyStateHandler(req, updateCart)。在这个示例中,getReadyStateHandler() 返回的函数将检查在 req 变量中的 XMLHttpRequest 是否已经完成,然后用响应的 XML 调用名为 updateCart 的函数。

提取购物车数据

清单 7 是 updateCart() 本身的代码。函数用 DOM 调用检查购物车的 XML 文档,然后更新 Web 页面(请参阅 清单 1),反映新的购物车内容。这里的重点是用来从 XML DOM 提取数据的调用。cart 元素的 generated 属性是在 Cart 序列化为 XML 时生成的一个时间戳,检查它可以保证新的购物车数据不会被旧的数据覆盖。Ajax 请求天生是异步的,所以这个检查可以处理服务器响应未按次序到达的情况。


清单 7. 更新页面,反映购物车的 XML 文档

function updateCart(cartXML) {

// Get the root "cart" element from the document
var cart = cartXML.getElementsByTagName("cart")[0];

// Check that a more recent cart document hasn't been processed
// already
var generated = cart.getAttribute("generated");
if (generated > lastCartUpdate) {
lastCartUpdate = generated;

// Clear the HTML list used to display the cart contents
var contents = document.getElementById("cart-contents");
contents.innerHTML = "";

// Loop over the items in the cart
var items = cart.getElementsByTagName("item");
for (var I = 0 ; I < items.length ; I++) {

var item = items[I];

// Extract the text nodes from the name and quantity elements
var name = item.getElementsByTagName("name")[0]
.firstChild.nodeValue;

var quantity = item.getElementsByTagName("quantity")[0]
.firstChild.nodeValue;

// Create and add a list item HTML element for this cart item
var li = document.createElement("li");
li.appendChild(document.createTextNode(name+" x "+quantity));
contents.appendChild(li);
}
}

// Update the cart's total using the value from the cart document
document.getElementById("total").innerHTML =
cart.getAttribute("total");
}

到此,整个 Ajax 往返过程完成了,但是您可能想让 Web 应用程序运行一下查看实际效果(请参阅 下载 一节)。这个示例非常简单,有很多需要改进之处。例如,我包含了从购物车中清除项目的服务器端代码,但是无法从 UI 访问它。作为一个好的练习,请试着在应用程序现有的 JavaScript 代码之上构建出能够实现这个功能的代码。





使用 Ajax 的挑战

就像任何技术一样,使用 Ajax 也有许多出错的可能性。我目前在这里讨论的问题还缺乏容易的解决方案,但是会随着 Ajax 的成熟而改进。随着开发人员社区增加开发 Ajax 应用程序的经验,将会记录下最佳实践和指南。

XMLHttpRequest 的可用性

Ajax 开发人员面临的一个最大问题是:在没有 XMLHttpRequest 可用时该如何响应?虽然主要的现代浏览器都支持 XMLHttpRequest,但仍然有少数用户的浏览器不支持,或者浏览器的安全设置阻止使用 XMLHttpRequest。如果开发的 Web 应用程序要部署在企业内部网,那么可能拥有指定支持哪种浏览器的权力,从而可以认为 XMLHttpRequest 总能使用。但是,如果要部署在公共 Web 上,那么就必须当心,如果假设 XMLHttpRequest 可用,那么就可能会阻止那些使用旧的浏览器、残疾人专用浏览器和手持设备上的轻量级浏览器的用户使用您的应用程序。

所以,您应当努力让应用程序“平稳降级”,在没有 XMLHttpRequest 支持的浏览器中也能够工作。在购物车的示例中,把应用程序降级的最好方式可能是让 Add to Cart 按钮执行一个常规的表单提交,刷新页面来反映购物车更新后的状态。Ajax 的行为应当在页面装入的时候就通过 JavaScript 添加到页面,只有在 XMLHttpRequest 可用时才把 JavaScript 事件处理函数附加到每个 Add to Cart 按钮。另一种方式是在用户登录时检测 XMLHttpRequest 是否可用,然后相应地提供应用程序的 Ajax 版本或基于表单的普通版本。

可用性考虑

关于 Ajax 应用程序的某些可用性问题比较普遍。例如,让用户知道他们的输入已经注册了可能是重要的,因为沙漏光标和 spinning 浏览器的常用反馈机制“throbber”对 XMLHttpRequest 不适用。一种技术是用“Now updating...”类型的信息替换 Submit 按钮,这样用户在等候响应期间就不会反复单击按钮了。

另 一个问题是,用户可能没有注意到他们正在查看的页面的某一部分已经更新了。可以使用不同的可视技术,把用户的眼球带到页面的更新区域,从而缓解这个问题。 由 Ajax 更新页面造成的其他问题还包括:“破坏了”浏览器的后退按钮,地址栏中的 URL 也无法反映页面的整个状态,妨碍了设置书签。请参阅 参考资料 一节,获得专门解决 Ajax 应用程序可用性问题的文章。

服务器负载

用 Ajax 实现代替普通的基于表单的 UI,会大大提高对服务器发出的请求数量。例如,一个普通的 Google Web 搜索对服务器只有一个请求,是在用户提交搜索表单时出现的。而 Google Suggest 试图自动完成搜索术语,它要在用户输入时向服务器发送多个请求。在开发 Ajax 应用程序时,要注意将要发送给服务器的请求数量以及由此造成的服务器负荷。降低服务器负载的办法是,在客户机上对请求进行缓冲并且缓存服务器响应(如果可 能的话)。还应该尝试将 Ajax Web 应用程序设计为在客户机上执行尽可能多的逻辑,而不必联络服务器。

处理异步

非常重要的是,要理解无法保证 XMLHttpRequest 会按照分派它们的顺序完成。实际上,应当假设它们不会按顺序完成,并且在设计应用程序时把这一点记在心上。在购物车的示例中,使用最后更新的时间戳来确保新的购物车数据不会被旧的数据覆盖(请参阅 清单 7)。这个非常基本的方式可以用于购物车场景,但是可能不适合其他场景。所以在设计时请考虑如何处理异步的服务器响应。

posted @ 2005-10-30 15:36 bluesky 阅读(275) | 评论 (0)编辑 收藏

如果您正在使用异步 JavaScript 和 XML(Ajax)进行 Java™ Web 开发,那么您最关心的问题可能就是把数据从服务器传递给客户机。在 面向 Java 开发人员的 Ajax 系列的第二篇文章中,Philip McCarthy 介绍了 Java 对象序列化的五种方式,并提供了选择最适合应用程序的数据格式和技术所需要的全部信息。

在这个系列的 第一篇文章 中,我介绍了 Ajax 的构造块:

  • 如何用 JavaScript XMLHttpRequest 对象从 Web 页面向服务器发送异步请求。
  • 如何用 Java servlet 处理和响应请求(向客户机返回 XML 文档)。
  • 如何在客户端用响应文档更新页面视图。

这一次,将继续讨论 Ajax 开发的基础知识,但是将侧重于许多 Java Web 开发人员最关心的问题:为客户机生成数据。

多 数 Java 开发人员已经把模型-视图-控制器(MVC)模式应用在他们的 Web 应用程序上。在传统的 Web 应用程序中,视图组件由 JSP 或者其他表示技术(例如 Velocity 模板)构成。这些表示组件动态地生成全新的 HTML 页面,替代用户以前正在查看的页面,从而更新用户界面。但是,在 Java Web 应用程序使用 Ajax UI 的情况下,基于从 XMLHttpRequest 的响应接收到的数据,JavaScript 客户端代码对于更新用户看到的内容负有最终责任。从服务器的角度来看,视图成为它响应客户机请求而发送的数据表示。

这 篇文章侧重于可以用来生成 Java 对象以数据为中心的视图的技术。我将演示可以把 JavaBeans 变成 XML 文档的各种方法,并且讨论每种方法的优劣。您将看到为什么 XML 并不总是最好的途径:对于简单的 Ajax 请求来说,传输纯文本更好。最后,我将介绍 JavaScript 对象标注(JSON)。JSON 允许数据以序列化的 JavaScript 对象图的形式传输,在客户端代码中处理序列化的 JavaScript 对象图极为容易。

关于示例

    使用一个示例应用程序和几个用例来演示这里讨论的技术特性和技术。图 1 显示的极为简单的数据模型可以表示示例用例。这个模型代表在线商店中的顾客帐户。顾客拥有以前订单的集合,每个订单包含几个商品。


图 1. 简单的对象模型
代表顾客帐户的对象模型

虽然 XMLHttpRequest 对于发送数据使用的格式没有做任何限制,但是对于多数目的来说,只发送传统的表单数据是适合的,所以我的讨论集中在服务器的响应上。响应也可以有基于文本的格式,但是正如它的名字表示的,XMLHttpRequest 具有内置的处理 XML 响应数据的能力。这使 XML 成为 Ajax 响应的默认选择,所以我们从 XML 格式开始讨论。





从 Java 类产生 XML

把 Ajax 响应作为 XML 来传递有许多原因:每个支持 Ajax 的浏览器都有导航 XML 文档的方法,也有许多服务器端技术可以处理 XML 数据。通过制定一个方案,描述要交换的文档类型,在 Ajax 客户端和服务器端之间很容易定义合约,而且如果服务器端架构采用面向服务的方式,那么使用 XML 也可以允许非 Ajax 客户机使用您提供的数据。

    我们从 Java 对象产生 XML 数据的三种方法,并讨论每种方法的优劣。





自行进行序列化

首先,可以从对象图以编程的方式生成 XML。这种方式可以简单到只是在每个 JavaBean 类中实现 toXml() 方法即可。然后就可以选择合适的 XML API,让每个 bean 提供表示自己状态的元素,并递归地对自己的成员调用对象图。显然,这种方式无法扩展到大量的类,因为每个类都需要专门编写自己的 XML 生成代码。从好的方面来看,这是一个实现起来简单的方式,没有额外的配置支出或者更复杂的构建过程支出,任何 JavaBean 图都可以只用几个调用就变成 XML 文档。

     前一篇文章 的示例代码中,把 XML 标记字符串连接在一起,实现了 toXml() 方法。上次我就提到过,这是个糟糕的方法,因为它把确保标记配对、实体编码等工作的负担放在每个 toXml() 方法的代码中。在 Java 平台上有几个 XML API 可以替您做这些工作,这样您就可以把精力集中在 XML 的内容上。清单 1 用 JDOM API 实现了在线商店示例中表示订单的类中的 toXml()(请参阅 图 1)。


清单 1. Order 类的 toXml() 的 JDOM 实现

public Element toXml() {

Element elOrder = new Element("order");
elOrder.setAttribute("id",id);

elOrder.setAttribute("cost",getFormattedCost());

Element elDate = new Element("date").addContent(date);
elOrder.addContent(elDate);

Element elItems = new Element("items");
for (Iterator iter =
items.iterator() ; iter.hasNext() ; ) {
elItems.addContent(iter.next().toXml());
}
elOrder.addContent(elItems);

return elOrder;
}

在这里可以看到用 JDOM 创建元素、使用属性和添加元素内容有多么简单。递归地调用复合 JavaBean 的 toXml() 方法是为了取得它们子图的 Element 表示。例如,items 元素的内容是通过调用 Order 聚合的每个 Item 对象上的 toXml() 得到的。

一旦所有的 JavaBean 都实现了 toXml() 方法,那么把任意对象图序列化成 XML 文档并返回给 Ajax 客户机就简单了,如清单 2 所示。


清单 2. 从 JDOM 元素生成 XML 响应

public void doGet(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException, ServletException {

String custId = req.getParameter("username");
Customer customer = getCustomer(custId);

Element responseElem = customer.toXml();
Document responseDoc = new Document(responseElem);

res.setContentType("application/xml");
new XMLOutputter().output(responseDoc,res.getWriter());
}

JDOM 再次把工作变得非常简单。只需要在对象图返回的 XML 元素外面包装一个 Document,然后用 XMLOutputter 把文档写入 servlet 响应即可。清单 3 显示了用这种方式生成的 XML 示例,用 JDOM Format.getPrettyFormat()XMLOutputter 进行初始化,格式化得非常好。在这个示例中,顾客只做了一个订单,包含两个商品。


清单 3. 代表顾客的 XML 文档



James Hyrax


08-26-2005


Oolong 512MB CF Card
512 Megabyte Type 1 CompactFlash card.
Manufactured by Oolong Industries

$49.99


Fujak Superpix72 Camera
7.2 Megapixel digital camera featuring six
shooting modes and 3x optical zoom. Silver.

$299.99






自行序列化的不足

有趣的是,清单 3 中的代码展示了让 JavaBean 把自己序列化为 XML 的一个主要不足。假设要用这个文档表示顾客的订单历史视图。在这种情况下,不太可能要显示每个历史订单中每个商品的完整说明,或者告诉顾客他或她自己的姓名。但是如果应用程序有一个 ProductSearch 类,它就是以 Item bean 列表的形式返回搜索结果,那么在 Item 的 XML 表示中包含说明可能会有帮助。而且,Item 类上代表当前库存水平的额外字段,在产品搜索视图中可能就是需要显示的有用信息。但是,不管当前的库存水平是否与当前情况相关(比如对顾客的订单历史来说),这个字段都会从包含 Item 的任何对象图中序列化出来。

从 设计的角度来看,这是数据模型与视图生成耦合的经典问题。每个 bean 只能用一种途径序列化自己,一成不变的方式意味着 Ajax 交互最终要交换它们不需要交换的数据,因此造成客户端代码要从文档中找到需要的信息更加困难,而且也会增加带宽消耗和客户端的 XML 解析时间。这种耦合的另一个后果就是 XML 的语法不能脱离 Java 类独立变化。例如,对顾客文档的方案做修改,可能会影响多个 Java 类,造成它们也不得不做修改和重新编译。

稍后会解决这些问题,但是首先来看一个对自行序列化方式的可伸缩性问题的解决方案:XML 绑定框架。





XML 绑定框架

近 些年来,已经开发了多个 Java API 来简化 XML 文档到 Java 对象图的绑定过程。多数都提供了 XML 编排和拆解;也就是说,它们可以在 Java 对象图和 XML 之间执行双向会话。这些框架封装了 XML 处理的全部工作,这意味着应用程序代码只需要处理普通的 Java 类。它们还希望提供有用的辅助功能,例如文档验证。笼统来说,这些框架采用了两种不同的方式:代码生成和对象到 XML 映射。我将分别解释这两种方式。

代码生成方式

使 用代码生成的框架包括 XMLBeans、JAXB、Zeus 和 JBind。Castor 也能使用这项技术。这类框架的起点是描述文档数据类型的 XML 方案。使用框架提供的工具,就可以生成代表这些方案定义类型的 Java 类。最后,用这些生成的类编写应用程序,表示自己的模型数据,并通过框架提供的一些辅助机制把数据序列化成 XML。

如果应用程序要使用大 型 XML 语法,那么代码生成方式是个很好的方法。在数十个类上编写定制 XML 序列化代码的可伸缩性问题由此消除。另一方面,也不再需要定义自己的 JavaBean。框架生成的 Java 类通常非常符合 XML 的结构,所以对它们进行编码很难。而且,生成的类变成哑数据容器,因为一般不能向它们添加行为。一般来说,在应用程序代码中要做些妥协,才能很好地处理方 案生成的类型。另一个缺陷是如果修改方案,会造成生成的类也要修改,所以也就会对围绕它们编写的代码带来相应的影响。

这种类型的 XML 绑定框架在数据拆解时最有用(例如,使用 XML 文档并把它们转化成 Java 对象)。除非拥有大型数据模型而且有可能从生成的类中获益,否则基于代码生成的框架对于 Ajax 应用程序来说可能有很大的杀伤力。

映射方式

采 用映射方式的框架包括 Castor 和 Apache Commons Betwixt。映射通常是比代码生成更灵活和更轻量的解决方案。首先,可以像通常一样编写 JavaBean,包括任何行为以及任何自己喜欢的方便的方法。然后,在运行时,调用框架中基于内省的编排器,并根据对象成员的类型、名称和值生成 XML 文档。通过定义类的映射文件,可以覆盖默认的绑定策略,并就类在 XML 中的表示方式对编排器提出建议。

这种方法是在可伸缩性与 灵活性之间的良好折中。可以按照自己喜欢的方式编写 Java 类,编排器负责处理 XML。虽然映射定义文件编写起来简单,可伸缩性也足够好,但是映射规则最多只能改变标准的绑定行为,而且在对象结构和它们的 XML 表示之间总要残留一些耦合。最终,可能不得不在 Java 表示或 XML 格式之间任选一个做些折中,才能让映射方法起作用。

数据绑定总结

Dennis Sosnoski 就 XML 数据绑定 API 的主题,在代码生成和代码映射两个方面写了深入的文章。如果想进一步研究这个领域,我推荐他在 Castor 和代码生成框架方面的精彩文章(请参阅 参考资料 中的链接)。

总之,代码生成方式损失了过多的灵活性和方便性,对于典型的 Ajax 应用程序用处不大。另一方面,基于映射的框架可能工作得很好,但是要恰到好处地调整它们的映射策略,以便从对象生成需要的 XML。

所 有的 XML 绑定 API 都具有手工序列化技术的一个主要不足:模型和视图的耦合。被限制为一个类型一个 XML 表示,就意味着在网络上总要有冗余数据传输。更严重的问题是,在情况要求客户端代码使用专门视图时,客户端代码却无法得到它,所以可能要费力地处理给定对 象图的一成不变的视图。

在传统的 Web 应用程序开发中,采用页面模板系统把视图生成与控制器逻辑和模型数据干净地分离。这种方法在 Ajax 场景中也会有帮助。





页面模板系统

任何通用目的的页面模板技术都可以用来生成 XML,从而使 Ajax 应用程序根据自己的数据模型生成任何 XML 响应文档。额外收获是:模板可以用简单的、表现力强的标记语言编写,而不是用一行行的 Java 代码编写。清单 5 是一个 JSP 页面,采用了 Customer bean 并表示出定制的 XML 视图,适合客户端代码生成订单历史组件。


清单 4. 生成订单历史文档的 JSP


<%@ page contentType="application/xml" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>





${order.date}




${item.formattedPrice}







这个简洁的模板只输出订单历史视图需要的数据,不输出不相关的资料(例如商品说明)。创建产品搜索视图的定制 XML 应当同样简单,这个视图包含每个商品的完整说明和库存水平。

模板的问题

另 一方面,现在我需要为每个不同视图创建一个新 JSP,而不能仅仅把需要的对象图组织起来并序列化它。从设计的角度来说,许多人可能会有争议,认为这无论如何是件好事,因为这意味着正式地考虑服务器要 生成的文档类型。而且,因为我现在要处理通用的模板环境,而不是特定于 XML 的 API,所以确保标记匹配、元素和属性的顺序正确以及 XML 实体(例如 <&)正确转义就成了我的责任。JSP 的核心 out 标记使后面这项工作变得很容易,但是不是所有的模板技术都提供了这样的机制。最后,没有方便的途径可以在服务器端根据方案检验生成的 XML 文档的正确性,但这毕竟不是要在生产环境中做的事,可以方便地在开发期间处理它。





不用 XML 的响应数据

迄 今为止,我介绍的所有技术都用 XML 文档的形式生成服务器响应。但是,XML 有一些问题。其中一个就是延迟。浏览器不能立即解析 XML 文档并生成 DOM 模型,所以这会降低某些 Ajax 组件需要的“迅捷”感,特别是在较慢的机器上解析大型文档的时候更是如此。“现场搜索”就是一个示例,在这种搜索中,当用户输入搜索术语时,就会从服务器 提取搜索结果并显示给用户。对于现场搜索组件来说,迅速地响应输入是非常重要的,但是同时它还需要迅速而持续地解析服务器的响应。

延迟是一个重要的考虑因素,但是避免使用 XML 的最大原因是差劲的客户端 DOM API。清单 5 显示了使用跨浏览器兼容的方式通过 DOM 得到某个值的时候,通常不得不面对的困难。


清单 5. 在 JavaScript 中导航 XML 响应文档

// Find name of first item in customer's last order
var orderHistoryDoc = req.responseXML;

var orders = orderHistoryDoc.getElementsByTagName("order");
var lastOrder = orders[orders.length - 1];

var firstItem = lastOrder.getElementsByTagName("item")[0];
var itemNameElement = firstItem.firstChild;

var itemNameText = itemNameElement.firstChild.data;

当元素中间存在空白时,情况就变得更加复杂,因为每个元素的 firstChild 经常是个空白文本节点。现在有 JavaScript 库可以缓解处理 XML 文档的麻烦。这些库包括 Sarissa (请参阅 参考资料)和 Google-ajaXSLT,这两个库都把 XPath 功能添加到了大多数浏览器中。

但是,想想替代方案还是值得的。除了 responseXML 之外,XMLHttpRequest 对象还提供了名为 responseText 的属性,这个属性只是以字符串的方式提供服务器的响应体。

responseText 属性

当服务器需要向客户机发送非常简单的值时,responseText 特别方便,它可以避免 XML 导致的带宽支出和处理支出。例如,简单的 true/false 响应可以由服务器以纯文本方式返回,可以是逗号分隔的简单的名称或数字列表。但是,一般来说,最好不要在同一个应用程序中把 XML 响应和纯文本响应混合使用;保持单一数据格式可以让代码抽象和重用更加简单。

responseText 与 XML 响应数据结合时也会有用。在只需要从响应文档中提取单一值的场景中,“欺骗性”地把 XML 当作文本字符串,而不把它当作结构化的文档对待,会更方便。例如,清单 6 显示了如何用正则表达式从顾客的订单历史中提取第一笔订单的日期。不过,这实际是种花招,一般不应当依赖 XML 文档的词汇表达。


清单 6. 用正则表达式处理 XMLHttpRequest 的 responseText 对象

var orderHistoryText = req.responseText;
var matches = orderHistoryText.match(/(.*?)<\/date>/);

var date = matches[1];

在某些情况下,采用即时方式使用 responseText 会比较方便。但是,理想情况下,应当有种途径,可以用一种能够让 JavaScript 轻松导航、却没有 XML 处理支出的格式表示复杂的结构化数据。幸运的是,确实存在这样一种格式。





JavaScript 对象标注

实 际上,JavaScript 对象的大部分都由联合数组、数字索引数组、字符串、数字或者这些类型的嵌套组合而成。因为所有类型都可以用 JavaScript 直接声明,所以可以在一条语句中静态地定义对象图。清单 7 使用 JSON 语法声明了一个对象,并演示了如何访问这个对象。大括号表示联合数组(即对象),它的键 -值组合由逗号分隔。方括号表示数字索引数组。


清单 7. 用 JSON 在 JavaScript 中直接声明一个简单对象

var band = {
name: "The Beatles",
members: [
{
name: "John",
instruments: ["Vocals","Guitar","Piano"]
},
{
name: "Paul",
instruments: ["Vocals","Bass","Piano","Guitar"]
},
{
name: "George",
instruments: ["Guitar","Vocals"]
},
{
name: "Ringo",
instruments: ["Drums","Vocals"]
}
]
};

// Interrogate the band object
var musician = band.members[3];
alert( musician.name
+ " played " + musician.instruments[0]
+ " with " + band.name );

既然 JSON 是一个有趣的语言特性,那么它对 Ajax 有什么意义呢?妙处在于可以用 JSON 在 Ajax 服务器响应中通过网络发送 JavaScript 对象图。这意味着在客户端可以避免使用笨拙的 DOM API 对 XML 进行导航 —— 只需要分析 JSON 响应,就会立即得到可以访问的 JavaScript 对象图。但是,首先需要把 JavaBean 变成 JSON。

从 Java 类产生 JSON

不同 XML 生成技术所具有的优缺点也适用于 JSON 的产生。而且可以证明,存在需要再次使用表示模板技术的情况。但是,使用 JSON 在理念上更接近于在应用层之间传递序列化的对象,而不是创建应用程序状态的视图。我将介绍如何用 org.json 这个 Java API 在 Java 类上创建 toJSONObject() 方法。然后,就可以把 JSONObject 简单地序列化成 JSON。清单 8 反映了 清单 1 讨论的 XML,显示了 Order 类的 toJSONObject() 实现。


清单 8. Order 类的 toJSONObject() 方法实现

public JSONObject toJSONObject() {

JSONObject json = new JSONObject();
json.put("id",id);
json.put("cost",getFormattedCost());
json.put("date",date);

JSONArray jsonItems = new JSONArray();
for (Iterator iter =
items.iterator() ; iter.hasNext() ; ) {
jsonItems.put(iter.next().toJSONObject());
}
json.put("items",jsonItems);

return json;
}

可以看到,org.json API 非常简单。 JSONObject 代表 JavaScript 对象(即联合数组),有不同的 put() 方法,方法接受的 String 键和值是原生类型、String 类型或其他 JSON 类型。JSONArray 代表索引数组,所以它的 put() 方法只接受一个值。请注意在清单 8 中,创建 jsonItems 数组,然后再用 put() 把它附加到 json 对象上;可以用另外一种方法做这项工作,就是对每个项目调用 json.accumulate("items",iter.next().toJSONObject());accumulate() 方法与 put() 类似,区别在于它把值添加到按照键进行识别的索引数组。

清单 9 显示了如何序列化 JSONObject 并把它写入 servlet 响应。


清单 9. 从 JSONObject 生成序列化的 JSON 响应

public void doGet(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException, ServletException {

String custId = req.getParameter("username");
Customer customer = getCustomer(custId);

res.setContentType("application/x-json");
res.getWriter().print(customer.toJSONObject());
}

可以看到,它实际上什么也没有做。在这里隐式调用的 JSONObjecttoString() 方法做了所有工作。请注意,application/x-json 内容类型还有一点不确定 —— 在编写这篇文章的时候,关于 JSON 应当属于什么 MIME 类型还没有定论。但是,目前 application/x-json 是合理的选择。清单 10 显示了这个 servlet 代码的示例响应。


清单 10. Customer bean 的 JSON 表示

{
"orders": [
{
"items": [
{
"price": "$49.99",
"description": "512 Megabyte Type 1 CompactFlash card.
Manufactured by Oolong Industries",
"name": "Oolong 512MB CF Card",
"id": "i-55768"
},
{
"price": "$299.99",
"description": "7.2 Megapixel digital camera featuring six
shooting modes and 3x optical zoom. Silver.",
"name": "Fujak Superpix72 Camera",
"id": "i-74491"
}
],
"date": "08-26-2005",
"cost": "$349.98",
"id": "o-11123"
}
],
"realname": "James Hyrax",
"username": "jimmy66"
}

在客户端使用 JSON

处理的最后一步是把在客户端把 JSON 数据变成 JavaScript 对象。这可以通过对 eval() 的简单调用实现,这个函数可以即时地解释包含 JavaScript 表达式的字符串。清单 11 把 JSON 响应转变成 JavaScript 对象图,然后执行清单 5 的任务,从顾客的最后一次订单中得到第一个商品的名称。


清单 11. 评估 JSON 响应

var jsonExpression = "(" + req.responseText + ")";
var customer = eval(jsonExpression);

// Find name of first item in customer's last order
var lastOrder = customer.orders[customer.orders.length-1];
var name = lastOrder.items[0].name;

比较清单 11 和 清单 5 可以发现使用 JSON 的客户端的优势。如果在 Ajax 项目中要在客户端对许多复杂的服务器响应进行导航,那么 JSON 可能适合您的需要。JSON 和 XMLHttpRequest 结合还会让 Ajax 交互看起来更像 RPC 调用而不是 SOA 请求,这对应用程序的设计可能会有意义。在下一篇文章中,我要研究的框架,就是明确地为了让 JavaScript 代码对服务器端对象进行远程方法调用而设计的。

JSON 的不足

JSON 也有它的不足。使用这里介绍的 JSON 方式,就没有办法针对每个请求对对象的序列化进行裁剪,所以不需要的字段可能经常会在网络上发送。另外,添加 toJSONObject() 方法到每个 JavaBean,可伸缩性不太好,虽然用内省和标注编写一个通用的 JavaBean 到 JSON 的序列化器可能很简单。最后,如果服务器端代码是面向服务的,没有单独针对处理 Ajax 客户请求调整过,那么由于对 XML 一致的支持,XML 会是更好的选择。





比较序列化技术

现 在已经看到了把 Java 状态传输到 Ajax 客户端的五种不同技术。我讨论了自行手工编码 XML 序列化、通过代码生成的 XML 绑定、通过映射机制的 XML 绑定、基于模板的 XML 生成以及手工编码到 JSON 的序列化。每种技术都有自己的优势和不足,分别适用于不同的应用程序架构。

为了总结每种方式的优势与不足,表 1 从六个方面进行了粗略的评分:

可伸缩性
描述技术适应大量数据类型的容易程度。对于每个附加类型,编码和配置工作量是否会增长?
易于集成
评估把技术集成到项目的简单程度。是否需要更加复杂的构建过程?是否增加了部署的复杂性?
Java 类 API
描述以指定方式处理服务器端 Java 对象的容易程度。是可以编写普通的 bean,还是不得不处理笨拙的文档表示?
对输出的控制
描述对类的序列化表示控制的精确程度。
视图灵活性
评估从同一组对象是否可以创建不同的、定制的数据序列化。
客户端数据访问
描述 JavaScript 代码处理服务器响应数据的难易程度。
表 1. 数据生成技术的相对价值

自行编写 XML通过代码生成的 XML 绑定通过映射的 XML 绑定页面模板 XML手工编码的 JSON 序列化
可伸缩性一般一般
易于集成一般一般
Java 类 API
对输出的控制一般
视图灵活性
客户端数据访问一般




结束语

表 1 中的数据并不表明某项序列化技术比其他的技术好。毕竟,六种标准的相对重要性取决于项目的具体情况。例如,如果要处理数百种数据类型,这时想要的是可伸缩 性,那么代码生成可能就是最好的选择。如果需要为同一数据模型生成多个不同视图,那么就应当使用页面模板。如果处理的是小规模项目,想降低需要编写的 JavaScript 代码数量,那么请考虑 JSON。

希望这篇文章为您提供了选择适合自己应用程序的序列化技术所需要的信息。

posted @ 2005-10-30 15:32 bluesky 阅读(399) | 评论 (0)编辑 收藏

1.前言

  Internet的高速发展,给人们的工作和生活带来了极大的便利,对Internet的服务品质和访问速度要求越来越高,虽然带宽不断增加, 用户数量也在不断增加,受Web服务器的负荷和传输距离等因数的影响,响应速度慢还是经常抱怨和困扰。解决方案就是在网络传输上利用缓存技术使得Web服 务数据流能就近访问,是优化网络数据传输非常有效的技术,从而获得高速的体验和品质保证。

  网络缓存技术,其目的就是减少网络中冗余数据的重复传输,使之最小化,将广域传输转为本地或就近访问。互联网上传递的内容,大部分为重复的 Web/FTP数据,Cache服务器及应用Caching技术的网络设备,可大大优化数据链路性能,消除数据峰值访问造成的结点设备阻塞。Cache服 务器具有缓存功能,所以大部分网页对象(Web page object),如html, htm, php等页面文件,gif,tif,png,bmp等图片文件,以及其他格式的文件,在有效期(TTL)内,对于重复的访问,不必从原始网站重新传送文件 实体, 只需通过简单的认证(Freshness Validation)- 传送几十字节的Header,即可将本地的副本直接传送给访问者。由于缓存服务器通常部署在靠近用户端,所以能获得近似局域网的响应速度,并有效减少广域 带宽的消耗。据统计,Internet上超过80%的用户重复访问20%的信息资源,给缓存技术的应用提供了先决的条件。缓存服务器的体系结构与Web服 务器不同,缓存服务器能比Web服务器获得更高的性能,缓存服务器不仅能提高响应速度,节约带宽,对于加速Web服务器,有效减轻源服务器的负荷是非常有 效的。

  高速缓存服务器(Cache Server)是软硬件高度集成的专业功能服务器,主要做高速缓存加速服务,一般部署在网络边缘。根据加速对象不同,分为客户端加速和服务器加速,客户端 加速Cache部署在网络出口处,把常访问的内容缓存在本地,提高响应速度和节约带宽;服务器加速,Cache部署在服务器前端,作为Web服务器的前置 机,提高Web服务器的性能,加速访问速度。如果多台Cache加速服务器且分布在不同地域,需要通过有效地机制管理Cache网络,引导用户就近访问, 全局负载均衡流量,这就是CDN内容传输网络的基本思想。

2.什么是CDN?

  CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络"边缘",使用户可 以就近取得所需的内容,解决Internet网络拥塞状况,提高用户访问网站的响应速度。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等 原因,解决用户访问网站的响应速度慢的根本原因。

  狭义地讲,内容分发布网络(CDN)是一种新型的网络构建方式,它是为能在传统的IP网发布宽带丰富媒体而特别优化的网络覆盖层;而从广义的角 度,CDN代表了一种基于质量与秩序的网络服务模式。简单地说,内容发布网(CDN)是一个经策略性部署的整体系统,包括分布式存储、负载均衡、网络请求 的重定向和内容管理4个要件,而内容管理和全局的网络流量管理(Traffic Management)是CDN的核心所在。通过用户就近性和服务器负载的判断,CDN确保内容以一种极为高效的方式为用户的请求提供服务。总的来说,内 容服务基于缓存服务器,也称作代理缓存(Surrogate),它位于网络的边缘,距用户仅有"一跳"(Single Hop)之遥。同时,代理缓存是内容提供商源服务器(通常位于CDN服务提供商的数据中心)的一个透明镜像。这样的架构使得CDN服务提供商能够代表他们 客户,即内容供应商,向最终用户提供尽可能好的体验,而这些用户是不能容忍请求响应时间有任何延迟的。据统计,采用CDN技术,能处理整个网站页面的 70%~95%的内容访问量,减轻服务器的压力,提升了网站的性能和可扩展性。

  与目前现有的内容发布模式相比较,CDN强调了网络在内容发布中的重要性。通过引入主动的内容管理层的和全局负载均衡,CDN从根本上区别于传 统的内容发布模式。在传统的内容发布模式中,内容的发布由ICP的应用服务器完成,而网络只表现为一个透明的数据传输通道,这种透明性表现在网络的质量保 证仅仅停留在数据包的层面,而不能根据内容对象的不同区分服务质量。此外,由于IP网的"尽力而为"的特性使得其质量保证是依靠在用户和应用服务器之间端 到端地提供充分的、远大于实际所需的带宽通量来实现的。在这样的内容发布模式下,不仅大量宝贵的骨干带宽被占用,同时ICP的应用服务器的负载也变得非常 重,而且不可预计。当发生一些热点事件和出现浪涌流量时,会产生局部热点效应,从而使应用服务器过载退出服务。这种基于中心的应用服务器的内容发布模式的 另外一个缺陷在于个性化服务的缺失和对宽带服务价值链的扭曲,内容提供商承担了他们不该干也干不好的内容发布服务。

  纵观整个宽带服务的价值链,内容提供商和用户位于整个价值链的两端,中间依靠网络服务提供商将其串接起来。随着互联网工业的成熟和商业模式的变 革,在这条价值链上的角色越来越多也越来越细分。比如内容/应用的运营商、托管服务提供商、骨干网络服务提供商、接入服务提供商等等。在这一条价值链上的 每一个角色都要分工合作、各司其职才能为客户提供良好的服务,从而带来多赢的局面。从内容与网络的结合模式上看,内容的发布已经走过了ICP的内容(应 用)服务器和IDC这两个阶段。IDC的热潮也催生了托管服务提供商这一角色。但是,IDC并不能解决内容的有效发布问题。内容位于网络的中心并不能解决 骨干带宽的占用和建立IP网络上的流量秩序。因此将内容推到网络的边缘,为用户提供就近性的边缘服务,从而保证服务的质量和整个网络上的访问秩序就成了一 种显而易见的选择。而这就是内容发布网(CDN)服务模式。CDN的建立解决了困扰内容运营商的内容"集中与分散"的两难选择。无疑对于构建良好的互联网 价值链是有价值的,也是不可或缺的。

3.CDN新应用和客户

  目前的CDN服务主要应用于证券、金融保险、ISP、ICP、网上交易、门户网站、大中型公司、网络教学等领域。另外在行业专网、互联网中都可 以用到,甚至可以对局域网进行网络优化。利用CDN,这些网站无需投资昂贵的各类服务器、设立分站点,特别是流媒体信息的广泛应用、远程教学课件等消耗带 宽资源多的媒体信息,应用CDN网络,把内容复制到网络的最边缘,使内容请求点和交付点之间的距离缩至最小,从而促进Web站点性能的提高,具有重要的意 义。CDN网络的建设主要有企业建设的CDN网络,为企业服务;IDC的CDN网络,主要服务于IDC和增值服务;网络运营上主建的CDN网络,主要提供 内容推送服务;CDN网络服务商,专门建设的CDN用于做服务,用户通过与CDN机构进行合作,CDN负责信息传递工作,保证信息正常传输,维护传送网 络,而网站只需要内容维护,不再需要考虑流量问题。

  CDN能够为网络的快速、安全、稳定、可扩展等方面提供保障。

  IDC建立CDN网络,IDC运营商一般需要有分部各地的多个IDC中心,服务对象是托管在IDC中心的客户,利用现有的网络资源,投资较少, 容易建设。例如某IDC全国有10个机房,加入IDC的CDN网络,托管在一个节点的Web服务器,相当于有了10个镜像服务器,就近供客户访问。宽带城 域网,域内网络速度很快,出城带宽一般就会瓶颈,为了体现城域网的高速体验,解决方案就是将Internet网上内容高速缓存到本地,将Cache部署在 城域网各POP点上,这样形成高效有序的网络,用户仅一跳就能访问大部分的内容,这也是一种加速所有网站CDN的应用。

4.CDN 的工作原理

  在描述CDN的实现原理,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别:

  由上图可见,用户访问未使用CDN缓存网站的过程为:

  1)、用户向浏览器提供要访问的域名;

  2)、浏览器调用域名解析函数库对域名进行解析,以得到此域名对应的IP地址;

  3)、浏览器使用所得到的IP地址,域名的服务主机发出数据访问请求;

  4)、浏览器根据域名主机返回的数据显示网页的内容。

  通过以上四个步骤,浏览器完成从用户处接收用户要访问的域名到从域名服务主机处获取数据的整个过程。CDN网络是在用户和服务器之间增加 Cache层,如何将用户的请求引导到Cache上获得源服务器的数据,主要是通过接管DNS实现,下面让我们看看访问使用CDN缓存后的网站的过程:

  通过上图,我们可以了解到,使用了CDN缓存后的网站的访问过程变为:

  1)、用户向浏览器提供要访问的域名;

  2)、浏览器调用域名解析库对域名进行解析,由于CDN对域名解析过程进行了调整,所以解析函数库一般得到的是该域名对应的CNAME记录,为 了得到实际IP地址,浏览器需要再次对获得的CNAME域名进行解析以得到实际的IP地址;在此过程中,使用的全局负载均衡DNS解析,如根据地理位置信 息解析对应的IP地址,使得用户能就近访问。

  3)、此次解析得到CDN缓存服务器的IP地址,浏览器在得到实际的IP地址以后,向缓存服务器发出访问请求;

  4)、缓存服务器根据浏览器提供的要访问的域名,通过Cache内部专用DNS解析得到此域名的实际IP地址,再由缓存服务器向此实际IP地址提交访问请求;

  5)、缓存服务器从实际IP地址得得到内容以后,一方面在本地进行保存,以备以后使用,二方面把获取的数据返回给客户端,完成数据服务过程;

  6)、客户端得到由缓存服务器返回的数据以后显示出来并完成整个浏览的数据请求过程。

  通过以上的分析我们可以得到,为了实现既要对普通用户透明(即加入缓存以后用户客户端无需进行任何设置,直接使用被加速网站原有的域名即可访 问),又要在为指定的网站提供加速服务的同时降低对ICP的影响,只要修改整个访问过程中的域名解析部分,以实现透明的加速服务,下面是CDN网络实现的 具体操作过程。

  1)、作为ICP,只需要把域名解释权交给CDN运营商,其他方面不需要进行任何的修改;操作时,ICP修改自己域名的解析记录,一般用cname方式指向CDN网络Cache服务器的地址。

  2)、作为CDN运营商,首先需要为ICP的域名提供公开的解析,为了实现sortlist,一般是把ICP的域名解释结果指向一个CNAME记录;

  3)、当需要进行sorlist时,CDN运营商可以利用DNS对CNAME指向的域名解析过程进行特殊处理,使DNS服务器在接收到客户端请求时可以根据客户端的IP地址,返回相同域名的不同IP地址;

  4)、由于从cname获得的IP地址,并且带有hostname信息,请求到达Cache之后,Cache必须知道源服务器的IP地址,所以在CDN运营商内部维护一个内部DNS服务器,用于解释用户所访问的域名的真实IP地址;

  5)、在维护内部DNS服务器时,还需要维护一台授权服务器,控制哪些域名可以进行缓存,而哪些又不进行缓存,以免发生开放代理的情况。

5.CDN的技术手段

  实现CDN的主要技术手段是高速缓存、镜像服务器。可工作于DNS解析或HTTP重定向两种方式,通过Cache服务器,或异地的镜像站点 完成内容的传送与同步更新。DNS方式用户位置判断准确率大于85%,HTTP方式准确率为99%以上;一般情况,各Cache服务器群的用户访问流入数 据量与Cache服务器到原始网站取内容的数据量之比在2:1到3:1之间,即分担50%到70%的到原始网站重复访问数据量(主要是图片,流媒体文件等 内容);对于镜像,除数据同步的流量,其余均在本地完成,不访问原始服务器。

  镜像站点(Mirror Site)服务器是我们经常可以看到的,它让内容直截了当地进行分布,适用于静态和准动态的数据同步。但是购买和维护新服务器的费用较高,另外还必须在各 个地区设置镜像服务器,配备专业技术人员进行管理与维护。大型网站在随时更新各地服务器的同时,对带宽的需求也会显著增加,因此一般的互联网公司不会建立 太多的镜像服务器。

  高速缓存手段的成本较低,适用于静态内容。Internet的统计表明,超过80%的用户经常访问的是20%的网站的内容,在这个规律下,缓存 服务器可以处理大部分客户的静态请求,而原始的WWW服务器只需处理约20%左右的非缓存请求和动态请求,于是大大加快了客户请求的响应时间,并降低了原 始WWW服务器的负载。根据美国IDC公司的调查,作为CDN的一项重要指标-缓存的市场正在以每年近100%的速度增长,全球的营业额在2004年将达 到45

6.CDN的网络架构

  CDN网络架构主要由两大部分,分为中心和边缘两部分,中心指CDN网管中心和DNS重定向解析中心,负责全局负载均衡,设备系统安装在管理中心机房,边缘主要指异地节点,CDN分发的载体,主要由Cache和负载均衡器等组成。

  当用户访问加入CDN服务的网站时,域名解析请求将最终交给全局负载均衡DNS进行处理。全局负载均衡DNS通过一组预先定义好的策略,将当时 最接近用户的节点地址提供给用户,使用户能够得到快速的服务。同时,它还与分布在世界各地的所有CDNC节点保持通信,搜集各节点的通信状态,确保不将用 户的请求分配到不可用的CDN节点上,实际上是通过DNS做全局负载均衡。

  对于普通的Internet用户来讲,每个CDN节点就相当于一个放置在它周围的WEB。通过全局负载均衡DNS的控制,用户的请求被透明地指向离他最近的节点,节点中CDN服务器会像网站的原始服务器一样,响应用户的请求。由于它离用户更近,因而响应时间必然更快。

  每个CDN节点由两部分组成:负载均衡设备和高速缓存服务器

  负载均衡设备负责每个节点中各个Cache的负载均衡,保证节点的工作效率;同时,负载均衡设备还负责收集节点与周围环境的信息,保持与全局负载DNS的通信,实现整个系统的负载均衡。

  高速缓存服务器(Cache)负责存储客户网站的大量信息,就像一个靠近用户的网站服务器一样响应本地用户的访问请求。

  CDN的管理系统是整个系统能够正常运转的保证。它不仅能对系统中的各个子系统和设备进行实时监控,对各种故障产生相应的告警,还可以实时监测 到系统中总的流量和各节点的流量,并保存在系统的数据库中,使网管人员能够方便地进行进一步分析。通过完善的网管系统,用户可以对系统配置进行修改。

  理论上,最简单的CDN网络有一个负责全局负载均衡的DNS和各节点一台Cache,即可运行。DNS支持根据用户源IP地址解析不同的IP, 实现就近访问。为了保证高可用性等,需要监视各节点的流量、健康状况等。一个节点的单台Cache承载数量不够时,才需要多台Cache,多台Cache 同时工作,才需要负载均衡器,使Cache群协同工作。

亿美元。网络流媒体的发展还将剌激这个市场的需求。

7. CDN 示例

  商业化的CDN网络是用于服务性质的,高可用性等要求非常高,有专业产品和CDN网络解决方案,本文主要从理论角度,理解CDN的实现过程,并利用已有网络环境和开源软件做实际配置,更深刻理解CDN的具体工作过程。

  Linux 是开放源代码的免费操作系统,已经成功应用于许多关键领域。Bind是Unix/FreeBSD/Linux等类unix平台上非常有名DNS服务程序, Internet上超过60%的DNS运行的是bind。Bind的最新版本是9.x,用的比较多的是8.x,bind 9有很多新特性,其中一项是根据用户端源地址对同一域名解析不同的IP地址,有了这种特性,能把用户对同一域名的访问,引导到不同地域节点的服务器上去访 问。Squid是Linux等操作系统上有名的Cache引擎,与商业Cache引擎相比,Squid的性能比较低,基本功能工作原理与商业Cache产 品是一致的,作为试验,是非常容易配置运行起来。以下简要介绍CDN的配置流程。

  1、要加入CDN服务的网站,需要域名(如www.linuxaid.com.cn,地址202.99.11.120)解析权提供给CDN运营 商,Linuxaid的域名解析记录只要把www主机的A记录改为CNAME并指向cache.cdn.com即可。cache.cdn.com是CDN 网络自定义的缓存服务器的标识。在/var/named/linuxaid.com.cn域名解析记录中,由:


www IN A 202.99.11.120
改为
www IN CNAME cache.cdn.com.

  2、CDN运营商得到域名解析权以后,得到域名的CNAME记录,指向CDN网络属下缓存服务器的域名,如cache.cdn.com,CDN网络的全局负载均衡DNS,需要把CNAME记录根据策略解析出IP地址,一般是给出就近访问的Cache地址。

  Bind 9的基本功能可以根据不同的源IP地址段解析对应的IP,实现根据地域就近访问的负载均衡,一般可以通过Bind 9的sortlist选项实现根据用户端IP地址返回最近的节点IP地址,具体的过程为:

  1)为cache.cdn.com设置多个A记录,/var/named/cdn.com 的内容如下:


$TTL 3600
@ IN SOA ns.cdn.com. root.ns.cdn.com. (
2002090201 ;Serial num
10800 ;Refresh after 3 hours
3600 ;Retry
604800 ;Expire
1800 ;Time to live
)
IN NS ns
www IN A 210.33.21.168
ns IN A 202.96.128.68
cache IN A 202.93.22.13 ;有多少个CACHE地址
cache IN A 210.21.30.90 ;就有多少个CACHE的A记录
cache IN A 211.99.13.47

  2) /etc/named.conf中的内容为:


options {
directory "/var/named";
sortlist {
#这一段表示当在本地执行查询时
#将按照202.93.22.13,210.21.30.90,211.99.13.47的顺序返回地址
{ localhost;
{ localnets;
202.93.22.13;
{ 210.21.30.90; 211.99.13.47; };
};
};
#这一段表示当在202/8地址段进行DNS查询时
#将按照202.93.22.13,210.21.30.90,211.99.13.47的顺序返回地址
{ 202/8;
{ 202.93.22.13;
{ 210.21.30.90; 211.99.13.47; };
};
};
#这一段表示当在211/8地址段进行DNS查询时
#将按照211.99.13.47,202.93.22.13,210.21.30.90的顺序返回地址,
#也就是211.99.13.47是最靠近查询地点的节点
{ 211/8;
{ 211.99.13.47;
{ 202.93.22.13; 210.21.30.90; };
};
};
{ 61/8;
{ 202.93.22.13;
{ 210.21.30.90; 211.99.13.47; };
};
};
};
};

zone "." {
type hint;
file "root.cache";
};

zone "localhost" {
type master;
file "localhost";
};

zone "cdn.com" {
type master;
file "cdn.com";
};

  3、Cache在CDN网络中如果工作在服务器加速模式,因为配置里已经写明加速服务器的url,所以Cache直接匹配用户请求,到源服务器 获得内容并缓存供下次使用;如果Cache工作在客户端加速模式,Cache需要知道源服务器的IP地址,所以CDN网络维护和运行一个供Cache使用 的DNS服务器,解析域名的真实IP地址,如202.99.11.120 ,各域名的解析记录与未加入CDN网络之前一样。

  4、工作在CDN网络中缓存服务器必须工作在透明方式,对于Squid来说,需要设置以下参数:


httpd_accel_host virtual
httpd_accel_port 80
httpd_accel_with_proxy on
httpd_accel_uses_host_header on


posted @ 2005-10-30 14:48 bluesky 阅读(332) | 评论 (0)编辑 收藏

<html> 
<head> 
  
<meta http-equiv="Content-Type" content="text/html; charset=gb2312"> 
  
<noscript><meta http-equiv="refresh" content="0;url=about:noscript">noscript> 
  
<title>屏蔽鼠标右键、Ctrl+N、Shift+F10、Alt+F4、F11、F5刷新、退格键title> 
head> 
<body> 
<script language="Javascript">