在 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 端口)鏈接到服務器。

然而,市面上存在着大量開源庫,您可利用它們輕鬆編寫自定義的加密例程。相似地,配置防火牆也是垂手可得的,實際上,只需付出不多的代價,便可得到強大的實時服務器推送功能。

相關文章
相關標籤/搜索