反向Ajax,第1部分:Comet介紹

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

前言web


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

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

Ajax、反向Ajax和WebSocket後端


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

清單1. Ajax請求舉例瀏覽器

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中看到這一例子的做用。app

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

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

反向Ajax技術


反向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 05 15:17:14 EDT 2011 
[event] At Sun Jun 05 15:17:14 EDT 2011 
[client] checking for events... 
[client] 1 events 
[event] At Sun Jun 05 15: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 05 16:08:32 EDT 2011 
[event] At Sun Jun 05 16:08:34 EDT 2011 
[event] At Sun Jun 05 16:08:34 EDT 2011 
[event] At Sun Jun 05 16: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標籤來處理的,所以你沒有辦法知道鏈接什麼時候在哪一端已被斷開了。

多部分的XMLHttpRequest 

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

清單6. 設置多部分流請求的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。

請繼續閱讀這一系列的第2部分:該部分將會探討第三種反向Ajax技術:WebSocket。儘管還不是全部的瀏覽器都支持該技術,但WebSocket確定是一種很是好的反向Ajax通訊媒介,WebSocket消除了全部與HTTP鏈接的無狀態特性相關的限制。第2部分還會談及由Comet和WebSocket技術帶來的服務器端約束。

相關文章
相關標籤/搜索