在 Ajax 应用程序中实现实时数据推送

简介 node

Ajax 技术已经存在了一段时间,开发的动力已经真正开始获得了人们的承认。愈来愈多的 Web 站点正在考虑使用 Ajax 进行设计,开发人员也开始将 Ajax 的能力发挥到极限。随着社交网络和协做式报告等现象的出现,一组全新的要求浮现出来。若是有其余用户更改了某位用户正在观察的任何活动,则用户但愿获得通知。若是一个 Web 站点显示动态数据,如股价等,那么全部用户都必须当即获得关于变动的通知。 web

这些场景自己属于一类称为 “服务器推送” 的问题。一般,服务器是中心实体,服务器将首先得到关于所发生的任何更改的通知,服务器负责将此类更改通知全部链接的客户端。但遗憾的是,HTTP 是客户端-服务器通讯的标准协议,它是无状态的,并且在某种意义上来讲,也是一种单向的协议。HTTP 场景中的全部通讯都必须由客户端发起,至服务器结束,然而咱们所提到的场景的需求则彻底相反。对于服务器推送来讲,须要由服务器发起通讯,并向客户端发送数据。HTTP 协议并没有相关配置,Web 站点应用程序开发人员使用首创的方法来绕过这些问题,例如轮询,客户端会以固定(或可配置)的时间间隔与服务器联系,查找是否有新更新可用。在大多数时候,这些轮询纯粹是浪费,于是服务器没有任何更新。这种方法不是没有代价的,它有两大主要问题。 canvas

  1. 这种方法极度浪费网络资源。每个轮询请求一般都会建立一个 TCP 套接字链接(除非 HTTP 1.1 将本身的 keepAlive 设置为 true,此时将使用以前建立的套接字)。套接字链接自己代价极高。除此以外,每一次请求都要在网络上传输一些数据,若是请求未在服务器上发现任何更新,那么这样的数据传输就是浪费资源。若是在客户端机器上还运行着其余应用程序,那么这些轮询会减小传输数据可用的带宽。
  2. 即使是请求成功,确实为客户端传回了更新,考虑到轮询的频率,这样的更新也不是实时的。例如,假设轮询配置为每 20 秒一次,就在一次请求刚刚从服务器返回时,发生了更新。那么此次更新将在 20 秒后的下一次请求到来时才能返回客户端。于是,服务器上准备好供客户端使用的更新必须等待一段时间,才能真正地为客户端所用。对于须要以尽量实时的方式运行的应用程序来讲,这样的等待是不可接受的。

考虑到这样两个问题,对于须要关键、实时的服务器端更新的企业应用程序而言,轮询并非最理想的方法。在这篇文章中,我将介绍多种能够替代轮询的方法。每一种替代方法在某些场景中都有本身的突出之处。我将说明这些场景,并展现须要实时服务器推送的一组 UI。 后端

回页首 浏览器

Ajax 应用程序中的服务器更新技术 缓存

让咱们来具体看看用于更新来自服务器的信息的一些经常使用技术,这些技术模拟了服务器推送。 安全

短轮询 服务器

短轮询也称为高频轮询,就是我在本文开头处介绍的技术。这种方法在如下状况中表现最好: 网络

  1. 有足够的带宽可用。
  2. 根据统计数据,大多数时候,请求都能得到更新。例如,股市数据就老是有可用更新。
  3. 使用 HTTP 1.1 协议。设置 keepAlive=true,于是,同一个套接字链接始终保持活动状态,并可重用。

长轮询 app

长轮询是用于更新服务器数据的另一种方法。这种方法的理念就是客户端创建链接,服务器阻塞链接(经过使请求线程在某些条件下处于等待状态),有数据可用时,服务器将经过阻塞的链接发送数据,随后关闭链接。客户端在接收到更新后,当即从新创建链接,服务器重复上述过程,以此实现近于实时的通讯。然而,长轮询具备如下缺陷:

  1. 通常的浏览器默认容许每台服务器具备两个链接。在这种状况下,一个链接始终是繁忙状态。于是,UI 只有一个链接(也就是说,能力减半)可用于为用户请求提供服务。这可能会致使某些操做的性能下降。
  2. 仍然须要打开和关闭 HTTP 链接,若是采用的是非持久链接模式(keepAlive=false),那么这种方法的代价可能极高。
  3. 这种方法近于实时,但并不是真正的实时。(固然,某些外部因素老是不可控的,好比网络延时,在任何方法中都会存在这些因素。)

流通道

流通道(streaming channel)与长轮询大体相同,差异在于服务器不会关闭响应流。而是特地保持其处于打开状态,使浏览器认为还有更多数据即将到来。可是,流通道也有着本身的缺陷:

  1. 最大的问题就是数据刷新(flushing)。过去,Web 服务器会缓存响应数据,仅在接受到足够的字节数或块数后才会发送出去。在这种状况下,即使应用程序刷新数据,也仍然会由服务器缓存,以实现优化。更糟的是,若是在客户端和服务器之间存在代理服务器,那么代理也可能会为自身之便缓存数据。
  2. 若是发现套接字将打开较长的时间,某些浏览器实现可能会自行决定关闭套接字。在这种状况下,通道须要从新创建。

一般,第一个问题可经过为每一个流响应附加垃圾有效载荷来解决,使响应数据足以填满缓冲区。第二个问题可经过 “保持活动” 或按固定间隔 “同步” 消息来欺瞒浏览器,使浏览器认为数据是以较慢的速率传入的。

这些解决方案适用的用例范围狭窄。全部这些方法都已经在 Internet 上的某些解决方案中获得了应用。然而,这些解决方案都遭遇了相同的问题:缺少可伸缩性。典型状况下,要阻塞一个请求,您须要阻塞处理请求的线程,由于现在几乎全部应用服务器都会执行阻塞 I/O。即使不是这样,Java™ 2 Platform, Enterprise Edition (J2EE) 也未提供为 HTTP 请求和响应执行非阻塞 I/O 的标准。(Servlets 3.0 API 可解决这一问题,由于这些 API 中包含 Comet Servlet。)

至此,您须要具有非阻塞 I/O(NIO)服务器,客户端应用程序经过它进行链接。因为此类套接字是纯 TCP 二进制套接字,于是将实现如下目标:

  1. 因为服务器端具备 NIO,于是可实现更高的可伸缩性。
  2. 响应缓存的问题不复存在,由于这个套接字直接受应用程序的控制。

基于上述说明,有必要指出这种方法的四个缺点:

  1. 因为使用的是二进制 TCP 套接字,于是应用程序没法真正地利用 HTTPS 层提供的 SSL 安全性。因此,要求数据安全性的应用程序可能须要提供本身的加密工具。
  2. 一般状况下,服务器套接字将在 80 之外的端口上运行,若是防火墙仅容许来自端口 80 的流量,将出现问题。于是,可能须要进行一些端口配置。
  3. Ajax 客户端没法经过后端打开 TCP 套接字链接。
  4. 即使 Ajax 客户端可以执行 open 函数,也没法理解二进制内容,这是由于 Ajax 使用的是 XML 或 JSON(基于文本)格式。

在这篇文章中,我要强调的是如何真正地绕开第三个和第四个问题。若是您可以处理安全性和防火墙问题,那么其余问题也能获得处理。这种作法的获益极为显著。

您可为应用程序实现最大程度的实时服务器推送行为(不考虑网络延时等外部因素),您将得到高度可伸缩的解决方案(以同时链接的客户端数量为准)。

下面咱们将开始探索如何解决上述的第三个和第四个问题。

回页首

基于套接字的 RIA 技术

Ajax 并不能真正地解决第三个和第四个问题。于是,您须要利用其余 RIA 技术寻求解决方案。有两种 RIA 技术提供的套接字 API 可与 Ajax 应用程序交互。这两种技术是 Adobe Flex 和 OpenLaszlo。全面介绍这两种技术并不是本文讨论范围以内(更多信息请参见 参考资料),但这些技术提供的两种特性以下所示:

  1. 均能经过后端打开 TCP 二进制套接字
  2. 均能出色地与运行在同一个浏览器窗口中的 Ajax 应用程序(主要是 JavaScript)交互

但这仅仅解决了部分问题。您确实能够打开套接字,可使 Ajax 应用程序使用它们,但 Ajax 应用程序仍然没法处理纯二进制数据。这又该怎么办?实际上,这两种技术都提供了二进制 TCP 套接字的一种变体,称为 XMLSocket,它可用于来回传输纯 XML 数据。这正是您须要的东西。若是这些技术可以经过服务器打开套接字,若是它们可以传输 XML 数据,咱们的任务就完成了。Ajax 应用程序可充分利用这一点,模拟实时服务器推送技术。下面将介绍如何实现。

实现 Ajax 服务器推送

我将使用两种工具解释这项技术:Adobe Flex 和 OpenLaszlo。首先,您须要编写可以接收并缓存链接的后端服务器。在这里不能太过偏离主题,于是要保证服务器基于阻塞 I/O。

您须要建立一个服务器套接字,接收预先指定地址的链接:


清单 1. 建立服务器套接字
public class SimpleServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("localhost",20340));
        Socket socket = serverSocket.accept();
    }
}

在这里,我将服务器套接字绑定到 localhost:20340 这一地址。当一个客户端链接到该服务器套接字时,它将为我提供一个套接字,显示链接。Flex 客户端随后会要求策略文件,这是其安全性模型的一部分。一般,这个策略文件的形式相似于清单 2。


清单 2. Flex 客户端策略文件
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM 
    "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy> 
<allow-access-from domain="*" to-ports="20340"/> 
</cross-domain-policy>

就在链接以后,Flex 客户端会当即发送一条策略文件的请求。该请求仅包含一个 XML 标记:<policy-file-request/>。在响应中,您须要返回此策略文件。清单 3 中的代码就完成了这个任务。


清单 3. 发送策略文件响应
public static void main(String[] args) throws IOException {
   ServerSocket serverSocket = new ServerSocket();
   serverSocket.bind(new InetSocketAddress("localhost", 20340));
   Socket socket = serverSocket.accept();
   String POLICY_REQUEST = "<policy-file-request/>\u0000";
   String POLICY_FILE = "<?xml version=\"1.0\"?>\n" +
      "<!DOCTYPE cross-domain-policy SYSTEM 
         \"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd\">\n" +
      "<cross-domain-policy> \n" +
      " <allow-access-from domain=\"*\" to-ports=\"20340\"/> \n" +
      "</cross-domain-policy>";
   byte[] b = new byte[POLICY_REQUEST.length()];
   DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
   dataInputStream.readFully(b);
   String request = new String(b);
   if (POLICY_REQUEST.equals(request)) {
       DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
       dataOutputStream.write(POLICY_FILE.getBytes());
       dataOutputStream.flush();
       dataOutputStream.close();
   } else throw new IllegalArgumentException("unknown request format " + request);
 }

此代码创建了与客户端的成功链接。如今,服务器能够与客户端发起 “握手” 之类的协议,此时,服务器一般会指定一个唯一的 ID,并将其发送给客户端,此后,服务器可根据 ID 缓存套接字,在此以后,若是服务器须要向客户端推送某些数据,能够按照 ID 定位套接字,并使用其输出流。幸运的是,OpenLaszlo 也使用了相同的基于策略文件的机制,于是,一样的服务器代码适用于两种场景。

下面将介绍如何建立 Flex 套接字,随后将其与 Ajax 应用程序链接。

使用 Adobe Flex 打开客户端套接字

清单 4 中的代码展现了如何经过 Flex 打开客户端套接字:


清单 4. 经过 Flex 打开客户端
var socket : XMLSocket = new XMLSocket();
// register events:
socket.addEventListener(Event.CLOSE, closehandler);
socket.addEventListener(Event.CONNECT, connectHandler);
socket.addEventListener(Event.OPEN, openHandler);
socket.addEventListener(ProgressEvent.SOCKET_DATA, readHandler);
socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
socket.connect("localhost",20340);

完成 socket.connect() 调用后,Flex 将向服务器发送一条请求,要求提供策略文件,期待得到 XML 响应。完成以后,链接即创建,这个套接字如今便可用于从服务器推送数据。

做为拼图的最后一块,您将看到 Flex 如何将 Ajax 做为应用程序调用。为此,要编写一个可处理服务器端消息的通用 JavaScript 函数。咱们将此方法命名为 handleServerMessageReceived(message)。此方法会获取来自服务器的 XML 代码,此方法对于消息的处理方式以应用程序为依据。清单 5 中的代码展现了 Flex 如何调用 JavaScript 函数。这是 readHandler 方法的代码,该方法在接收到服务器 XML 消息时被调用。


清单 5. 使用 handleServerMessageReceived(message) 的 readhHandler 代码
public  function readHandler(e :  DataEvent) : void {
  var message   : XML = e.data as XML;
  ExternalInterface.call("handleServerMessageReceived", message);
}

就是这样!就是这样简单。您已经建立了一个 XML 套接字链接。当来自服务器的数据送达时,您可调用 Ajax 中的某些通用处理函数,处理这些消息。完整源代码可供下载(请参见 下载 部分)。

下面来看看 OpenLaszlo 如何实现相同的目标。

使用 OpenLaszlo 打开客户端套接字

因为 OpenLaszlo 应用程序以 Flash 和 DHTML 平台为目标,于是其 API 和脚本语言相似于 Flash 和 JavaScript。这主要是为但愿迁移到 OpenLaszlo(做为 RIA 的替代方案)的 Web 开发人员提供便利。

OpenLaszlo 提供了两种建立与后端之间的持久链接的方法。一种方法要使用 Lz(Laszlo 的缩写)标准库中提供的 ConnectionManager API。但其文档明确说明了如下内容:

警告:这项特性是临时的。此特性用于容量有限的环境,可以用于开发,但咱们不推荐使用此特性进行部署(不包括低容量、非任务关键型的部署)。若对使用此版本的持久链接的应用程序的健壮性有任何问题,请直接咨询 Laszlo Systems。”

或许目前这是一项实验技术,但在将来的 OpenLaszlo 版本中,它将获得证明。

第二种方法与 Flex 类似,您要手动打开 XML 套接字链接,等待 READ_DATA 事件发生。清单 6 展现了实现方法。


清单 6. 定义 XMLSocket 类
<class name="ClientSocket" extends="node">
	<attribute name="host" />
	<attribute name="port" />
	<XMLSocket name='xml_socket'/>
	<handler name="oninit">
		// connect the socket here:
		xml_socket.connect(host,port);
	</handler>
	<handler name='onData' reference='xml_socket' args='messageXML'>
 <![CDATA[
		ExternalInterface.call(‘handleServerMessageReceived',messageXML);
	]]>
	</method>	
</class>

(为简短起见,忽略了其余处理方法。在本文的 下载 部分中可得到完整的代码清单。)

就是这样,建立一个套接字对象并链接此对象就是这样轻松。这一代码清单建立了一个名为 ClientSocket 的新类,随后声明了一个名为 “xml_socket” 的 XML 套接字对象。只要此套接字对象读取到来自服务器的数据,就会触发 onData 事件,该事件将由为 onData定义的处理方法处理。最后,在 onData 处理方法中,调用 Ajax 应用程序中的外部 JavaScript 函数。此后的流程与 Flex 客户端相同。

要建立 ClientSocket 对象,只需声明它便可:


清单 7. 声明 ClientSocket
<canvas>
	<ClientSocket id='serverPushSocket' host='localhost' port='20340'/>
</canvas>

为 ClientSocket 触发了 init 事件时,将尝试链接指定主机和端口的后端。(请参见清单 6 中的 oninit 处理方法。)

回页首

结束语

这篇文章讨论了几种模拟服务器推送的方法,从纯轮询到实时服务器推送,文中说明了每种方法的优缺点。最后,我重点关注了可以提供最优服务器可伸缩性和实时服务器推送行为的方法。

服务器推送并不是适用于每个应用程序。实际上,大多数应用程序都很是适合普通的请求/响应场景。其余一些应用程序使用轮询和相似的技术足以知足需求。只有那些服务器更新极为重要、客户端须要获得即时通知的重量级应用程序才须要本文所述技术。有必要再次强调,这种技术有两个主要的缺点:

  1. 若是数据须要经过 HTTPS 传输,客户端套接字没法利用 SSL 加密工具。
  2. 防火墙须要容许客户端套接字经过非标准端口(非 80 端口)链接到服务器。

然而,市面上存在着大量开源库,您可利用它们轻松编写自定义的加密例程。相似地,配置防火墙也是垂手可得的,实际上,只需付出不多的代价,便可得到强大的实时服务器推送功能。

相关文章
相关标签/搜索