天天看點

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

英文原文: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 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。

  請繼續閱讀這一系列的第2部分:該部分将會探讨第三種反向Ajax技術:WebSocket。盡管還不是所有的浏覽器都支援該技術,但WebSocket肯定是一種非常好的反向Ajax通信媒介,WebSocket消除了所有與HTTP連接配接的無狀态特性相關的限制。第2部分還會談及由Comet和WebSocket技術帶來的伺服器端限制。

  代碼下載下傳

  reverse_ajaxpt1_source.zip

  參考資料

  學習資料

  1. 在維基百科上了解這些内容:

  1.1 Ajax

  1.2 Reverse Ajax

  1.3 Comet

  1.4 WebSockets

  2. “Exploring Reverse AJAX”(Google Maps .Net Control部落格,2006年8月):獲得一些關于反向Ajax技術的介紹說明。

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

  4. “Cross-Origin Resource Sharing (CORS)”規範(W3C, July 2010):了解更多關于這一機制的内容,該機制允許XHR執行跨域請求。

  5. “Build Ajax applications with Ext JS”(developerWorks, July 2008):對大大增強了JavaScript開發的這一架構有一個大概的了解。

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

  7. “Create Ajax applications for the mobile Web”(developerWorks, March 2010):了解如何使用Ajax建構跨浏覽器的智能手機Web應用。

  8. “Improve the performance of Web 2.0 applications“(developerWorks, December 2009):探讨不同的浏覽器端緩存機制。

  9. “Introducing JSON”(JSON.org):獲得對JSON文法的一個入門介紹。

  10. developerWorks Web development zone:獲得各種談論基于Web的解決方案的文章。

  11. developerWorks podcasts:收聽各種與軟體開發者進行的有趣的訪談和讨論。

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

  擷取産品和技術

  1. 擷取ExtJS,這是一個用來建構富網際網路應用的跨浏覽器JavaScript庫。

  2. XAMPP為Apache、PHP、MySQL和其他産品提供了一種簡易的安裝。

  3. 免費試用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論壇。

  關于作者

  Mathieu Carbou是Ovea的一位提供服務和開發解決方案的Java web架構師和顧問。他是幾個開源項目的送出者和上司者,也是Montreal的Java User Group的一位演講者和上司者。Mathieu有着很強的代碼設計和最佳實踐背景,他是一個從用戶端到後端的事件驅動的web開發方面的專家。他的工作重點是在高度可伸縮的web應用中提供事件驅動的和消息式的解決方案。你可以看一看他的部落格。