2007 年 8 月 02 日 受異步服務器端事件驅動的 Ajax 應用程序實現(xiàn)較為困難,并且難于擴展。Philip McCarthy 在其廣受歡迎的 系列文章 中介紹了一種行之有效的方法:結合使用 Comet 模式(將數(shù)據(jù)推到客戶機)和 Jetty 6 的 Continuations API(將 Comet 應用程序擴展到大量客戶機中)。您可以方便地在 Direct Web Remoting (DWR) 2 中將 Comet 和 Continuations 與 Reverse Ajax 技術結合使用。 作為一種廣泛使用的 Web 應用程序開發(fā)技術,Ajax 牢固確立了自己的地位,隨之而來的是一些通用 Ajax 使用模式。例如,Ajax 經(jīng)常用于對用戶輸入作出響應,然后使用從服務器獲得的新數(shù)據(jù)修改頁面的部分內容。但是,有時 Web 應用程序的用戶界面需要進行更新以響應服務器端發(fā)生的異步事件,而不需要用戶操作 —— 例如,顯示到達 Ajax 聊天應用程序的新消息,或者在文本編輯器中顯示來自另一個用戶的改變。由于只能由瀏覽器建立 Web 瀏覽器和服務器之間的 HTTP 連接,服務器無法在改動發(fā)生時將變化 “推送” 給瀏覽器。 Ajax 應用程序可以使用兩種基本的方法解決這一問題:一種方法是瀏覽器每隔若干秒時間向服務器發(fā)出輪詢以進行更新,另一種方法是服務器始終打開與瀏覽器的連接并在數(shù)據(jù)可用時發(fā)送給瀏覽器。長期連接技術被稱為 Comet(請參閱 參考資料)。本文將展示如何結合使用 Jetty servlet 引擎和 DWR 簡捷有效地實現(xiàn)一個 Comet Web 應用程序。 為什么使用 Comet? 輪詢方法的主要缺點是:當擴展到更多客戶機時,將生成大量的通信量。每個客戶機必須定期訪問服務器以檢查更新,這為服務器資源添加了更多負荷。最壞的一種情況是對不頻繁發(fā)生更新的應用程序使用輪詢,例如一種 Ajax 郵件 Inbox。在這種情況下,相當數(shù)量的客戶機輪詢是沒有必要的,服務器對這些輪詢的回答只會是 “沒有產(chǎn)生新數(shù)據(jù)”。雖然可以通過增加輪詢的時間間隔來減輕服務器負荷,但是這種方法會產(chǎn)生不良后果,即延遲客戶機對服務器事件的感知。當然,很多應用程序可以實現(xiàn)某種權衡,從而獲得可接受的輪詢方法。 盡管如此,吸引人們使用 Comet 策略的其中一個優(yōu)點是其顯而易見的高效性??蛻魴C不會像使用輪詢方法那樣生成煩人的通信量,并且事件發(fā)生后可立即發(fā)布給客戶機。但是保持長期連接處于打開狀態(tài)也會消耗服務器資源。當?shù)却隣顟B(tài)的 servlet 持有一個持久性請求時,該 servlet 會獨占一個線程。這將限制 Comet 對傳統(tǒng) servlet 引擎的可伸縮性,因為客戶機的數(shù)量會很快超過服務器棧能有效處理的線程數(shù)量。
Jetty 6 有何不同 Jetty 6 的目的是擴展大量同步連接,使用 Java? 語言的非阻塞 I/O(java.nio )庫并使用一個經(jīng)過優(yōu)化的輸出緩沖架構(參閱 參考資料)。Jetty 還為處理長期連接提供了一些技巧:該特性稱為 Continuations。我將使用一個簡單的 servlet 對 Continuations 進行演示,這個 servlet 將接受請求,等待處理,然后發(fā)送響應。接下來,我將展示當客戶機數(shù)量超過服務器提供的處理線程后發(fā)生的狀況。最后,我將使用 Continuations 重新實現(xiàn) servlet,您將了解 Continuations 在其中扮演的角色。 為了便于理解下面的示例,我將把 Jetty servlet 引擎限制在一個單請求處理線程。清單 1 展示了 jetty.xml 中的相關配置。我實際上需要在 ThreadPool 使用三個線程:Jetty 服務器本身使用一個線程,另一線程運行 HTTP 連接器,偵聽到來的請求。第三個線程執(zhí)行 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 毫秒的時間。它還在執(zhí)行開始和結束時輸出系統(tǒng)時間。為了區(qū)別輸出和不同的請求,還將作為標識符的請求參數(shù)記錄在日志中。 清單 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()); } } | 現(xiàn)在可以觀察到 servlet 響應一些同步請求的行為。清單 3 展示了控制臺輸出,五個使用 lynx 的并行請求。命令行啟動五個 lynx 進程,將標識序號附加在請求 URL 的后面。 清單 3. 對 BlockingServlet 并發(fā)請求的輸出 $ 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 只可以使用一個線程執(zhí)行 servlet 的 service() 方法。Jetty 對請求進行排列,并按順序提供服務。當針對某請求發(fā)出響應后將立即顯示時間戳(一個 end 消息),servlet 接著處理下一個請求(后續(xù)的 start 消息)。因此即使同時發(fā)出五個請求,其中一個請求必須等待 8 秒鐘的時間才能接受 servlet 處理。 請注意,當 servlet 被阻塞時,執(zhí)行任何操作都無濟于事。這段代碼模擬了請求等待來自應用程序不同部分的異步事件。這里使用的服務器既不是 CPU 密集型也不是 I/O 密集型:只有線程池耗盡之后才會對請求進行排隊。 現(xiàn)在,查看 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 的五個并發(fā)請求的輸出 $ 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 消息出現(xiàn)兩次;先不要著急。其次,更重要的一點,請求現(xiàn)在不需排隊就能夠并發(fā)處理,注意所有 start 和 end 消息的時間戳是相同的。因此,每個請求的處理時間不會超過兩秒,即使只運行一個 servlet 線程。
Jetty Continuations 機制原理 理解了 Jetty Continuations 機制的實現(xiàn)原理,您就能夠解釋 清單 5 中的現(xiàn)象。要使用 Continuations,必須對 Jetty 進行配置,以使用其 SelectChannelConnector 處理請求。這個連接器構建在 java.nio API 之上,因此使它能夠不用消耗每個連接的線程就可以持有開放的連接。當使用 SelectChannelConnector 時,ContinuationSupport.getContinuation() 將提供一個 SelectChannelConnector.RetryContinuation 實例。(然而,您應該只針對 Continuation 接口進行編碼;請參閱 Portability and the Continuations API。)當對 RetryContinuation 調用 suspend() 時,它將拋出一個特殊的運行時異常 —— RetryRequest —— 該異常將傳播到 servlet 以外并通過過濾器鏈傳回,并由 SelectChannelConnector 捕獲。 但是發(fā)生該異常之后并沒有將響應發(fā)送給客戶機,請求被放到處于等待狀態(tài)的 Continuation 隊列中,而 HTTP 連接仍然保持打開狀態(tài)。此時,為該請求提供服務的線程將返回 ThreadPool ,用以為其他請求提供服務。 | 可移植性和 Continuations API 我提到過應該使用 Jetty 的 SelectChannelConnector 來啟用 Continuations 功能。然而,Continuations API 仍然可用于傳統(tǒng)的 SocketConnector ,這種情況下 Jetty 將回退到不同的 Continuation 實現(xiàn),該實現(xiàn)使用 wait()/notify() 方法。您的代碼仍然可以編譯和運行,但是卻失去了非阻塞 Continuations 的優(yōu)點。如果您希望繼續(xù)使用非 Jetty 服務器,您應該考慮編寫自己的 Continuation 包裝器,在運行時期使用反射檢查 Jetty Continuations 庫是否可用。DWR 就使用了這種策略。 | | 暫停的請求將一直保持在等待狀態(tài)的 Continuation 隊列,直到超出指定的時限,或者當對 resume() 方法的 Continuation 調用 resume() 時(稍后將詳細介紹)。出現(xiàn)上述任意一種條件時,請求將被重新提交到 servlet(通過過濾器鏈)。事實上,整個請求被重新進行處理,直到首次調用 suspend() 。當執(zhí)行第二次發(fā)生 suspend() 調用時,RetryRequest 異常不會被拋出,執(zhí)行照常進行。 現(xiàn)在應該可以解釋 清單 5 中的輸出了。每個請求依次進入 servlet 的 service() 方法后,將發(fā)送 start 消息進行響應,Continuation 的 suspend() 方法引發(fā) servlet 異常,將釋放線程使其處理下一個請求。所有五個請求快速通過 service() 方法的第一部分,并進入等待狀態(tài),并且所有 start 消息將在幾毫秒內輸出。兩秒后,當超過 suspend() 的時限后,將從等待隊列中檢索第一個請求,并將其重新提交給 ContinuationServlet 。第二次輸出 start 消息,立即返回對 suspend() 的第二次調用,并且發(fā)送 end 消息進行響應。然后將在此執(zhí)行 servlet 代碼來處理隊列中的下一個請求,以此類推。 因此,在 BlockingServlet 和 ContinuationServlet 兩種情況中,請求被放入隊列中以訪問單個 servlet 線程。然而,雖然 servlet 線程執(zhí)行期間 BlockingServlet 發(fā)生兩秒暫停,SelectChannelConnector 中的 ContinuationServlet 的暫停發(fā)生在 servlet 之外。ContinuationServlet 的總吞吐量更高一些,因為 servlet 線程沒有將大部分時間用在 sleep() 調用中。
使 Continuations 變得有用 現(xiàn)在您已經(jīng)了解到 Continuations 能夠不消耗線程就可以暫停 servlet 請求,我需要進一步解釋 Continuations API 以向您展示如何在實際應用中使用。 resume() 方法生成一對 suspend() ??梢詫⑺鼈円暈闃藴实?Object wait() /notify() 機制的 Continuations 等價體。就是說,suspend() 使 Continuation (因此也包括當前方法的執(zhí)行)處于暫停狀態(tài),直到超出時限,或者另一個線程調用 resume() 。suspend() /resume() 對于實現(xiàn)真正使用 Continuations 的 Comet 風格的服務非常關鍵。其基本模式是:從當前請求獲得 Continuation ,調用 suspend() ,等待異步事件的到來。然后調用 resume() 并生成一個響應。 然而,與 Scheme 這種語言中真正的語言級別的 continuations 或者是 Java 語言的 wait() /notify() 范例不同的是,對 Jetty Continuation 調用 resume() 并不意味著代碼會從中斷的地方繼續(xù)執(zhí)行。正如您剛剛看到的,實際上和 Continuation 相關的請求被重新處理。這會產(chǎn)生兩個問題:重新執(zhí)行 清單 4 中的 ContinuationServlet 代碼,以及丟失狀態(tài):即調用 suspend() 時丟失作用域內所有內容。 第一個問題的解決方法是使用 isPending() 方法。如果 isPending() 返回值為 true,這意味著之前已經(jīng)調用過一次 suspend() ,而重新執(zhí)行請求時還沒有發(fā)生第二次 suspend() 調用。換言之,根據(jù) isPending() 條件在執(zhí)行 suspend() 調用之前運行代碼,這樣將確保對每個請求只執(zhí)行一次。在 suspend() 調用具有等冪性之前,最好先對應用程序進行設計,這樣即使調用兩次也不會出現(xiàn)問題,但是某些情況下無法使用 isPending() 方法。Continuation 也提供了一種簡單的機制來保持狀態(tài):putObject(Object) 和 getObject() 方法。在 Continuation 發(fā)生暫停時,使用這兩種方法可以保持上下文對象以及需要保存的狀態(tài)。您還可以使用這種機制作為在線程之間傳遞事件數(shù)據(jù)的方式,稍后將演示這種方法。
編寫基于 Continuations 的應用程序 作為實際示例場景,我將開發(fā)一個基本的 GPS 坐標跟蹤 Web 應用程序。它將在不規(guī)則的時間間隔內生成隨機的經(jīng)緯度值對。發(fā)揮一下想象力,生成的坐標值可能就是臨近的一個公共車站、隨身攜帶著 GPS 設備的馬拉松選手、汽車拉力賽中的汽車或者運輸中的包裹。令人感興趣的是我將如何告訴瀏覽器這個坐標。圖 1 展示了這個簡單的 GPS 跟蹤器應用程序的類圖: 圖 1. 顯示 GPS 跟蹤器應用程序主要組件的類圖 首先,應用程序需要某種方法來生成坐標。這將由 RandomWalkGenerator 完成。從一對初始坐標對開始,每次調用它的私有 generateNextCoord() 方法時,將從該位置移動隨機指定的距離,并將新的位置作為 GpsCoord 對象返回。初始化完成后,RandomWalkGenerator 將生成一個線程,該線程以隨機的時間間隔調用 generateNextCoord() 方法并將生成的坐標發(fā)送給任何注冊了 addListener() 的 CoordListener 實例。清單 6 展示了 RandomWalkGenerator 循環(huán)的邏輯: 清單 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 類實現(xiàn) CoordListener 。ContinuationBasedTracker 的另一個公有方法是 getNextPosition(Continuation, int) 。清單 7 展示了這些方法的實現(xiàn): 清單 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 方法將檢查此時的請求是否是第二次執(zhí)行,然后將它添加到等待坐標的 Continuation 集合中。然后該 Continuation 被暫停。同時,onCoord —— 生成新坐標時將被調用 —— 循環(huán)遍歷所有處于等待狀態(tài)的 Continuation ,對它們設置 GPS 坐標,并重新使用它們。之后,每個再次執(zhí)行的請求完成 getNextPosition() 執(zhí)行,從 Continuation 檢索 GpsCoord 并將其返回給調用者。注意此處的同步需求,是為了保護 pendingContinuations 集合中的實例狀態(tài)不會改變,并確保新增的 Continuation 在暫停之前沒有被處理過。 最后一個難點是 servlet 代碼本身,如 清單 8 所示: 清單 8. GPSTrackerServlet 實現(xiàn) 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 只執(zhí)行了很少的工作。它僅僅獲取了請求的 Continuation ,調用 getNextPosition() ,將 GPSCoord 轉換成 JavaScript Object Notation (JSON),然后輸出。這里不需要防止重新執(zhí)行,因此我不必檢查 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 } } | 這個示例并不引人注意,但是提供了概念證明。發(fā)出請求后,它們將一直保持打開的連接直至生成坐標,此時將快速生成響應。這是 Comet 模式的基本原理,Jetty 使用這種原理在一個線程內處理 5 個并發(fā)請求,這都是 Continuations 的功勞。
創(chuàng)建一個 Comet 客戶機 現(xiàn)在您已經(jīng)了解了如何使用 Continuations 在理論上創(chuàng)建非阻塞 Web 服務,您可能想知道如何創(chuàng)建客戶端代碼來使用這種功能。一個 Comet 客戶機需要完成以下功能: - 保持打開
XMLHttpRequest 連接,直到收到響應。 - 將響應發(fā)送到合適的 JavaScript 處理程序。
- 立即建立新的連接。
更高級的 Comet 設置將使用一個連接將數(shù)據(jù)從不同服務推入瀏覽器,并且客戶機和服務器配有相應的路由機制。一種可行的方法是根據(jù)一種 JavaScript 庫,例如 Dojo,編寫客戶端代碼,這將提供基于 Comet 的請求機制,其形式為 dojo.io.cometd 。 然而,如果服務器使用 Java 語言,使用 DWR 2 可以同時在客戶機和服務器上獲得 Comet 高級支持,這是一種不錯的方法(參閱 參考資料)。如果您并不了解 DWR 的話,請參閱本系列第 3 部分 “結合 Direct Web Remoting 使用 Ajax”。DWR 透明地提供了一種 HTTP-RPC 傳輸層,將您的 Java 對象公開給網(wǎng)絡中 JavaScript 代碼的調用。DWR 生成客戶端代理,將自動封送和解除封送數(shù)據(jù),處理安全問題,提供方便的客戶端實用工具庫,并可以在所有主要瀏覽器上工作。
DWR 2: Reverse Ajax DWR 2 最新引入了 Reverse Ajax 概念。這種機制可以將服務器端事件 “推入” 到客戶機??蛻舳?DWR 代碼透明地處理已建立的連接并解析響應,因此從開發(fā)人員的角度來看,事件是從服務器端 Java 代碼輕松地發(fā)布到客戶機中。 DWR 經(jīng)過配置之后可以使用 Reverse Ajax 的三種不同機制。第一種就是較為熟悉的輪詢方法。第二種稱為 piggyback,這種機制并不創(chuàng)建任何到服務器的連接,相反,將一直等待直至發(fā)生另一個 DWR 服務,piggybacks 使事件等待該請求的響應。這使它具有較高的效率,但也意味著客戶機事件通知被延遲到直到發(fā)生另一個不相關的客戶機調用。最后一種機制使用長期的、Comet 風格的連接。最妙的是,當運行在 Jetty 下時,DWR 能夠自動檢測并切換為使用 Contiuations,實現(xiàn)非阻塞 Comet。 我將在 GPS 示例中結合使用 Reverse Ajax 和 DWR 2。通過這種演示,您將對 Reverse Ajax 的工作原理有更多的了解。 此時不再需要使用 servlet。DWR 提供了一個控制器 servlet,它將在 Java 對象之上直接轉交客戶機請求。同樣也不需要顯式地處理 Continuations,因為 DWR 將在內部進行處理。因此我只需要一個新的 CoordListener 實現(xiàn),將坐標更新發(fā)布到到任何客戶機瀏覽器上。 ServerContext 接口提供了 DWR 的 Reverse Ajax 功能。ServerContext 可以察覺到當前查看給定頁面的所有 Web 客戶機,并提供一個 ScriptSession 進行相互通信。ScriptSession 用于從 Java 代碼將 JavaScript 片段推入到客戶機。清單 10 展示了 ReverseAjaxTracker 響應坐標通知的方式,并使用它們生成對客戶端 updateCoordinate() 函數(shù)的調用。注意對 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 創(chuàng)建了一個 ReverseAjaxTracker 新實例并將其放到 application 范圍中。所有后續(xù)請求將訪問這個實例。 我還需告訴 DWR 如何將數(shù)據(jù)從 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 對象的名字,在本例中,我的客戶端代碼沒有使用該屬性,而是將數(shù)據(jù)從跟蹤器推入到其中。同樣,還需對 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)的常規(guī)行為 —— 在本例中這是必須的,因為客戶機不會主動對 ReverseAjaxTracker 調用方法。 最后,我需要實現(xiàn)調用自 DWR 的客戶端 JavaScript 函數(shù)。將向回調函數(shù) —— updateCoordinate() —— 傳遞 GpsCoord Java bean 的 JSON 表示,由 DWR 的 BeanConverter 自動序列化。該函數(shù)將從坐標中提取 longitude 和 latitude 字段,并通過調用 Document Object Model (DOM) 將它們附加到列表中。清單 13 展示了這一過程,以及頁面的 onload 函數(shù)。onload 包含對 dwr.engine.setActiveReverseAjax(true) 的調用,將通知 DWR 打開與服務器的持久連接并等待回調。 清單 13. 簡單 Reverse Ajax GPS 跟蹤器的客戶端實現(xiàn) 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 代碼的數(shù)量,可以使用 ScriptSession 編寫 JavaScript 回調:將 ScriptSession 實例封裝在 DWR Util 對象中。該類將提供直接操作瀏覽器 DOM 的簡單 Java 方法,并在后臺自動生成所需的腳本。 | | 現(xiàn)在我可以將瀏覽器指向跟蹤器頁面,DWR 將在生成坐標數(shù)據(jù)時把數(shù)據(jù)推入客戶機。該實現(xiàn)輸出生成坐標的列表,如 圖 2 所示: 圖 2. ReverseAjaxTracker 的輸出 可以看到,使用 Reverse Ajax 創(chuàng)建事件驅動的 Ajax 應用程序非常簡單。請記住,正是由于 DWR 使用了 Jetty Continuations,當客戶機等待新事件到來時不會占用服務器上面的線程。 此時,集成來自 Yahoo! 或 Google 的地圖部件非常簡單。通過更改客戶端回調,可輕松地將坐標傳送到地圖 API,而不是直接附加到頁面中。圖 3 展示了 DWR Reverse Ajax GPS 跟蹤器在此類地圖組件上標繪隨機路線: Figure 3. 具有地圖 UI 的 ReverseAjaxTracker
結束語 通過本文,您了解了如何結合使用 Jetty Continuations 和 Comet 為事件驅動 Ajax 應用程序提供高效的可擴展解決方案。我沒有給出 Continuations 可擴展性的具體數(shù)字,因為實際應用程序的性能取決于多種變化的因素。服務器硬件、所選擇的操作系統(tǒng)、JVM 實現(xiàn)、Jetty 配置以及應用程序的設計和通信量配置文件都會影響 Jetty Continuations 的性能。然而,Webtide 的 Greg Wilkins(主要的 Jetty 開發(fā)人員)曾經(jīng)發(fā)布了一份關于 Jetty 6 的白皮書,對使用 Continuations 和沒有使用 Continuations 的 Comet 應用程序的性能進行了比較,該程序同時處理 10000 個并發(fā)請求(參閱 參考資料)。在 Greg 的測試中,使用 Continuations 能夠減少線程消耗,并同時減少了超過 10 倍的棧內存消耗。 您還看到了使用 DWR 的 Reverse Ajax 技術實現(xiàn)事件驅動 Ajax 應用程序是多么簡單。DWR 不僅省去了大量客戶端和服務器端編碼,而且 Reverse Ajax 還從代碼中將完整的服務器-推送機制抽象出來。通過更改 DWR 的配置,您可以自由地在 Comet、輪詢,甚至是 piggyback 方法之間進行切換。您可以對此進行實驗,并找到適合自己應用程序的最佳性能策略,同時不會影響到自己的代碼。 如果希望對自己的 Reverse Ajax 應用程序進行實驗,下載并研究 DWR 演示程序的代碼(DWR 源代碼發(fā)行版的一部分,參閱 參考資源)將非常有幫助。如果希望親自運行示例,還可獲得本文使用的示例代碼(參見 下載)。
下載 描述 | 名字 | 大小 | 下載方法 | 示例代碼 | jetty-dwr-comet-src.tgz | 8KB | HTTP |
參考資料 學習 獲得產(chǎn)品和技術 討論
關于作者 | | | Philip McCarthy 是一名在倫敦市工作的軟件開發(fā)顧問,專攻 Java 和 Web 技術。他曾經(jīng)參與過 Orange 和 Hewlett Packard Labs 的項目。目前的工作重點是研究使用開源框架構建的基于 Web 的財務系統(tǒng)。 |
|