反向ajax實現

在過去的幾年中,web開發已經發生了很大的變化。現現在,咱們指望的是可以經過web快速、動態地訪問應用。在這一新的文章系列中,咱們學習如何使用反 向Ajax(Reverse Ajax)技術來開發事件驅動的web應用,以此來實現更好的用戶體驗。客戶端的例子使用的是JQuery JavaScript庫,在這首篇文章中,咱們探索不一樣的反向Ajax技術,使用可下載的例子來學習使用了流(streaming)方法和長輪詢(long polling)方法的Comet。

  前言javascript

  web開發在過去的幾年中有了很大的進展,咱們已經遠超了把靜態網頁連接在一塊兒的作法,這種作法會引發瀏覽器的刷新,而且要等待頁面的加載。如今須要 的是可以經過web來訪問的徹底動態的應用,這些應用一般須要儘量的快,提供近乎實時的組件。在這一新的由五部分組成的文章系列中,咱們學習如何使用反 向Ajax(Reverse Ajax)技術來開發事件驅動的web應用。html

  在這第一篇文章中,咱們要了解反向Ajax、輪詢(polling)、流(streaming)、Comet和長輪詢(long polling),學習如何實現不一樣的反向Ajax通訊技術,並探討每種方法的優勢和缺點。你能夠下載本文中例子的相應源代碼。html5

  Ajax、反向Ajax和WebSocketjava

  異步的JavaScript和XML(Asynchronous JavaScript and XML,Ajax),一種可經過JavaScript來訪問的瀏覽器功能特性,其容許腳本向幕後的網站發送一個HTTP請求而又無需從新加載頁面。Ajax的出現已經超過了十年,儘管其名字中包含了XML,但你幾乎能夠在Ajax請求中傳送任何的東西,最經常使用的數據是JSON,其與JavaScript語法很接近,且消耗更少帶寬。清單1給出了這樣的一個例子,Ajax請求經過某個地方的郵政編碼來檢索該地的名稱。git

  清單1. Ajax請求舉例github

複製代碼
var url ='http://www.geonames.org/postalCodeLookupJSON?postalcode='
  + $('#postalCode').val() +'&country='
  + $('#country').val() +'&callback=?';
  $.getJSON(url, function(data) {
  $('#placeName').val(data.postalcodes[0].placeName);
});
複製代碼

  在本文可下載的源代碼中,你可在listing1.html中看到這一例子的做用。web

  反向Ajax(Reverse Ajax)本質上則是這樣的一種概念:可以從服務器端向客戶端發送數據。在一個標準的HTTP Ajax請求中,數據是發送給服務器端的,反向Ajax能夠某些特定的方式來模擬發出一個Ajax請求,這些方式本文都會論及,這樣的話,服務器就能夠儘量快地向客戶端發送事件(低延遲通訊)。ajax

  WebSocket技術來自HTML5,是一種最近纔出現的技術,許多瀏覽器已經支持它(Firefox、Google Chrome、Safari等等)。WebSocket啓 用雙向的、全雙工的通訊信道,其經過某種被稱爲WebSocket握手的HTTP請求來打開鏈接,並用到了一些特殊的報頭。鏈接保持在活動狀態,你能夠用 JavaScript來寫和接收數據,就像是正在用一個原始的TCP套接口同樣。WebSocket會在這一文章系列的第二部分中談及。apache

  反向Ajax技術json

  反向Ajax的目的是容許服務器端向客戶端推送信息。Ajax請求在缺省狀況下是無狀態的,且只能從客戶端向服務器端發出請求。你能夠經過使用技術模擬服務器端和客戶端之間的響應式通訊來繞過這一限制。

  HTTP輪詢和JSONP輪詢

  輪詢(polling)涉及了從客戶端向服務器端發出請求以獲取一些數據,這顯然就是一個純粹的Ajax HTTP請求。爲了儘快地得到服務器端事件,輪詢的間隔(兩次請求相隔的時間)必須儘量地小。但有這樣的一個缺點存在:若是間隔減少的話,客戶端瀏覽器就會發出更多的請求,這些請求中的許多都不會返回任何有用的數據,而這將會白白地浪費掉帶寬和處理資源。

  圖1中的時間線說明了客戶端發出了某些輪詢請求,但沒有信息返回這種狀況,客戶端必需要等到下一個輪詢來獲取兩個服務器端接收到的事件。

  圖1. 使用HTTP輪詢的反向Ajax

     

 

 

  JSONP輪詢基本上與HTTP輪詢同樣,不一樣之處則是JSONP能夠發出跨域請求(不是在你的域內的請求)。清單1使用JSONP來經過郵政編碼獲取地名,JSONP請求一般可經過它的回調參數和返回內容識別出來,這些內容是可執行的JavaScript代碼。

  要在JavaScript中實現輪詢的話,你可使用setInterval來按期地發出Ajax請求,如清單2所示:

  清單2. JavaScript輪詢

setInterval(function() {
  $.getJSON('events', function(events) {
    console.log(events);
  });
}, 2000);

  文章源代碼中的輪詢演示給出了輪詢方法所消耗的帶寬,間隔很小,但能夠看到有些請求並未返回事件,清單3給出了這一輪詢示例的輸出。

  清單3. 輪詢演示例子的輸出

複製代碼
[client ] checking for events...
[client ] no event
[client ] checking for events...
[client ]2 events
[event ] At Sun Jun 0515:17:14 EDT 2011
[event ] At Sun Jun 0515:17:14 EDT 2011
[client ] checking for events...
[client ]1 events
[event ] At Sun Jun 0515:17:16 EDT 2011
複製代碼

  用JavaScript實現的輪詢的優勢和缺點:

  1. 優勢:很容易實現,不須要任何服務器端的特定功能,且在全部的瀏覽器上都能工做。

  2. 缺點:這種方法不多被用到,由於它是徹底不具伸縮性的。試想一下,在100個客戶端每一個都發出2秒鐘的輪詢請求的狀況下,所損失的帶寬和資源數量,在這種狀況下30%的請求沒有返回數據。

  Piggyback

  捎帶輪詢(piggyback polling)是一種比輪詢更加聰明的作法,由於它會刪除掉全部非必需的請求(沒有返回數據的那些)。不存在時間間隔,客戶端在須要的時候向服務器端發送請求。不一樣之處在於響應的那部分上,響應被分紅兩個部分:對請求數據的響應和對服務器事件的響應,若是任何一部分有發生的話。圖2給出了一個例子。

  圖2. 使用了piggyback輪詢的反向Ajax

  在實現piggyback技術時,一般針對服務器端的全部Ajax請求可能會返回一個混合的響應,文章的下載中有一個實現示例,以下面的清單4所示。

  清單4. piggyback代碼示例

複製代碼
$('#submit').click(function() {
  $.post('ajax', function(data) {
    var valid = data.formValid;
    // 處理驗證結果
    // 而後處理響應的其餘部分(事件)
    processEvents(data.events);
  });
});
複製代碼

  清單5給出了一些piggyback輸出。

  清單5. piggyback輸出示例

複製代碼
[client ] checking for events...
[server ] form valid ? true
[client ]4 events
[event ] At Sun Jun 0516:08:32 EDT 2011
[event ] At Sun Jun 0516:08:34 EDT 2011
[event ] At Sun Jun 0516:08:34 EDT 2011
[event ] At Sun Jun 0516:08:37 EDT 2011
複製代碼

  你能夠看到表單驗證的結果和附加到響應上的事件,一樣,這種方法也有着一些優勢和缺點:

  1. 優勢:沒有不返回數據的請求,由於客戶端對什麼時候發送請求作了控制,對資源的消耗較少。該方法也是可用在全部的瀏覽器上,不須要服務器端的特殊功能。

  2. 缺點:當累積在服務器端的事件須要傳送給客戶端時,你卻一點都不知道,由於這須要一個客戶端行爲來請求它們。

  Comet

  使用了輪詢或是捎帶的反向Ajax很是受限:其不具伸縮性,不提供低延遲通訊(只要事件一到達服務器端,它們就以儘量快的速度到達瀏覽器端)。 Comet是一個web應用模型,在該模型中,請求被髮送到服務器端並保持一個很長的存活期,直到超時或是有服務器端事件發生。在該請求完成後,另外一個長 生存期的Ajax請求就被送去等待另外一個服務器端事件。使用Comet的話,web服務器就能夠在無需顯式請求的狀況下向客戶端發送數據。

  Comet的一大優勢是,每一個客戶端始終都有一個向服務器端打開的通訊鏈路。服務器端能夠經過在事件到來時當即提交(完成)響應來把事件推給客戶端, 或者它甚至能夠累積再連續發送。由於請求長時間保持打開的狀態,故服務器端須要特別的功能來處理全部的這些長生存期請求。圖3給出了一個例子。(這一文章 系列的第2部分會更加詳細地解釋服務器端的約束條件)。

  圖3. 使用Comet的反向Ajax

  Comet的實現能夠分紅兩類:使用流(streaming)的那些和使用長輪詢(long polling)的那些。

  使用HTTP流的Comet

  在流(streaming)模式中,有一個持久鏈接會被打開。只會存在一個長生存期請求(圖3中的#1),由於每一個到達服務器端的事件都會經過這同一 鏈接來發送。所以,客戶端須要有一種方法來把經過這同一鏈接發送過來的不一樣響應分隔開來。從技術上來說,兩種常見的流技術包括Forever Iframe(隱藏的IFrame),或是被用來在JavaScript中建立Ajax請求的XMLHttpRequest對象的多部分(multi- part)特性。

  Forever Iframe

  Forever Iframe(永存的Iframe)技術涉及了一個置於頁面中的隱藏Iframe標籤,該標籤的src屬性指向返回服務器端事件的servlet路徑。每 次在事件到達時,servlet寫入並刷新一個新的script標籤,該標籤內部帶有JavaScript代碼,iframe的內容被附加上這一 script標籤,標籤中的內容就會獲得執行。

  1. 優勢:實現簡單,在全部支持iframe的瀏覽器上均可用。

  2. 缺點: 沒有方法可用來實現可靠的錯誤處理或是跟蹤鏈接的狀態,由於全部的鏈接和數據都是由瀏覽器經過HTML標籤來處理的,所以你沒有辦法知道鏈接什麼時候在哪一端已被斷開了。

  Multi-part XMLHttpRequest

  第二種技術,更可靠一些,是XMLHttpRequest對象上使用某些瀏覽器(好比說Firefox)支持的multi-part標誌。Ajax請 求被髮送給服務器端並保持打開狀態,每次有事件到來時,一個多部分的響應就會經過這同一鏈接來寫入,清單6給出了一個例子。

  清單6. 設置Multi-part XMLHttpRequest的JavaScript代碼示例

複製代碼
var xhr = $.ajaxSettings.xhr();
xhr.multipart =true;
xhr.open('GET', 'ajax', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    processEvents($.parseJSON(xhr.responseText));
  }
};
xhr.send(null);
複製代碼

  在服務器端,事情要稍加複雜一些。首先你必需要設置多部分請求,而後掛起鏈接。清單7展現瞭如何掛起一個HTTP流請求。(這一系列的第3部分會更加詳細地談及這些API。)

  清單7. 使用Servlet 3 API來在servlet中掛起一個HTTP流請求

複製代碼
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
  // 開始請求的掛起
  AsyncContext asyncContext = req.startAsync();
  asyncContext.setTimeout(0);

  // 給客戶端發回多部分的分隔符
  resp.setContentType("multipart/x-mixed-replace;boundary=\""
  + boundary +"\"");
  resp.setHeader("Connection", "keep-alive");
  resp.getOutputStream().print("--"+ boundary);
  resp.flushBuffer();

  // 把異步上下文放在列表中以被未來只用
  asyncContexts.offer(asyncContext);
}
複製代碼

  如今,每次有事件發生時你均可以遍歷全部的掛起鏈接並向它們寫入數據,如清單8所示:

  清單8. 使用Servlet 3 API來向掛起的多部分請求發送事件

複製代碼
for (AsyncContext asyncContext : asyncContexts) {
  HttpServletResponse peer = (HttpServletResponse)
  asyncContext.getResponse();
  peer.getOutputStream().println("Content-Type: application/json");
  peer.getOutputStream().println();
  peer.getOutputStream().println(new JSONArray()
  .put("At "+new Date()).toString());
  peer.getOutputStream().println("--"+ boundary);
  peer.flushBuffer();
}
複製代碼

  本文可下載文件的Comet-straming文件夾中的部分說明了HTTP流,在運行例子並打開主頁時,你會看到只要事件一到達服務器端,雖然不一樣 步但它們幾乎馬上會出如今頁面上。並且,若是打開Firebug控制檯的話,你就能看到只有一個Ajax請求是打開的。若是再往下看一些,你會看到 JSON響應被附在Response選項卡中,如圖4所示:

  圖4. HTTP流請求的FireBug視圖

  照例,作法存在着一些優勢和缺點:

  1. 優勢:只打開了一個持久鏈接,這就是節省了大部分帶寬使用率的Comet技術。

  2. 缺點:並不是全部的瀏覽器都支持multi-part標誌。某些被普遍使用的庫,好比說用Java實現的CometD,被報告在緩衝方面有問題。例如,一些 數據塊(多個部分)可能被緩衝,而後只有在鏈接完成或是緩衝區已滿時才被髮送,而這有可能會帶來比預期要高的延遲。

  使用HTTP長輪詢的Comet

  長輪詢(long polling)模式涉及了打開鏈接的技術。鏈接由服務器端保持着打開的狀態,只要一有事件發生,響應就會被提交,而後鏈接關閉。接下來。一個新的長輪詢鏈接就會被正在等待新事件到達的客戶端從新打開。

  你可使用script標籤或是單純的XMLHttpRequest對象來實現HTTP長輪詢。

  script標籤

  正如iframe同樣,其目標是把script標籤附加到頁面上以讓腳本執行。服務器端則會:掛起鏈接直到有事件發生,接着把腳本內容發送回瀏覽器,而後從新打開另外一個script標籤來獲取下一個事件。

  1. 優勢:由於是基於HTML標籤的,全部這一技術很是容易實現,且可跨域工做(缺省狀況下,XMLHttpRequest不容許向其餘域或是子域發送請求)。

  2. 缺點:相似於iframe技術,錯誤處理缺失,你不能得到鏈接的狀態或是有干涉鏈接的能力。

  XMLHttpRequest長輪詢

  第二種,也是一種推薦的實現Comet的作法是打開一個到服務器端的Ajax請求而後等待響應。服務器端須要一些特定的功能來容許請求被掛起,只要一 有事件發生,服務器端就會在掛起的請求中送回響應並關閉該請求,徹底就像是你關閉了servlet響應的輸出流。而後客戶端就會使用這一響應並打開一個新 的到服務器端的長生存期的Ajax請求,如清單9所示:

  清單9. 設置長輪詢請求的JavaScript代碼示例

複製代碼
function long_polling() {
  $.getJSON('ajax', function(events) {
    processEvents(events);
    long_polling();
  });
}
long_polling();
複製代碼

  在後端,代碼也是使用Servlet 3 API來掛起請求,正如HTTP流的作法同樣,但你不須要全部的多部分處理代碼,清單10給出了一個例子。

  清單10. 掛起一個長輪詢Ajax請求

複製代碼
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
  AsyncContext asyncContext = req.startAsync();
  asyncContext.setTimeout(0);
  asyncContexts.offer(asyncContext);
}
複製代碼

  在接收到事件時,只是取出全部的掛起請求並完成它們,如清單11所示:

  清單11. 在有事件發生時完成長輪詢Ajax請求

複製代碼
while (!asyncContexts.isEmpty()) {
  AsyncContext asyncContext = asyncContexts.poll();
  HttpServletResponse peer = (HttpServletResponse)
  asyncContext.getResponse();
  peer.getWriter().write(
    new JSONArray().put("At " + new Date()).toString());
  peer.setStatus(HttpServletResponse.SC_OK);
  peer.setContentType("application/json");
  asyncContext.complete();
}
複製代碼

  在附帶的下載源文件中,comet-long-polling文件夾包含了一個長輪詢示例web應用,你可使用 mvn jetty:run 命令來運行它。

  1. 優勢:客戶端很容易實現良好的錯誤處理系統和超時管理。這一可靠的技術還容許在與服務器端的鏈接之間有一個往返,即便鏈接是非持久的(當你的應用有許多的 客戶端時,這是一件好事)。它可用在全部的瀏覽器上;你只須要確保所用的XMLHttpRequest對象發送到的簡單的Ajax請求就能夠了。

  2. 缺點:相比於其餘技術來講,不存在什麼重要的缺點,像全部咱們已經討論過的技術同樣,該方法依然依賴於無狀態的HTTP鏈接,其要求服務器端有特殊的功能來臨時掛起鏈接。

  建議

  由於全部現代的瀏覽器都支持跨域資源共享(Cross-Origin Resource Share,CORS)規範,該規範容許XHR執行跨域請求,所以基於腳本的和基於iframe的技術已成爲了一種過期的須要。

  把Comet作爲反向Ajax的實現和使用的最好方式是經過XMLHttpRequest對象,該作法提供了一個真正的鏈接句柄和錯誤處理。考慮到不 是全部的瀏覽器都支持multi-part標誌,且多部分流可能會遇到緩衝問題,所以建議你選擇經由HTTP長輪詢使用XMLHttpRequest對象 (在服務器端掛起的一個簡單的Ajax請求)的Comet模式,全部支持Ajax的瀏覽器也都支持該種作法。

  結論

  本文提供的是反向Ajax技術的一個入門級介紹,文章探索了實現反向Ajax通訊的不一樣方法,並說明了每種實現的優點和弊端。你的具體狀況和應用需求 將會影響到你對最合適方法的選擇。不過通常來講,若是你想要在低延遲通訊、超時和錯誤檢測、簡易性,以及全部瀏覽器和平臺的良好支持這幾方面有一個最好的 折中的話,那就選擇使用了Ajax長輪詢請求的Comet。

 

這一文章系列探討了如何使用反向Ajax(Reverse Ajax)技術來開發事件驅動的web應用,第1部份內容介紹了實現反向Ajax通訊的幾種不一樣方式:輪詢(polling)、捎帶(piggyback)以及使用了長輪詢(long-polling)和流(streaming)的Comet。在本文中,咱們學習一種新的實現反向Ajax的技術:使用WebSocket,一個新的HTML5 API。WebSocket可由瀏覽器廠商來作本地化實現,或是經過把調用委託給隱藏的被稱爲FlashSocket的Flash組件這種橋接手段來實現。本文還討論了反向Ajax技術帶來的一些服務器端約束。

  前言

  時至今日,用戶期待的是可經過web訪問快速、動態的應用。這一文章系列展現瞭如何使用反向Ajax(Reverse Ajax)技術來開發事件驅動的web應用。該系列的第1部分介紹了反向Ajax、輪詢(polling)、流(streaming)、Comet和長輪詢(long polling)。你已經瞭解了Comet是如何使用HTTP長輪詢的,這是可靠地實現反向Ajax的最好方式,由於現有的全部瀏覽器都提供支持。

  在本文中,咱們將學習如何使用WebSocket來實現反向Ajax。代碼例子被用來幫助說明WebSocket、FlashSocket、服務器端約束、請求做用域(request-scoped)服務以及暫停長生存期請求等,你能夠下載本文中用到的這些源代碼。

  前提條件

  理想狀況下,要充分體會本文的話,你應該對JavaScrpit和Java有必定的瞭解。本文中建立的例子是使用Google Guice來構建的,這是一個使用Java編寫的依賴注入框架。若要讀懂文中所談內容,你應該要熟悉諸如Guice、Spring或是Pico一類的依賴注入框架的概念。

  若要運行本文中的例子,你還須要最新版本的Maven和JDK(參見參考資料)。

  WebSocket

  在HTML5中出現的WebSocket是一種比Comet還要新的反向Ajax技術,WebSocket啓用了雙向的全雙工通訊信道,許多瀏覽器(Firefox、Google Chrome和Safari)都已對此作了支持。鏈接是經過一個被稱爲WebSocket握手的HTTP請求打開的,其用到了一些特殊的報頭。鏈接會保持在活動狀態,你可使用JavaScript來寫入和接收數據,就像是在使用一個原始的TCP套接口同樣。

  WebSocket URL的起始輸入是ws://或是wss://(在SSL上)。

  圖1中的時間線說明了使用WebSocket的通訊。一個帶有特定報頭的HTTP握手被髮送到了服務器端,接着在服務器端或是客戶端就能夠經過JavaScript來使用某種套接口(socket)了,這一套接口可被用來經過事件句柄異步地接收數據。

  圖1. 使用WebSocket的反向Ajax

  本文可下載的源代碼中有一個WebSocket例子,在運行該例子時,你應該會看到相似清單1的輸出。其說明了客戶端的事件是如何發生的,以及如何會當即在客戶端顯示出來。當客戶端發送一些數據時,服務器端迴應客戶端的發送行爲。

  清單1. JavaScript中的WebSocket例子

[client]  WebSocket connection opened
[server] 1 events
[event] ClientID =0
[server] 1 events
[event] At Fri Jun 1721:12:01 EDT 2011
[server] 1 events
[event] From 0 : qqq
[server] 1 events
[event] At Fri Jun 1721:12:05 EDT 2011
[server] 1 events
[event] From 0 : vv

  一般狀況下,在JavaScript中你會如清單2所說明的那樣來使用WebSocket,若是你的瀏覽器支持它的話。

  清單2. JavaScript客戶端例子

var  ws = new WebSocket('ws://127.0.0.1:8080/async');
ws.onopen = function() {
    // 鏈接被打開時調用
};
ws.onerror = function(e) {
    // 在出現錯誤時調用,例如在鏈接斷掉時
};
ws.onclose = function() {
    // 在鏈接被關閉時調用
};
ws.onmessage = function(msg) {
    // 在服務器端向客戶端發送消息時調用
    // msg.data包含了消息
};
// 這裏是如何給服務器端發送一些數據
ws.send('some data');
// 關閉套接口
ws.close();

  發送和接收的數據能夠是任意類型的,WebSocket可被當作是TCP套接口,所以這取決於客戶端和服務器端知道要來回發送的數據是哪一種類型的。這裏的例子發送的是JSON串。

  在JavaScript WebSocket對象被建立後,若是在瀏覽器的控制檯(或是Firebug)中仔細看一下HTTP請求的話,你應該會看到WebSocket特有的報頭。清單3給出了一個例子。

  清單3. HTTP請求和相應報頭示例

Request URL:ws://127.0.0.1:8080/async
Request Method:GET
Status Code:101  WebSocket Protocol Handshake

Request Headers
Connection:Upgrade
Host:127.0.0.1:8080
Origin:http://localhost:8080
Sec-WebSocket-Key1:1 &1~ 33188Yd]r8dp W75q
Sec-WebSocket-Key2:17; 229 *043M 8
Upgrade:WebSocket
(Key3):B4:BB:20:37:45:3F:BC:C7

Response Headers
Connection:Upgrade
Sec-WebSocket-Location:ws://127.0.0.1:8080/async
Sec-WebSocket-Origin:http://localhost:8080
Upgrade:WebSocket
(Challenge Response):AC:23:A5:7E:5D:E5:04:6A:B5:F8:CC:E7:AB:6D:1A:39

  WebSocket握手使用全部的這些報頭來驗證並設置一個長生存期的鏈接,WebSocket的JavaScript對象還包含了兩個有用的屬性:

  ws.url:返回WebSocket服務器的URL
  ws.readyState:返回當前鏈接狀態的值
  1. CONNECTING = 0
  2. OPEN = 1
  3. CLOSED = 2

  服務器端對WebSocket的處理要稍加複雜一些,如今尚未某個Java規範以一種標準的方式來支持WebSocket。要使用web容器(例如Tomcat或是Jetty)的WebSocket功能的話,你得把應用代碼和容器特定的庫緊密耦合在一塊兒才能訪問WebSocket的功能。

  示例代碼的websocket文件夾中的例子使用的是Jetty的WebSocket API,由於咱們使用的是Jetty容器。清單4 給出了WebSocket的處理程序。(本系列的第3部分會使用不一樣的後端WebSocket API。)

  清單4. Jetty容器的WebSocket處理程序

public  final class ReverseAjaxServlet extends WebSocketServlet {
  @Override
  protected WebSocket doWebSocketConnect(HttpServletRequest request,String protocol) {
    return [...]
  }
}

  就Jetty來講,有幾種處理WebSocket握手的方式,比較容易的一種方式是子類化Jetty的WebSocketServlet並實現doWebSocketConnect方法。該方法要求你返回Jetty的WebSocket接口的一個實例,你必需要實現該接口並返回表明了WebSocket鏈接的某種端點(endpoint)。清單5提供了一個例子。

  清單5. WebSocket實現示例

class  Endpoint implements WebSocket {
Outbound outbound;
@Override
publicvoid onConnect(Outbound outbound) {
  this.outbound = outbound;
}
@Override
publicvoid onMessage(byte opcode, String data) {
  // 在接收到消息時調用
  // 你一般用到的就是這一方法
}
@Override
publicvoid onFragment(boolean more, byte opcode,byte[] data, int offset, int length) {
  // 在完成一段內容時,onMessage被調用
  // 一般不在這一方法中寫入東西
}
@Override
publicvoid onMessage(byte opcode, byte[] data,int offset, int length) {
  onMessage(opcode, new String(data, offset, length));
}
@Override
publicvoid onDisconnect() {
  outbound =null;
}
}

  若要向客戶端發送消息的話,你要向outbound中寫入消息,若是清單6所示:

  清單6. 發送消息給客戶端

if  (outbound != null && outbound.isOpen()) {
  outbound.sendMessage('Hello World !');
}

  要斷開並關閉到客戶端的WebSocket鏈接的話,使用outbound.disconnect()。

  WebSocket是一種實現無延遲雙向通訊的很是強大的方法,Firefox、Google Chrome、Opera和其餘的現代瀏覽器都支持這種作法。根據jWebSocket網站的說法:

  1. Chrome從4.0.249版本開始包含本地化的WebSocket。
  2. Safari 5.x包含了本地化的WebSocket。
  3. Firefox 3.7a6和4.0b1+包含了本地化的WebSocket。
  4. Opera從10.7.9.67開始包含了本地化的WebSocket。

  欲瞭解更多關於jWebSocket方面的內容,請查閱參考資料。

  優勢

  WebSocket功能強大、雙向、低延遲,且易於處理錯誤,其不會像Comet長輪詢那樣有許多的鏈接,也沒有Comet流所具備的一些缺點。它的API也很容易使用,無需另外的層就能夠直接使用,而Comet則須要一個很好的庫來處理重鏈接、超時、Ajax請求、確認以及選擇不一樣的傳輸(Ajax長輪詢和jsonp輪詢)。

  缺點

  WebSocket的缺點有這些:

  1. 是一個來自HTML5的新規範,尚未被全部的瀏覽器支持。

  2. 沒有請求做用域(request scope),由於WebSocket是一個TCP套接口而不是一個HTTP請求,有做用域的請求服務,好比說Hibernate的SessionInViewFilter,就不太容易使用。Hibernate是一個持久性框架,其在HTTP請求的外圍提供了一個過濾器。在請求開始時,其在請求線程中設定了一個上下文(包括事務和JDBC鏈接)邊界;在請求結束時,過濾器銷燬這一上下文。

  FlashSocket

  對於不支持WebSocket的瀏覽器來講,有些庫可以回退到FlashSocket(經由Flash的套接口)上。這些庫一般會提供一樣的官方WebSocket API,但他們是經過把調用委託給一個包含在網站中的隱藏的Flash組件來實現的。

  優勢

  FlashSocket透明地提供了WebSocket的功能,即便是在不支持HTML5 WebSocket的瀏覽器上也是如此。

  缺點

  FlashSocket有着下面的這些缺點:

  1. 其須要安裝Flash插件(一般狀況下,全部瀏覽器都會有該插件)。

  2. 其要求防火牆的843端口是打開的,這樣Flash組件才能發出HTTP請求來檢索包含了域受權的策略文件。若是843端口是不可到達的話,則庫應該有回退動做或是給出一個錯誤,全部的這些處理都須要一些時間(最多3秒,這取決於庫),而這會下降網站的速度。

  3. 若是客戶端處在某個代理服務器的後面的話,到端口843的鏈接可能會被拒絕。

  WebSocketJS項目提供了一種橋接方式,其要求一個至少是10版本的Flash來爲Firefox 三、Inernet Explorer 8和Internet Explorer 9提供WebSocket支持。

  建議

  相比於Comet,WebSocket帶來了更多的好處。在平常開發中,客戶端支持的WebSocket速度更快,且產生較少的請求(從而消耗更少的帶寬)。不過,因爲並不是全部的瀏覽器都支持WebSocket,所以,對於Reverse Ajax庫來講,最好的選擇就是可以檢測對WebSocket的支持,而且若是不支持WebSocket的話,還可以回退到Comet(長輪詢)上。

  因爲這兩種技術須要從全部瀏覽器中得到最好的作法並保持兼容性,所以個人建議是使用一個客戶端的JavaScript庫,該庫在這些技術之上提供一個抽象層。本系列的第3和第4部份內容會探討一些庫,第5部分則是說明它們的應用。在服務器端,正以下一節內容討論的那樣,事情則會稍加複雜一些。

  服務器端的反向Ajax約束

  如今你對客戶端可用的反向Ajax解決方案已經有了一個概觀,讓咱們再來看看服務器端的反向Ajax解決方案。到目前爲止,例子使用的都還主要是客戶端的JavaScript代碼。在服務器端,要接受反向Ajax鏈接的話,相比你所熟悉的短HTTP請求,某些技術須要特定的功能來處理長生存期的鏈接。爲了獲得更好的伸縮性,應該要使用一種新的線程模型,該模型須要Java中的某個特定API來暫停請求。還有,就WebSocket來講,你必需要正確地管理應用中用到的服務的做用域。

  線程和非阻塞I/O

  一般狀況下,web服務器會把一個線程或是一個進程與每一個傳入的HTTP鏈接關聯起來。這一鏈接能夠是持久的(保持活動),這樣多個請求就能夠經過這同一個鏈接進行了。在本文的例子中,Apache web服務器能夠配置成mpm_fork或是mpm_worker模式來改變這一行爲。Java web服務器(應用服務器也包括在內——這是同一回事)一般會爲每一個傳入的鏈接使用單獨的一個線程。

  產生一個新的線程會帶來內存的消耗和資源的浪費,由於其並不保證產生的線程會被用到。鏈接可能會創建起來,可是沒有來自客戶端或是服務器端的數據在發送。無論這一線程是否被用到,其都會消耗用於調度和上下文切換的內存和CPU資源。並且,在使用線程模式來配置服務器時,你一般須要配置一個線程池(設定處理傳入鏈接的線程的最大數目)。若是該值配置不當,值過小的話,你最終就會遭遇線程飢餓問題;請求就會一直處於等待狀態直到有線程可用來處理它們,在達到最大併發鏈接時,響應時間就會降低。另外一方面,配置一個高值則可會致使內存不足的異常,產生過多線程會消耗盡JVM的全部可用的堆,致使服務器崩潰。

  Java最近引入一個新的I/O API,其被稱爲非阻塞式的I/O。這一API使用一個選擇器來避免每次有新的HTTP鏈接在服務器端創建時都要綁定一個線程的作法,當有數據到來時,就會有一個事件被接收,接着某個線程就被分配來處理該請求。所以,這種作法被稱爲每一個請求一個線程(thread-per-request)模式。其容許web服務器,好比說WebSphere和Jetty等,使用固定數量的線程來容納並處理愈來愈多的用戶鏈接。在相同硬件配置的狀況下,在這一模式下運行的web服務器的伸縮性要比運行在每一個鏈接一個線程(thread-per-connection)模型下的好得多。

  在Philip McCarthy(Comet and Reverse Ajax的做者)的博客中,關於這兩種線程模式的可伸縮性有一個頗有意思的衡量基準(參見參考資料中的連接)。在圖2中,你會發現一樣的模式:在有太多鏈接時,線程模式會中止工做。

  圖2. 線程模式的衡量基準

  每一個鏈接一個線程模式(圖2中的Threads)一般會有一個更好的響應時間,由於全部的線程都已啓動、準備好且是等待中,但在鏈接的數目太高時,其會中止提供服務。在每一個請求一個線程模式(圖2中的Continuations)中,線程被用來爲到達的請求提供服務,鏈接則是經過一個NIO選擇器來處理。響應時間可能會較慢一些,但線程會回收再用,所以該方案在大容量鏈接方面有着更好的伸縮性。

  想要了解線程在幕後是如何工做的話,能夠把一個LEGO™積木塊想象成是選擇器,每次傳入的鏈接到達這一LEGO積木塊時,其由一個管腳來標識。LEGO積木塊/選擇器有着與鏈接數同樣多的管腳(同樣多的鍵)。那麼,只須要一個線程來等待新事件的發生,而後在這些管腳上遍歷就能夠了。當有事情發生時,選擇器線程從發生的事件中檢索出鍵值,而後就可使用一個線程來爲傳入的請求提供服務。

  「Rox Java NIO Tutorial」這一教程有很好的使用Java中的NIO的例子(參見參考資料)。

  有請求做用域的服務

  許多框架都提供了服務或是過濾器(filter)來處理到達servlet的web請求,例如,某個過濾器會:

  1. 把JDBC鏈接綁定到某個請求線程上,這樣整個請求就只用到一個鏈接。

  2. 在請求結束時提交所作的改變。

  另外一個例子是Google Guice(一個依賴注入庫)的Guice Servlet擴展。相似於Spring,Guice可把服務綁定在請求的做用域內,一個實例至多隻會爲每一個新請求建立一次(參閱參考資料得到更多信息)。

  一般的作法包括了使用用戶id來把從儲存庫中檢索出來的用戶對象緩存在請求中,用戶id則是取自集羣化的HTTP會話。在Google Guice中,你可能會有相似清單7中給出的代碼。

  清單7. 請求做用域的綁定

@Provides
@RequestScoped
Member member(AuthManager authManager,
MemberRepository memberRepository) {
  return  memberRepository.findById(authManager.getCurrentUserId());
}

  當某個member被注入到類中時,Guice會嘗試這從請求中獲取該對象,若是沒有找到的話,它就會執行儲存庫調用並把結果放在請求中。

  請求做用域可與除了WebSocket以外的其餘任何的反向Ajax解決方案一塊兒使用,任何其餘的依賴於HTTP請求的解決方案,不管是短的仍是長的生存期的均可以,每一個請求都會經過servlet分發系統,過濾器都會被執行。在完成一個暫停的(長生存其)HTTP請求時,你會在這一系列的後繼部分中瞭解到還有另外一種作法可以讓請求再次經過過濾器鏈。

  對於WebSocket來講,數據直接到達onMessage回調函數上,就像是在TCP套接口中的狀況那樣。不存在任何的HTTP請求送達這一數據,故也不存在獲取或是存放做用域對象的請求上下文。所以在onMessage回調中使用須要做用域對象的服務就會失敗。可下載源代碼中的guice-and-websocket例子說明了如何繞過這一限制,以便仍然可在onMessage回調中使用請求做用域對象。當你運行這一例子,並在網頁上點擊每一個按鈕來測試一個Ajax調用(有請求做用域的)、一個WebSocket調用和一個使用了模擬請求做用域的WebSocket調用時,你會獲得圖3所示的輸出。

  圖3. 使用了請求做用域服務的WebSocket處理程序

  在使用下面任一種技術時,你可能都會遇到這些問題:

  1. Spring

  2. Hibernate

  3. 任何其餘須要請求做用域或是每一請求模型的框架,好比說OpenSessionInViewFilter。

  4. 任何在過濾器的內部使用ThreadLocal這一設施來指定變量的做用域爲請求線程並在之後訪問這些變量的系統。

  Guice有一個優雅的解決方案,如清單8所示:

  清單8. 在WebSocket的onMessage回調中模擬一個請求做用域

//  在調用doWebSocketMethod時
// 保存到請求的引用
HttpServletRequest request = [...]
Map, Object> bindings =new HashMap, Object>();
// 我有一個服務須要一個請求來獲取會話
// 所以我提供一個請求,但你能夠提供任何其餘
// 可能須要的綁定
bindings.put(Key.get(HttpServletRequest.class), request);
ServletScopes.scopeRequest(new Callable() {
  @Override
  public Object call() throws Exception {
  // 調用你的儲存庫或是任何用到做用域對象的服務
    outbound.sendMessage([...]);
    return null;
  }
}, bindings).call();

  暫停長生存期請求

  若使用Comet的話,還有另外一障礙存在,那就是服務器端如何在不影響性能的狀況下暫停一個長生存期請求,而後在服務器端事件到來時儘量快地恢復並完成請求呢?

  很顯然,你不能簡單地讓請求和響應停在那裏,這會引起線程飢餓和高內存消耗。暫停非阻塞式的I/O中的一個長生存期請求,在Java中這須要一個特有的API。Servlet 3.0規範提供了這樣的一個API(參見本系列的第1部份內容)。清單9給出了一個例子。

  清單9. 使用Servlet 3.0來定義一個異步的servlet

<?xml  version="1.0" encoding="UTF-8"?>

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:j2ee="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml
/ns/j2ee/web-app_3.0.xsd">

<servlet>
<servlet-name>events</servlet-name>
<servlet-class>ReverseAjaxServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>

<servlet-mapping>
<servlet-name>events</servlet-name>
<url-pattern>/ajax</url-pattern>
</servlet-mapping>

</web-app>

  在定義了一個異步的servlet以後,你就可使用Servlet 3.0 API來掛起和恢復一個請求,如清單10所示:

  清單10. 掛起和恢復一個請求

AsyncContext asyncContext  = req.startAsync();
// 把asyncContext的引用保存在某處

// 而後在須要的時候,在另外一個線程中你能夠恢復並完成
HttpServletResponse req =
(HttpServletResponse) asyncContext.getResponse();
req.getWriter().write("data");
req.setContentType([...]);
asyncContext.complete();

  在Servlet 3.0以前,每一個容器都有着且如今仍有着本身的機制。Jetty的延續(continuation)就是一個頗有名的例子;Java中的許多反向Ajax庫都依賴於Jetty的continuation。其並不是什麼精彩絕倫的作法,也不須要你的應用運行在Jetty容器上。該API的聰明之處在於其可以檢測出你正在運行的容器,若是是運行在另外一個容器上,好比說Tomcat或是Grizzly,那麼若是Servlet 3.0 API可用的話,就回退到Servlet 3.0 API上。這對於Comet來講沒有問題,但若是你想要利用WebSocket的優點的話,目前別無選擇,只能使用容器特有的功能。

  Servlet 3.0規範尚未發佈,但許多容器都已經實現了這一API,由於這也是實施反向Ajax的一種標準作法。

  結束語

  WebSocket儘管存在一些不足之處,但倒是一個功能很是強大的反向Ajax解決方案。其目前還未在全部瀏覽器上實現,且若是沒有反向Ajax庫的幫助的話,在Java服務器端並不容易使用。由於你使用的不是標準的請求-響應風格,全部你不能依賴過濾器鏈的做用域執行。Comet和WebSocket須要服務器端的容器特定功能,所以在使用新出的容器時,你須要注意一下,它可能沒有作這方面的擴充。

  請繼續關注這一系列的第3部分,該部份內容將探討用於Comet和WebSocket的不一樣的服務器端API,你還可瞭解到Atomsphere,這是一個反向Ajax框架。

  下載

  描述        名稱         大小   下載方法

  文章的源代碼   reverse_ajaxpt2_source.zip 14KB   HTTP

  參考資料

  1. 「Start using HTML5 WebSockets today」(Nettuts+):重溫在PHP中如何運行一個WebSocket服務器,並考慮如何構建一個客戶端來經過WebSocket協議發送和接收消息。

  2. 「The WebSocket API」(W3C, July 2011):這一規範定義的API使得網頁可以使用WebSocket協議來和遠程主機進行雙向通訊。

  3. jWebSocket支持的瀏覽器:瞭解jWebSocket和Flash套接口橋所支持瀏覽器的各方面信息。

  4. 瞭解更多關於Servlet 3.0對異步處理的支持方面的內容。

  5. Philip McCarthy的博客上的文章Comet & Java: Threaded Vs Nonblocking I/O中有着更多的內容。

  6. The Rox Java NIO Tutorial這一教程收集了做者使用Java NIO庫的一些經驗,以及幾十條的訣竅、技巧、建議和充斥着互聯網的告誡作法。

  7. 在維基百科上了解這些內容:

  7.1 Ajax
  7.2 Reverse Ajax
  7.3 Comet
  7.4 WebSockets

  8. 「Exploring Reverse AJAX」(Google Maps .Net Control博客,2006年8月):得到一些關於反向Ajax技術的介紹說明。

  9. 「Cross-domain communications with JSONP, Part 1: Combine JSONP and jQuery to quickly build powerful mashups」(developerWorks, February 2009):瞭解如何把不起眼的跨域調用技術(JSONP)和一個靈活的JavaScript庫(JQuery)結合在一塊兒,以使人驚訝的速度構建出一些功能強大的聚合應用。

  10. 「Cross-Origin Resource Sharing (CORS)」規範(W3C, July 2010):瞭解更多關於這一機制的內容,該機制容許XHR執行跨域請求。

  11. 「Build Ajax applications with Ext JS」(developerWorks, July 2008):對大大加強了JavaScript開發的這一框架有一個大概的瞭解。

  12. 「Compare JavaScript frameworks」(developerWorks, February 2010):對極大地加強了JavaScript開發的那些框架有一個總體的瞭解。

  13. 「Mastering Ajax, Part 2: Make asynchronous requests with JavaScript and Ajax」(developerWorks, January 2006):學習如何使用Ajax和XMLHttpRequest對象來建立一種永不會讓用戶等待服務器響應的請求/響應模型。

  14. 「Create Ajax applications for the mobile Web」(developerWorks, March 2010):瞭解如何使用Ajax構建跨瀏覽器的智能手機Web應用。

  15. 「Where and when to use Ajax in your applications」(developerWorks, February 2008):瞭解如何使用Ajax來改進網站,同時避免糟糕的用戶體驗。

  16. 「Improve the performance of Web 2.0 applications「(developerWorks, December 2009):探討不一樣的瀏覽器端緩存機制。

  17. 「Introducing JSON」(JSON.org):得到對JSON語法的一個入門介紹。

  18. developerWorks Web development zone:得到各類談論基於Web的解決方案的文章。

  19. developerWorks podcasts:收聽各類與軟件開發者進行的有趣的訪談和討論。

  20. developerWorks technical events and webcasts:隨時關注developerWorks的技術事件和webcast的進展。

  獲取產品和技術

  1. WebSocketJS(WebSocket Flash Bridge):獲取這一由Flash支持的HTML5 WebSocket實現。

  2. Google Guice:獲取Google Guice,一個Java 5及以上版本的輕量級的依賴注入框架。

  3. Jetty:獲取Jetty,一個web服務器和javax.servlet容器,外帶對WebSocket的支持。

  4. Apache Maven:獲取Maven,一個軟件項目管理和包容工具。

  5. Java Development Kit, Version 6:得到Java平臺標準版(Java Platform, Standard Edition,Java SE),該平臺容許你在臺式機和服務器上,以及在當今要求苛刻的嵌入式環境上開發和部署Java應用。

  6. 免費試用IBM軟件,下載使用版,登陸在線試用,在沙箱環境中使用產品,或是經過雲來訪問,有超過100種IBM產品試用版選擇。

  討論

  1. 如今就建立你的developerWorks我的資料,並設置一個關於Reverse Ajax的觀看列表。與developerWorks社區創建聯繫並保持聯繫。

  2. 找到其餘在web開發方面感興趣的developerWorks成員

  3. 分享你的知識:加入一個關注web專題的developerWorks組

  4. Roland Barcia在他的博客中談論Web 2.0和中間件

  5. 關注developerWork成員的shared bookmarks on web topics

  6. 快速得到答案:訪問Web 2.0 Apps論壇

  7. 快速得到答案:訪問Ajax論壇

相關文章
相關標籤/搜索