英文原文: Reverse Ajax, Part 1: Introduction to Comet 在過去的幾年中,web開發已經發生了很大的變化。現如今,我們期望的是能夠通過web快速、動態地訪問應用。在這一新的文章系列中,我們學習如何使用反向Ajax(Reverse Ajax)技術來開發事件驅動的web應用,以此來實現更好的使用者體驗。用戶端的例子使用的是JQuery JavaScript庫,在這首篇文章中,我們探索不同的反向Ajax技術,使用可下載的例子來學習使用了流(streaming)方法和長輪詢(long polling)方法的Comet。
前言
web開發在過去的幾年中有了很大的進展,我們已經遠超了把靜態網頁連結在一起的做法,這種做法會引起瀏覽器的重新整理,並且要等待頁面的載入。現在需要的是能夠通過web來訪問的完全動態應用,這些應用通常需要儘可能的快,提供近乎即時的組件。在這一新的由五部分組成的文章系列中,我們學習如何使用反向Ajax(Reverse Ajax)技術來開發事件驅動的web應用。
在這第一篇文章中,我們要瞭解反向Ajax、輪詢(polling)、流(streaming)、Comet和長輪詢(long polling),學習如何?不同的反向Ajax通訊技術,並探討每種方法的優點和缺點。你可以下載本文中例子的相應原始碼。
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中看到這一例子的作用。
反向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標籤來處理的,因此你沒有辦法知道串連何時在哪一端已被斷開了。
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。
請繼續閱讀這一系列的第2部分:該部分將會探討第三種反向Ajax技術:WebSocket。儘管還不是所有的瀏覽器都支援該技術,但WebSocket肯定是一種非常好的反向Ajax通訊媒介,WebSocket消除了所有與HTTP串連的無狀態特性相關的限制。第2部分還會談及由Comet和WebSocket技術帶來的伺服器端約束。
代碼下載
reverse_ajaxpt1_source.zip