面向 Java 開發人員的 Ajax: 使用 Jetty 和 Direct Web Remoting 編寫可擴展的 Comet 應用程序

(轉自http://www.ibm.com/developerworks/cn/java/j-jettydwr/javascript

做爲一種普遍使用的 Web 應用程序開發技術,Ajax 牢固確立了本身的地位,隨之而來的是一些通用 Ajax 使用模式。例如,Ajax 常常用於對用戶輸入做出響應,而後使用從服務器得到的新數據修改頁面的部份內容。可是,有時 Web 應用程序的用戶界面須要進行更新以響應服務器端發生的異步事件,而不須要用戶操做 —— 例如,顯示到達 Ajax 聊天應用程序的新消息,或者在文本編輯器中顯示來自另外一個用戶的改變。因爲只能由瀏覽器創建 Web 瀏覽器和服務器之間的 HTTP 鏈接,服務器沒法在改動發生時將變化 「推送」 給瀏覽器。html

Ajax 應用程序可使用兩種基本的方法解決這一問題:一種方法是瀏覽器每隔若干秒時間向服務器發出輪詢以進行更新,另外一種方法是服務器始終打開與瀏覽器的鏈接並在數據可用時發送給瀏覽器。長期鏈接技術被稱爲 Comet(請參閱 參考資料)。本文將展現如何結合使用 Jetty servlet 引擎和 DWR 簡捷有效地實現一個 Comet Web 應用程序。java

爲何使用 Comet?git

輪詢方法的主要缺點是:當擴展到更多客戶機時,將生成大量的通訊量。每一個客戶機必須按期訪問服務器以檢查更新,這爲服務器資源添加了更多負荷。最壞的一種狀況是對不頻繁發生更新的應用程序使用輪詢,例如一種 Ajax 郵件 Inbox。在這種狀況下,至關數量的客戶機輪詢是沒有必要的,服務器對這些輪詢的回答只會是 「沒有產生新數據」。雖然能夠經過增長輪詢的時間間隔來減輕服務器負荷,可是這種方法會產生不良後果,即延遲客戶機對服務器事件的感知。固然,不少應用程序能夠實現某種權衡,從而得到可接受的輪詢方法。web

儘管如此,吸引人們使用 Comet 策略的其中一個優勢是其顯而易見的高效性。客戶機不會像使用輪詢方法那樣生成煩人的通訊量,而且事件發生後可當即發佈給客戶機。可是保持長期鏈接處於打開狀態也會消耗服務器資源。當等待狀態的 servlet 持有一個持久性請求時,該 servlet 會獨佔一個線程。這將限制 Comet 對傳統 servlet 引擎的可伸縮性,由於客戶機的數量會很快超過服務器棧能有效處理的線程數量。ajax


回頁首json

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 展現了 BlockingServletservice() 方法,該方法將使用 Thread.sleep() 調用在線程結束以前暫停 2000 毫秒的時間。它還在執行開始和結束時輸出系統時間。爲了區別輸出和不一樣的請求,還將做爲標識符的請求參數記錄在日誌中。


清單 2. BlockingServlet

<?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>


    

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 消息出現兩次;先不要着急。其次,更重要的一點,請求如今不需排隊就可以併發處理,注意全部 startend 消息的時間戳是相同的。所以,每一個請求的處理時間不會超過兩秒,即便只運行一個 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 消息進行響應,Continuationsuspend() 方法引起 servlet 異常,將釋放線程使其處理下一個請求。全部五個請求快速經過 service() 方法的第一部分,並進入等待狀態,而且全部 start 消息將在幾毫秒內輸出。兩秒後,當超過 suspend() 的時限後,將從等待隊列中檢索第一個請求,並將其從新提交給 ContinuationServlet。第二次輸出 start 消息,當即返回對 suspend() 的第二次調用,而且發送 end 消息進行響應。而後將在此執行 servlet 代碼來處理隊列中的下一個請求,以此類推。

所以,在 BlockingServletContinuationServlet 兩種狀況中,請求被放入隊列中以訪問單個 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 類實現 CoordListenerContinuationBasedTracker 的另外一個公有方法是 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 客戶機須要完成如下功能:

  1. 保持打開 XMLHttpRequest 鏈接,直到收到響應。
  2. 將響應發送到合適的 JavaScript 處理程序。
  3. 當即創建新的鏈接。

更高級的 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);
  }   
}


    

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>


    

<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>
   <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-paramactiveReverseAjaxEnabled 將激活輪詢和 Comet 功能。第二個 initApplicationScopeCreatorsAtStartup 通知 DWR 在應用程序啓動時初始化 ReverseAjaxTracker。這將在對 bean 生成第一個請求時改寫延遲初始化(lazy initialization)的常規行爲 —— 在本例中這是必須的,由於客戶機不會主動對 ReverseAjaxTracker 調用方法。

最後,我須要實現調用自 DWR 的客戶端 JavaScript 函數。將向回調函數 —— updateCoordinate() —— 傳遞 GpsCoord Java bean 的 JSON 表示,由 DWR 的 BeanConverter 自動序列化。該函數將從座標中提取 longitudelatitude 字段,並經過調用 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);
  }
}


    

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 源代碼發行版的一部分,參閱 參考資源)將很是有幫助。若是但願親自運行示例,還可得到本文使用的示例代碼(參見 下載)。

相關文章
相關標籤/搜索