轉載IBM開發社區
受異步服務器端事件驅動的 Ajax 應用程序實現較為困難,并且難于擴展。Philip McCarthy 在其廣受歡迎的 系列文章 中介紹了一種行之有效的方法:結合使用 Comet 模式(將數據推到客戶機)和 Jetty 6 的 Continuations API(將 Comet 應用程序擴展到大量客戶機中)。您可以方便地在 Direct Web Remoting
(DWR) 2 中將 Comet 和 Continuations 與 Reverse Ajax 技術結合使用。
作為一種廣泛使
用的 Web 應用程序開發技術,Ajax 牢固確立了自己的地位,隨之而來的是一些通用 Ajax 使用模式。例如,Ajax
經常用于對用戶輸入作出響應,然后使用從服務器獲得的新數據修改頁面的部分內容。但是,有時 Web
應用程序的用戶界面需要進行更新以響應服務器端發生的異步事件,而不需要用戶操作 —— 例如,顯示到達 Ajax
聊天應用程序的新消息,或者在文本編輯器中顯示來自另一個用戶的改變。由于只能由瀏覽器建立 Web 瀏覽器和服務器之間的 HTTP
連接,服務器無法在改動發生時將變化 “推送” 給瀏覽器。
Ajax 應用程序可以使用兩種基本的方法解決這一問題:一種方法是瀏覽器每隔若干秒時間向服務器發出輪詢以進行更新,另一種方法是服務器始終打開與瀏覽器的連接并在數據可用時發送給瀏覽器。長期連接技術被稱為 Comet(請參閱 參考資料)。本文將展示如何結合使用 Jetty servlet 引擎和 DWR 簡捷有效地實現一個 Comet Web 應用程序。
為什么使用 Comet?
輪
詢方法的主要缺點是:當擴展到更多客戶機時,將生成大量的通信量。每個客戶機必須定期訪問服務器以檢查更新,這為服務器資源添加了更多負荷。最壞的一種情
況是對不頻繁發生更新的應用程序使用輪詢,例如一種 Ajax 郵件
Inbox。在這種情況下,相當數量的客戶機輪詢是沒有必要的,服務器對這些輪詢的回答只會是
“沒有產生新數據”。雖然可以通過增加輪詢的時間間隔來減輕服務器負荷,但是這種方法會產生不良后果,即延遲客戶機對服務器事件的感知。當然,很多應用程
序可以實現某種權衡,從而獲得可接受的輪詢方法。
盡管如此,吸引人們使用 Comet
策略的其中一個優點是其顯而易見的高效性??蛻魴C不會像使用輪詢方法那樣生成煩人的通信量,并且事件發生后可立即發布給客戶機。但是保持長期連接處于打開
狀態也會消耗服務器資源。當等待狀態的 servlet 持有一個持久性請求時,該 servlet 會獨占一個線程。這將限制 Comet 對傳統
servlet 引擎的可伸縮性,因為客戶機的數量會很快超過服務器棧能有效處理的線程數量。
Jetty 6 有何不同
Jetty 6 的目的是擴展大量同步連接,使用 Java™ 語言的非阻塞 I/O(java.nio
)庫并使用一個經過優化的輸出緩沖架構(參閱 參考資料)。Jetty 還為處理長期連接提供了一些技巧:該特性稱為 Continuations。
我將使用一個簡單的 servlet 對 Continuations 進行演示,這個 servlet
將接受請求,等待處理,然后發送響應。接下來,我將展示當客戶機數量超過服務器提供的處理線程后發生的狀況。最后,我將使用
Continuations 重新實現 servlet,您將了解 Continuations 在其中扮演的角色。
為了便于理解下面的示例,我將把 Jetty servlet 引擎限制在一個單請求處理線程。清單 1 展示了 jetty.xml 中的相關配置。我實際上需要在 ThreadPool
使用三個線程:Jetty 服務器本身使用一個線程,另一線程運行 HTTP 連接器,偵聽到來的請求。第三個線程執行 servlet 代碼。
清單 1. 單個 servlet 線程的 Jetty 配置
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure id="Server" class="org.mortbay.jetty.Server">
<Set name="ThreadPool">
<New class="org.mortbay.thread.BoundedThreadPool">
<Set name="minThreads">3</Set>
<Set name="lowThreads">0</Set>
<Set name="maxThreads">3</Set>
</New>
</Set>
</Configure>
|
接下來,為了模擬對異步事件的等待,清單 2 展示了 BlockingServlet
的 service()
方法,該方法將使用 Thread.sleep()
調用在線程結束之前暫停 2000 毫秒的時間。它還在執行開始和結束時輸出系統時間。為了區別輸出和不同的請求,還將作為標識符的請求參數記錄在日志中。
清單 2. BlockingServlet
public class BlockingServlet extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {
String reqId = req.getParameter("id");
res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"\tstart:\t" + new Date());
res.getWriter().flush();
try {
Thread.sleep(2000);
} catch (Exception e) {}
res.getWriter().println("Request: "+reqId+"\tend:\t" + new Date());
}
}
|
現在可以觀察到 servlet 響應一些同步請求的行為。清單 3 展示了控制臺輸出,五個使用 lynx
的并行請求。命令行啟動五個 lynx
進程,將標識序號附加在請求 URL 的后面。
清單 3. 對 BlockingServlet 并發請求的輸出
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/blocking?id=$i & done
Request: 1 start: Sun Jul 01 12:32:29 BST 2007
Request: 1 end: Sun Jul 01 12:32:31 BST 2007
Request: 2 start: Sun Jul 01 12:32:31 BST 2007
Request: 2 end: Sun Jul 01 12:32:33 BST 2007
Request: 3 start: Sun Jul 01 12:32:33 BST 2007
Request: 3 end: Sun Jul 01 12:32:35 BST 2007
Request: 4 start: Sun Jul 01 12:32:35 BST 2007
Request: 4 end: Sun Jul 01 12:32:37 BST 2007
Request: 5 start: Sun Jul 01 12:32:37 BST 2007
Request: 5 end: Sun Jul 01 12:32:39 BST 2007
|
清單 3 中的輸出和預期一樣。因為 Jetty 只可以使用一個線程執行 servlet 的 service()
方法。Jetty 對請求進行排列,并按順序提供服務。當針對某請求發出響應后將立即顯示時間戳(一個 end
消息),servlet 接著處理下一個請求(后續的 start
消息)。因此即使同時發出五個請求,其中一個請求必須等待 8 秒鐘的時間才能接受 servlet 處理。
請注意,當 servlet 被阻塞時,執行任何操作都無濟于事。這段代碼模擬了請求等待來自應用程序不同部分的異步事件。這里使用的服務器既不是 CPU 密集型也不是 I/O 密集型:只有線程池耗盡之后才會對請求進行排隊。
現在,查看 Jetty 6 的 Continuations 特性如何為這類情形提供幫助。清單 4 展示了 清單 2 中使用 Continuations API 重寫后的 BlockingServlet
。我將稍后解釋這些代碼。
清單 4. ContinuationServlet
public class ContinuationServlet extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {
String reqId = req.getParameter("id");
Continuation cc = ContinuationSupport.getContinuation(req,null);
res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"\tstart:\t"+new Date());
res.getWriter().flush();
cc.suspend(2000);
res.getWriter().println("Request: "+reqId+"\tend:\t"+new Date());
}
}
|
清單 5 展示了對 ContinuationServlet
的五個同步請求的輸出;請與 清單 3 進行比較。
清單 5. 對 ContinuationServlet 的五個并發請求的輸出
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/continuation?id=$i & done
Request: 1 start: Sun Jul 01 13:37:37 BST 2007
Request: 1 start: Sun Jul 01 13:37:39 BST 2007
Request: 1 end: Sun Jul 01 13:37:39 BST 2007
Request: 3 start: Sun Jul 01 13:37:37 BST 2007
Request: 3 start: Sun Jul 01 13:37:39 BST 2007
Request: 3 end: Sun Jul 01 13:37:39 BST 2007
Request: 2 start: Sun Jul 01 13:37:37 BST 2007
Request: 2 start: Sun Jul 01 13:37:39 BST 2007
Request: 2 end: Sun Jul 01 13:37:39 BST 2007
Request: 5 start: Sun Jul 01 13:37:37 BST 2007
Request: 5 start: Sun Jul 01 13:37:39 BST 2007
Request: 5 end: Sun Jul 01 13:37:39 BST 2007
Request: 4 start: Sun Jul 01 13:37:37 BST 2007
Request: 4 start: Sun Jul 01 13:37:39 BST 2007
Request: 4 end: Sun Jul 01 13:37:39 BST 2007
|
清單 5 中有兩處需要重點注意。首先,每個 start
消息出現兩次;先不要著急。其次,更重要的一點,請求現在不需排隊就能夠并發處理,注意所有 start
和 end
消息的時間戳是相同的。因此,每個請求的處理時間不會超過兩秒,即使只運行一個 servlet 線程。
Jetty Continuations 機制原理
理解了 Jetty Continuations 機制的實現原理,您就能夠解釋 清單 5 中的現象。要使用 Continuations,必須對 Jetty 進行配置,以使用其 SelectChannelConnector
處理請求。這個連接器構建在 java.nio
API 之上,因此使它能夠不用消耗每個連接的線程就可以持有開放的連接。當使用 SelectChannelConnector
時,ContinuationSupport.getContinuation()
將提供一個 SelectChannelConnector.RetryContinuation
實例。(然而,您應該只針對 Continuation
接口進行編碼;請參閱 Portability and the Continuations API。)當對 RetryContinuation
調用 suspend()
時,它將拋出一個特殊的運行時異常 —— RetryRequest
—— 該異常將傳播到 servlet 以外并通過過濾器鏈傳回,并由 SelectChannelConnector
捕獲。 但是發生該異常之后并沒有將響應發送給客戶機,請求被放到處于等待狀態的 Continuation
隊列中,而 HTTP 連接仍然保持打開狀態。此時,為該請求提供服務的線程將返回 ThreadPool
,用以為其他請求提供服務。
 |
可移植性和 Continuations API
我提到過應該使用 Jetty 的 SelectChannelConnector 來啟用 Continuations 功能。然而,Continuations API 仍然可用于傳統的 SocketConnector ,這種情況下 Jetty 將回退到不同的 Continuation 實現,該實現使用 wait()/notify() 方法。您的代碼仍然可以編譯和運行,但是卻失去了非阻塞 Continuations 的優點。如果您希望繼續使用非 Jetty 服務器,您應該考慮編寫自己的 Continuation 包裝器,在運行時期使用反射檢查 Jetty Continuations 庫是否可用。DWR 就使用了這種策略。
|
|
暫停的請求將一直保持在等待狀態的 Continuation
隊列,直到超出指定的時限,或者當對 resume()
方法的 Continuation
調用 resume()
時(稍后將詳細介紹)。出現上述任意一種條件時,請求將被重新提交到 servlet(通過過濾器鏈)。事實上,整個請求被重新進行處理,直到首次調用 suspend()
。當執行第二次發生 suspend()
調用時,RetryRequest
異常不會被拋出,執行照常進行。
現在應該可以解釋 清單 5 中的輸出了。每個請求依次進入 servlet 的 service()
方法后,將發送 start
消息進行響應,Continuation
的 suspend()
方法引發 servlet 異常,將釋放線程使其處理下一個請求。所有五個請求快速通過 service()
方法的第一部分,并進入等待狀態,并且所有 start
消息將在幾毫秒內輸出。兩秒后,當超過 suspend()
的時限后,將從等待隊列中檢索第一個請求,并將其重新提交給 ContinuationServlet
。第二次輸出 start
消息,立即返回對 suspend()
的第二次調用,并且發送 end
消息進行響應。然后將在此執行 servlet 代碼來處理隊列中的下一個請求,以此類推。
因此,在 BlockingServlet
和 ContinuationServlet
兩種情況中,請求被放入隊列中以訪問單個 servlet 線程。然而,雖然 servlet 線程執行期間 BlockingServlet
發生兩秒暫停,SelectChannelConnector
中的 ContinuationServlet
的暫停發生在 servlet 之外。ContinuationServlet
的總吞吐量更高一些,因為 servlet 線程沒有將大部分時間用在 sleep()
調用中。
使 Continuations 變得有用
現在您已經了解到 Continuations 能夠不消耗線程就可以暫停 servlet 請求,我需要進一步解釋 Continuations API 以向您展示如何在實際應用中使用。
resume()
方法生成一對 suspend()
??梢詫⑺鼈円暈闃藴实?Object wait()
/notify()
機制的 Continuations 等價體。就是說,suspend()
使 Continuation
(因此也包括當前方法的執行)處于暫停狀態,直到超出時限,或者另一個線程調用 resume()
。suspend()
/resume()
對于實現真正使用 Continuations 的 Comet 風格的服務非常關鍵。其基本模式是:從當前請求獲得 Continuation
,調用 suspend()
,等待異步事件的到來。然后調用 resume()
并生成一個響應。
然而,與 Scheme 這種語言中真正的語言級別的 continuations 或者是 Java 語言的 wait()
/notify()
范例不同的是,對 Jetty Continuation
調用 resume()
并不意味著代碼會從中斷的地方繼續執行。正如您剛剛看到的,實際上和 Continuation
相關的請求被重新處理。這會產生兩個問題:重新執行 清單 4 中的 ContinuationServlet
代碼,以及丟失狀態:即調用 suspend()
時丟失作用域內所有內容。
第一個問題的解決方法是使用 isPending()
方法。如果 isPending()
返回值為 true,這意味著之前已經調用過一次 suspend()
,而重新執行請求時還沒有發生第二次 suspend()
調用。換言之,根據 isPending()
條件在執行 suspend()
調用之前運行代碼,這樣將確保對每個請求只執行一次。在 suspend()
調用具有等冪性之前,最好先對應用程序進行設計,這樣即使調用兩次也不會出現問題,但是某些情況下無法使用 isPending()
方法。Continuation
也提供了一種簡單的機制來保持狀態:putObject(Object)
和 getObject()
方法。在 Continuation
發生暫停時,使用這兩種方法可以保持上下文對象以及需要保存的狀態。您還可以使用這種機制作為在線程之間傳遞事件數據的方式,稍后將演示這種方法。
編寫基于 Continuations 的應用程序
作
為實際示例場景,我將開發一個基本的 GPS 坐標跟蹤 Web
應用程序。它將在不規則的時間間隔內生成隨機的經緯度值對。發揮一下想象力,生成的坐標值可能就是臨近的一個公共車站、隨身攜帶著 GPS
設備的馬拉松選手、汽車拉力賽中的汽車或者運輸中的包裹。令人感興趣的是我將如何告訴瀏覽器這個坐標。圖 1 展示了這個簡單的 GPS 跟蹤器應用程序的類圖:
圖 1. 顯示 GPS 跟蹤器應用程序主要組件的類圖
首先,應用程序需要某種方法來生成坐標。這將由 RandomWalkGenerator
完成。從一對初始坐標對開始,每次調用它的私有 generateNextCoord()
方法時,將從該位置移動隨機指定的距離,并將新的位置作為 GpsCoord
對象返回。初始化完成后,RandomWalkGenerator
將生成一個線程,該線程以隨機的時間間隔調用 generateNextCoord()
方法并將生成的坐標發送給任何注冊了 addListener()
的 CoordListener
實例。清單 6 展示了 RandomWalkGenerator
循環的邏輯:
清單 6. RandomWalkGenerator's run() 方法
public void run() {
try {
while (true) {
int sleepMillis = 5000 + (int)(Math.random()*8000d);
Thread.sleep(sleepMillis);
dispatchUpdate(generateNextCoord());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
|
CoordListener
是一個回調接口,僅僅定義 onCoord(GpsCoord coord)
方法。在本例中,ContinuationBasedTracker
類實現 CoordListener
。ContinuationBasedTracker
的另一個公有方法是 getNextPosition(Continuation, int)
。清單 7 展示了這些方法的實現:
清單 7. ContinuationBasedTracker 結構
public GpsCoord getNextPosition(Continuation continuation, int timeoutSecs) {
synchronized(this) {
if (!continuation.isPending()) {
pendingContinuations.add(continuation);
}
// Wait for next update
continuation.suspend(timeoutSecs*1000);
}
return (GpsCoord)continuation.getObject();
}
public void onCoord(GpsCoord gpsCoord) {
synchronized(this) {
for (Continuation continuation : pendingContinuations) {
continuation.setObject(gpsCoord);
continuation.resume();
}
pendingContinuations.clear();
}
}
|
當客戶機使用 Continuation
調用 getNextPosition()
時,isPending
方法將檢查此時的請求是否是第二次執行,然后將它添加到等待坐標的 Continuation
集合中。然后該 Continuation
被暫停。同時,onCoord
—— 生成新坐標時將被調用 —— 循環遍歷所有處于等待狀態的 Continuation
,對它們設置 GPS 坐標,并重新使用它們。之后,每個再次執行的請求完成 getNextPosition()
執行,從 Continuation
檢索 GpsCoord
并將其返回給調用者。注意此處的同步需求,是為了保護 pendingContinuations
集合中的實例狀態不會改變,并確保新增的 Continuation
在暫停之前沒有被處理過。
最后一個難點是 servlet 代碼本身,如 清單 8 所示:
清單 8. GPSTrackerServlet 實現
public class GpsTrackerServlet extends HttpServlet {
private static final int TIMEOUT_SECS = 60;
private ContinuationBasedTracker tracker = new ContinuationBasedTracker();
public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {
Continuation c = ContinuationSupport.getContinuation(req,null);
GpsCoord position = tracker.getNextPosition(c, TIMEOUT_SECS);
String json = new Jsonifier().toJson(position);
res.getWriter().print(json);
}
}
|
如您所見,servlet 只執行了很少的工作。它僅僅獲取了請求的 Continuation
,調用 getNextPosition()
,將 GPSCoord
轉換成 JavaScript Object Notation (JSON),然后輸出。這里不需要防止重新執行,因此我不必檢查 isPending()
。清單 9 展示了調用 GpsTrackerServlet
的輸出,同樣,有五個同步請求而服務器只有一個可用線程:
Listing 9. Output of GPSTrackerServlet
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/tracker & done
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
|
這個示例并不引人注意,但是提供了概念證明。發出請求后,它們將一直保持打開的連接直至生成坐標,此時將快速生成響應。這是 Comet 模式的基本原理,Jetty 使用這種原理在一個線程內處理 5 個并發請求,這都是 Continuations 的功勞。
創建一個 Comet 客戶機
現在您已經了解了如何使用 Continuations 在理論上創建非阻塞 Web 服務,您可能想知道如何創建客戶端代碼來使用這種功能。一個 Comet 客戶機需要完成以下功能:
- 保持打開
XMLHttpRequest
連接,直到收到響應。
- 將響應發送到合適的 JavaScript 處理程序。
- 立即建立新的連接。
更高級的 Comet 設置將使用一個連接將數據從不同服務推入瀏覽器,并且客戶機和服務器配有相應的路由機制。一種可行的方法是根據一種 JavaScript 庫,例如 Dojo,編寫客戶端代碼,這將提供基于 Comet 的請求機制,其形式為
dojo.io.cometd
。
然而,如果服務器使用 Java 語言,使用 DWR 2 可以同時在客戶機和服務器上獲得 Comet 高級支持,這是一種不錯的方法(參閱 參考資料)。如果您并不了解 DWR 的話,請參閱本系列第 3 部分 “結合 Direct Web Remoting 使用 Ajax”。DWR 透明地提供了一種 HTTP-RPC 傳輸層,將您的 Java 對象公開給網絡中 JavaScript 代碼的調用。DWR 生成客戶端代理,將自動封送和解除封送數據,處理安全問題,提供方便的客戶端實用工具庫,并可以在所有主要瀏覽器上工作。
DWR 2: Reverse Ajax
DWR 2 最新引入了 Reverse Ajax 概念。這種機制可以將服務器端事件 “推入” 到客戶機??蛻舳?DWR 代碼透明地處理已建立的連接并解析響應,因此從開發人員的角度來看,事件是從服務器端 Java 代碼輕松地發布到客戶機中。
DWR 經過配置之后可以使用 Reverse Ajax 的三種不同機制。第一種就是較為熟悉的輪詢方法。第二種稱為 piggyback,
這種機制并不創建任何到服務器的連接,相反,將一直等待直至發生另一個 DWR 服務,piggybacks
使事件等待該請求的響應。這使它具有較高的效率,但也意味著客戶機事件通知被延遲到直到發生另一個不相關的客戶機調用。最后一種機制使用長期的、
Comet 風格的連接。最妙的是,當運行在 Jetty 下時,DWR 能夠自動檢測并切換為使用 Contiuations,實現非阻塞
Comet。
我將在 GPS 示例中結合使用 Reverse Ajax 和 DWR 2。通過這種演示,您將對 Reverse Ajax 的工作原理有更多的了解。
此時不再需要使用 servlet。DWR 提供了一個控制器 servlet,它將在 Java 對象之上直接轉交客戶機請求。同樣也不需要顯式地處理 Continuations,因為 DWR 將在內部進行處理。因此我只需要一個新的 CoordListener
實現,將坐標更新發布到到任何客戶機瀏覽器上。
ServerContext
接口提供了 DWR 的 Reverse Ajax 功能。ServerContext
可以察覺到當前查看給定頁面的所有 Web 客戶機,并提供一個 ScriptSession
進行相互通信。ScriptSession
用于從 Java 代碼將 JavaScript 片段推入到客戶機。清單 10 展示了 ReverseAjaxTracker
響應坐標通知的方式,并使用它們生成對客戶端 updateCoordinate()
函數的調用。注意對 DWR ScriptBuffer
對象調用 appendData()
將自動把 Java 對象封送給 JSON(如果使用合適的轉換器)。
清單 10. ReverseAjaxTracker 中的通知回調方法
public void onCoord(GpsCoord gpsCoord) {
// Generate JavaScript code to call client-side
// function with coord data
ScriptBuffer script = new ScriptBuffer();
script.appendScript("updateCoordinate(")
.appendData(gpsCoord)
.appendScript(");");
// Push script out to clients viewing the page
Collection<ScriptSession> sessions =
sctx.getScriptSessionsByPage(pageUrl);
for (ScriptSession session : sessions) {
session.addScript(script);
}
}
|
接下來,必須對 DWR 進行配置以感知 ReverseAjaxTracker
的存在。在大型應用程序中,可以使用 DWR 的 Spring 集成提供 Spring 生成的 bean。但是,在本例中,我僅使用 DWR 創建了一個 ReverseAjaxTracker
新實例并將其放到 application
范圍中。所有后續請求將訪問這個實例。
我還需告訴 DWR 如何將數據從 GpsCoord
beans 封送到 JSON。由于 GpsCoord
是一個簡單對象,DWR 的基于反射的 BeanConverter
就可以完成此功能。清單 11 展示了 ReverseAjaxTracker
的配置:
清單 11. ReverseAjaxTracker 的 DWR 配置
<dwr>
<allow>
<create creator="new" javascript="Tracker" scope="application">
<param name="class" value="developerworks.jetty6.gpstracker.ReverseAjaxTracker"/>
</create>
<convert converter="bean" match="developerworks.jetty6.gpstracker.GpsCoord"/>
</allow>
</dwr>
|
create
元素的 javascript
屬性指定了 DWR 用于將跟蹤器公開為 JavaScript 對象的名字,在本例中,我的客戶端代碼沒有使用該屬性,而是將數據從跟蹤器推入到其中。同樣
,還需對 web.xml 進行額外的配置,以針對 Reverse Ajax 配置 DWR,如 清單 12 所示:
清單 12. DwrServlet 的 web.xml 配置
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>
org.directwebremoting.servlet.DwrServlet
</servlet-class>
<init-param>
<param-name>activeReverseAjaxEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>initApplicationScopeCreatorsAtStartup</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
|
第一個 servlet init-param
,activeReverseAjaxEnabled
將激活輪詢和 Comet 功能。第二個 initApplicationScopeCreatorsAtStartup
通知 DWR 在應用程序啟動時初始化 ReverseAjaxTracker
。這將在對 bean 生成第一個請求時改寫延遲初始化(lazy initialization)的常規行為 —— 在本例中這是必須的,因為客戶機不會主動對 ReverseAjaxTracker
調用方法。
最后,我需要實現調用自 DWR 的客戶端 JavaScript 函數。將向回調函數 —— updateCoordinate()
—— 傳遞 GpsCoord
Java bean 的 JSON 表示,由 DWR 的 BeanConverter
自動序列化。該函數將從坐標中提取 longitude
和 latitude
字段,并通過調用 Document Object Model (DOM) 將它們附加到列表中。清單 13 展示了這一過程,以及頁面的 onload
函數。onload
包含對 dwr.engine.setActiveReverseAjax(true)
的調用,將通知 DWR 打開與服務器的持久連接并等待回調。
清單 13. 簡單 Reverse Ajax GPS 跟蹤器的客戶端實現
window.onload = function() {
dwr.engine.setActiveReverseAjax(true);
}
function updateCoordinate(coord) {
if (coord) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(
coord.longitude + ", " + coord.latitude)
);
document.getElementById("coords").appendChild(li);
}
}
|
 |
不使用 JavaScript 更新頁面
如果希望最小化應用程序中使用的 JavaScript 代碼的數量,可以使用 ScriptSession 編寫 JavaScript 回調:將 ScriptSession 實例封裝在 DWR Util 對象中。該類將提供直接操作瀏覽器 DOM 的簡單 Java 方法,并在后臺自動生成所需的腳本。
|
|
現在我可以將瀏覽器指向跟蹤器頁面,DWR 將在生成坐標數據時把數據推入客戶機。該實現輸出生成坐標的列表,如 圖 2 所示:
圖 2. ReverseAjaxTracker 的輸出
可以看到,使用 Reverse Ajax 創建事件驅動的 Ajax 應用程序非常簡單。請記住,正是由于 DWR 使用了 Jetty Continuations,當客戶機等待新事件到來時不會占用服務器上面的線程。
此時,集成來自 Yahoo! 或 Google 的地圖部件非常簡單。通過更改客戶端回調,可輕松地將坐標傳送到地圖 API,而不是直接附加到頁面中。圖 3 展示了 DWR Reverse Ajax GPS 跟蹤器在此類地圖組件上標繪隨機路線:
Figure 3. 具有地圖 UI 的 ReverseAjaxTracker
結束語
通
過本文,您了解了如何結合使用 Jetty Continuations 和 Comet 為事件驅動 Ajax
應用程序提供高效的可擴展解決方案。我沒有給出 Continuations
可擴展性的具體數字,因為實際應用程序的性能取決于多種變化的因素。服務器硬件、所選擇的操作系統、JVM 實現、Jetty
配置以及應用程序的設計和通信量配置文件都會影響 Jetty Continuations 的性能。然而,Webtide 的 Greg
Wilkins(主要的 Jetty 開發人員)曾經發布了一份關于 Jetty 6 的白皮書,對使用 Continuations 和沒有使用
Continuations 的 Comet 應用程序的性能進行了比較,該程序同時處理 10000 個并發請求(參閱 參考資料)。在 Greg 的測試中,使用 Continuations 能夠減少線程消耗,并同時減少了超過 10 倍的棧內存消耗。
您
還看到了使用 DWR 的 Reverse Ajax 技術實現事件驅動 Ajax 應用程序是多么簡單。DWR
不僅省去了大量客戶端和服務器端編碼,而且 Reverse Ajax 還從代碼中將完整的服務器-推送機制抽象出來。通過更改 DWR
的配置,您可以自由地在 Comet、輪詢,甚至是 piggyback
方法之間進行切換。您可以對此進行實驗,并找到適合自己應用程序的最佳性能策略,同時不會影響到自己的代碼。
如果希望對自己的 Reverse Ajax 應用程序進行實驗,下載并研究 DWR 演示程序的代碼(DWR 源代碼發行版的一部分,參閱 參考資源)將非常有幫助。如果希望親自運行示例,還可獲得本文使用的示例代碼(參見 下載)。
下載
描述 | 名字 | 大小 | 下載方法 |
示例代碼 |
jetty-dwr-comet-src.tgz |
8KB |
HTTP |
參考資料
學習
獲得產品和技術
討論
查看
developerWorks blogs,加入
developerWorks 社區。