這一文章系列探討了如何使用反向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 HandshakeRequest 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. 如果用戶端處在某個Proxy 伺服器的後面的話,到連接埠843的串連可能會被拒絕。
WebSocketJS項目提供了一種橋接方式,其要求一個至少是10版本的Flash來為Firefox 3、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