異步服務(wù)器端事件驅(qū)動的Ajax程序很難實現(xiàn),也很難獲得伸縮性。在Java+developers:" target=blank>作者的系列文章里,Plilip McCarthy展示了一個有效的方式:
Comet模式允許您push數(shù)據(jù)到客戶端,而且Jetty 6的Continuations API讓您的Comet程序?qū)Υ罅靠蛻舳双@得高可伸縮性。您可以方便的同DWR 2使用Comet和Continuations。
隨著Ajax在Web程序開發(fā)技術(shù)里建立了牢固的位置,出現(xiàn)了幾種常見的Ajax使用模式。例如,Ajax通常用于響應(yīng)用戶輸入來使用新數(shù)據(jù)修改局部頁面。但有時候,Web程序的用戶界面需要根據(jù)偶爾的異步服務(wù)器端事件來更新,而不需要用戶動作 -- 例如,在Ajax聊天程序里顯示其他用戶輸入的一條新消息。由于Web瀏覽器和服務(wù)器間的HTTP連接只能由瀏覽器建立,服務(wù)器不能"推"更改數(shù)據(jù)到瀏覽器。
Ajax程序有兩個解決該問題的基本方式:瀏覽器每隔幾秒請求服務(wù)器來獲得更改,或者服務(wù)器維持與瀏覽器的連接并且傳遞數(shù)據(jù)。長連接技術(shù)稱為Comet。本文展示了怎樣使用Jetty服務(wù)器引擎和DWR來實現(xiàn)簡單而高效的Comet Web程序。
為什么要Comet?
輪詢方式的主要缺點是在大量客戶端時產(chǎn)生了大量的傳輸浪費。每個客戶端都必須有規(guī)律的請求服務(wù)器來獲得更改,這是服務(wù)器資源的一個重擔(dān)。最壞的情況是程序很少更新,例如Ajax郵件收件箱。在這種情況下,大量的客戶端輪詢是多余的,服務(wù)器僅僅簡單的響應(yīng)"沒有數(shù)據(jù)"。可以通過增加輪詢間隔時間來減輕服務(wù)器負荷,但是這引入了服務(wù)器事件和客戶端知曉之間的延遲。當(dāng)然,一個合理的折衷方案可以多數(shù)程序適用,并且輪詢的工作方式也可以接受。
然而,對Comet策略的呼喚來自它可感知的高效??蛻舳瞬粫a(chǎn)生輪詢方式特有的傳輸浪費,一旦事件發(fā)生,就會被發(fā)布到客戶端。但是維持長連接也消耗了服務(wù)器資源。當(dāng)servlet位置持久的請求在等候狀態(tài)時,servlet獨占一個線程。這樣傳統(tǒng)的servlet引擎就限制了Comet的伸縮性,因為客戶端的數(shù)量會迅速超過服務(wù)器??梢杂行幚淼木€程的數(shù)量。
Jetty 6有什么不同
Jetty 6設(shè)計來處理大量并發(fā)連接,它使用Java語言的不堵塞I/O(java.nio)庫并且使用優(yōu)化的輸出緩沖架構(gòu)。Jetty也有一個處理長連接的殺手锏:一個稱為Continuations的特性。我將用一個接收請求然后等待兩秒發(fā)送響應(yīng)的簡單servlet來示范Continuations。然后,我將展示當(dāng)服務(wù)器擁有更多的客戶端時將發(fā)生什么。最后我將使用Continuations重新實現(xiàn)servlet,并且您將看到它們的不同。
為了讓它更簡單,我將限制Jetty servlet引擎為一個單一的請求處理線程。列表1顯示了相關(guān)的jetty.xml配置。事實上我需要允許在ThreadPool里總共有3個線程: Jetty服務(wù)器本身使用一個,HTTP連接器使用一個來監(jiān)聽進來的請求,最后剩一個線程來執(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.threadBoundedThreadPool">
<Set name="minThreads">3</Set>
<Set name="lowThreads">0</Set>
<Set name="maxThreads">3</Set>
</New>
</Set>
</Configure>
下一步,為了模仿異步事件,列表2顯示了BlockingServlet的service()方法,它簡單的使用Thread.sleep()調(diào)用來在完成前暫停2,000毫秒。同時它也在執(zhí)行開始和結(jié)束時輸出系統(tǒng)時間。為了幫助區(qū)分不同請求的輸出,它也把一個請求參數(shù)作為標(biāo)識符記錄到日志。
列表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的5個并行請求時控制臺的輸出。命令行簡單的啟動5個lynx進程,加上一個標(biāo)識符序數(shù)到請求的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將每個請求列隊并按順序服務(wù)。時間戳顯示了在一個應(yīng)答分派給一個請求(以及end消息)后,servlet開始處理下一個請求(下一個 start消息)。所以即使所有的5個請求是同時發(fā)出的,最后的那個請求必須等待8秒才能得到處理。
現(xiàn)在,看看Jetty 6的Continuations特性在這種情形下是多么的有用。列表4顯示了列表2的BlockingServlet使用Continuations API重寫后的樣子。我將在后面解釋代碼。
列表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作5個并發(fā)請求時的輸出,可以和列表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 12:37:37 BST 2007
Request: 1 start: Sun Jul 01 12:37:39 BST 2007
Request: 1 end: Sun Jul 01 12:37:39 BST 2007
Request: 3 start: Sun Jul 01 12:37:37 BST 2007
Request: 3 start: Sun Jul 01 12:37:39 BST 2007
Request: 3 end: Sun Jul 01 12:37:39 BST 2007
Request: 2 start: Sun Jul 01 12:37:37 BST 2007
Request: 2 start: Sun Jul 01 12:37:39 BST 2007
Request: 2 end: Sun Jul 01 12:37:39 BST 2007
Request: 5 start: Sun Jul 01 12:37:37 BST 2007
Request: 5 start: Sun Jul 01 12:37:39 BST 2007
Request: 5 end: Sun Jul 01 12:37:39 BST 2007
Request: 4 start: Sun Jul 01 12:37:37 BST 2007
Request: 4 start: Sun Jul 01 12:37:39 BST 2007
Request: 4 end: Sun Jul 01 12:37:39 BST 2007
在列表5里有兩件重要的事情值得注意。首先,每個start消息出現(xiàn)了兩次,暫時不要擔(dān)心這點。其次,更重要的是,現(xiàn)在請求是并發(fā)處理的,沒有排隊。注意所有的start和end消息時間戳是一樣的。因此,沒有哪個請求耗時超多兩秒,即使只有單一的 servlet線程在運行。
深入Jetty的Continuations機制
理解Jetty的Continuations機制的將解釋您在列表5里看到的東西。為了使用Continuatins,Jetty必須配置為使用它的SelectChannelConnector處理請求。這個 connector構(gòu)建在java.nio API之上,允許它維持每個連接開放而不用消耗一個線程。當(dāng)使用SelectChannelConnector時, ContinuationSupport.getContinuation()提供一個 SelectChannelConnector.RetryContinuation實例(但是,您必須針對Continuation接口編程)。當(dāng)在 RetryContinuation上調(diào)用suspend()時,它拋出一個特殊的運行時異常 -- RetryRequest,該異常傳播到servlet外并且回溯到filter鏈,最后被SelectChannelConnector捕獲。但是不會發(fā)送一個異常響應(yīng)給客戶端,而是將請求維持在未決 Continuations隊列里,則HTTP連接保持開放。這樣,用來服務(wù)請求的線程返回給ThreadPool,然后又可以用來服務(wù)其他請求。
暫停的請求停留在未決 Continuations隊列里直到指定的過期時間,或者在它的Continuation上調(diào)用resume()方法。當(dāng)任何一個條件觸發(fā)時,請求會重新提交給servlet(通過filter鏈)。這樣,整個請求被"重播"直到RetryRequest異常不再拋出,然后繼續(xù)按正常情況執(zhí)行。
列表5里的輸出現(xiàn)在應(yīng)該能理解了。對每個請求,按順序進入到servlet的service()方法,start消息發(fā)送給應(yīng)答,然后 Continuation的suspend()方法保留servlet,然后釋放線程來開始下一請求。所有的5個請求迅速運行service()方法的第一部分并馬上進入暫停狀態(tài),所有的start消息在幾毫秒內(nèi)輸出。兩秒后,suspend()過期,第一個請求從未決隊列里重新得到并重新提交給 ContinuationServlet。start消息第二次輸出,對suspend()方法的第二次調(diào)用立即返回,然后end消息被發(fā)送給應(yīng)答。然后 servlet代碼執(zhí)行下一個隊列請求,以此類推。
所以,在BlockingServlet和ContinuationServlet兩種情況下,請求被排入隊列來訪問單一的servlet線程。盡管如此,在BlockingServlet里的兩秒鐘暫停在servlet線程里執(zhí)行時,ContinuationServlet的暫停發(fā)生于 servlet外面的SelectChannelConnector里。ContinuationServlet全部的吞吐量會更高,因為servlet 線程不會在sleep()調(diào)用時阻礙大多數(shù)時間。
讓Continuations變得有用
現(xiàn)在您已經(jīng)看到Continuations運行servlet請求暫停而不消耗線程,我需要多解釋一下Continuations API來展示怎樣使用Continuations達到特殊的目的。
一個resume()方法和一個suspend()方法配對。您可以認為它們是標(biāo)準(zhǔn)的Object wait()/notify()機制的Continuations等價物。即,suspend()維持一個Continuation直到過期或者另一個線程調(diào)用resume()。suspend()/resume()方法是使用Continuations實現(xiàn)真實的Comet風(fēng)格服務(wù)的關(guān)鍵所在?;镜哪J绞菑漠?dāng)前請求維持Continuation,調(diào)用suspend(),然后等待直到您的異步時間到達,然后調(diào)用resume()并生成應(yīng)答。
但是,不像編程語言里真實的語言級continuations,如Scheme,或Java語言里的wait()/notify(),在 Jetty Continuation上調(diào)用resume()并不意味著代碼執(zhí)行于它停止的確切位置。您已經(jīng)看到,真正發(fā)生的是與Continuation相關(guān)的請求被重播。這導(dǎo)致兩個問題:列表4的ContinuationServlet里代碼不合需要的重新執(zhí)行,以及丟失狀態(tài) -- 暫停時作用域里的任何東西都丟失了。
第一個問題的解決方案是isPending()方法,如果isPending()方法的返回值為true,這意味著suspend()在前面已經(jīng)被調(diào)用過了,并且二次請求的執(zhí)行不會再次接觸suspend()方法。換句話說,給您的suspend()調(diào)用前的代碼加上isPending()條件可以確保它只被執(zhí)行一次。Continuation也提供了一個簡單的機制來保持狀態(tài):putObject(Object)和getObject()方法。使用它們來維持一個context對象,這樣當(dāng)Continuation暫停時任何您需要維持的狀態(tài)都可以得到保護。您也可以使用該機制作為一種在線程之間傳遞事件數(shù)據(jù)的方法,后面您將看到。
寫一個基于Continuations的程序
作為一個真實世界里的例子,我將開發(fā)一個基本的GPS坐標(biāo)跟蹤Web程序。它將在無規(guī)律間隔內(nèi)生成隨機的緯度-經(jīng)度對。假設(shè)生成的坐標(biāo)可以為附近的公眾移動位置,如拿著GPS設(shè)備馬拉松運動員,成隊的汽車,或者運輸中的包裹位置。有意思的部分在于我怎樣告訴瀏覽器坐標(biāo)信息。圖1顯示了這個簡單的GPS跟蹤程序的類圖:
圖1. 顯示GPS跟蹤程序主要組件的類圖
首先,該程序需要生成坐標(biāo)的一些東西,這是RandomWalkGenerator的工作。從一個初始坐標(biāo)開始,每次對它的私有方法 generateNextCoord()的調(diào)用都從該位置隨機走一步并返回一個GpsCoord對象。當(dāng)初始化時, RandomWalkGenerator創(chuàng)建一個線程,該線程在隨機間隔內(nèi)調(diào)用generateNextCoorld()方法并發(fā)送生成的坐標(biāo)給任何使用 addListener()注冊自己的CoordListener實例。
列表6顯示了RandomWalkGenerator的循環(huán)邏輯:
列表6. RandomWalkGenerator的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)方法的回調(diào)接口。在例子中,ContinuationBasedTracker類實現(xiàn)了CoordListener。 ContinuationBasedTracker的另外一個方法為getNextPosition(Continuation, int)。列表7顯示了這些方法的具體實現(xiàn):
列表7. ContinuationBasedTracker的內(nèi)臟
代碼
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(pgsCoord);
continuation.resume();
}
pendingContinuations.clear();
}
}
當(dāng)客戶端在Continuation里調(diào)用getNextPosition()時,isPending()方法檢查這次請求不是重試,然后添加它到一個等待坐標(biāo)的Continuations集合里,然后Continuation被暫停。同時,onCoord -- 當(dāng)生成新坐標(biāo)時調(diào)用 -- 簡單的循環(huán)每個未決Continuations,為它們設(shè)置GPS坐標(biāo),然后恢復(fù)它們。然后每個重試的請求完成getNextPosition()的執(zhí)行,從Continuation得到GpsCoord并返回它給調(diào)用者。注意這里需要同步,不僅預(yù)防pendingContinuations集合里出現(xiàn)不一致的狀態(tài),也確保了新添加的Continuation在它被暫停之前不會被恢復(fù)。
謎題最后一部分是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所做很少。它簡單的維持請求的Continuation,調(diào)用 getNextPosition(),轉(zhuǎn)換GPSCoord為JavaScript Object Notation(JSON)并輸出。這里不需要防止任何代碼重執(zhí)行,所以我不需要檢查isPending()。列表9顯示了對 GpsTrackerServlet的調(diào)用的輸出,使用服務(wù)器可得到的單一線程上的5個并發(fā)請求。
列表9. 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ā)幾秒鐘直到生成坐標(biāo),這時迅速產(chǎn)生應(yīng)答。這是Comet模式的基本原理,使用Jetty單一線程處理5個并發(fā)請求,感謝Continuations。
創(chuàng)建一個Comet客戶端
現(xiàn)在您已經(jīng)看到Continuations怎樣用于創(chuàng)建非阻塞Web服務(wù),您可能想知道怎樣創(chuàng)建客戶端代碼來使用它。一個Comet客戶端需要:
1. 維持一個XMLHttpRequest連接直到接收應(yīng)答.
2. 分派應(yīng)答給合適的JavaScript處理者.
3. 立即建立一個新連接.
更高級的Comet可以在客戶端和服務(wù)器使用合適的路由機制來使用一個連接來從多個不同的服務(wù)推數(shù)據(jù)到瀏覽器。一個可能性為使用JavaScript庫如Dojo等寫客戶端代碼來提供基于Comet的請求機制,形如dojo.io.comet。
盡管如此,如果您正在使用Java作為服務(wù)器語言,在客戶端和服務(wù)器端得到高級Comet支持的更好的方式是使用DWR 2。如果您不熟悉DWR,您可以該系列的第3部分,"Ajax with Direct Web Remoting"。DWR透明的提供一個HTTP-RPC傳輸層,暴露您的Java對象來使用JavaScript代碼調(diào)用。DWR生成客戶端代理,自動marshall和unmarshall數(shù)據(jù),處理安全問題,提供一個便利的客戶端輔助庫,并且對所有主要的瀏覽器工作。
DWR 2:反轉(zhuǎn)Ajax
DWR 2新引入的概念為反轉(zhuǎn)Ajax。該機制將服務(wù)端事件"推"給客戶端??蛻舳薉WR代碼透明的處理連接建立和應(yīng)答解析,所以從開發(fā)人員的角度來看,事件可以從服務(wù)端Java代碼簡單的發(fā)布到客戶端。
DWR可以配置使用3個不同的機制來反轉(zhuǎn)Ajax。一種是我們熟悉的輪詢方式。第二種方式稱為piggyback,它不創(chuàng)建任何到服務(wù)器的連接,而是等待直到另一個DWR服務(wù)調(diào)用發(fā)生并piggyback未決事件到該請求應(yīng)答。這可以獲得高效率但是意味著客戶端事件通知被延遲直到客戶端作出一個不相干的調(diào)用。最后一種機制使用Comet風(fēng)格的長連接。最好的是,當(dāng)DWR運行在Jetty下并且使用Continuations來獲得非阻塞Comet 時可以自動檢測事件。
我將修改我的GPS例子來使用DWR 2反轉(zhuǎn)Ajax。同時,您將看到反轉(zhuǎn)Ajax怎樣工作的更多細節(jié)。
我不再需要我的servlet。DWR提供了一個controller servlet,它協(xié)調(diào)客戶端請求直接訪問Java對象。我也不再需要顯示處理Continuations,因為DWR在幕后處理了這些。所以我只需要一個新的CoordListener實現(xiàn)來發(fā)布坐標(biāo)更新到任何客戶端瀏覽器。
一個稱為ServerContext的接口提供DWR的反轉(zhuǎn)Ajax魔法。ServerContext知道當(dāng)前查看一個給定頁面的所有Web客戶端并且可以提供一個ScriptSession來與每個客戶端交流。ScriptSession用來從Java代碼推JavaScript片段到客戶端。列表10顯示了ReverseAjaxTracker怎樣響應(yīng)坐標(biāo)通知,以及使用它們來生成客戶端updateCoordinate()方法調(diào)用。注意如果一個合適的轉(zhuǎn)換器是可用的,則DWR的ScriptBuffer對象的appendData()調(diào)用會自動marshall一個Java對象到 JSON,。
列表10. ReverseAjaxTracker里的通知回調(diào)方法
代碼
public void onCoord(GpsCoord gpsCoord) {
// Generate JavaScriptcode 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創(chuàng)建的beans來提供DWR。但是這里,我將僅僅讓DWR創(chuàng)建一個新的ReverseAjaxTracker實例并把它放在application作用域里。所有后續(xù)的DWR請求將訪問這個單一的實例。
我也需要告訴DWR怎樣從GpsCoord beans來marshall數(shù)據(jù)到JSON。由于GpsCoord是一個簡單對象,DWR基于反射的BeanConverter足夠。
列表11顯示了ReverseAjaxTracker配置。
列表11. ReverseAjaxTracker的DWR配置
代碼
<dwr>
<allow>
<create creator="new" javascrit="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用來暴露tracker作為一個JavaScript對象的名字。但是,在這里,我的客戶端代碼不會使用它,而是從tracker推數(shù)據(jù)給它。同時,也需要在web.xml里做一些額外的配置來讓DWR使用反轉(zhuǎn)Ajax,見列表12。
列表12. DwrServlet的web.xml配置
代碼
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>
org.directwebremoteing.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>
</servlet>
第一個servlet init-param,activeReverseAjaxEnabled,激活輪詢和Comet功能。第二個, initApplicationScopeCreatorsAtStartup,告訴DWR當(dāng)程序開始時初始化ReverseAjaxTracker。這會覆蓋通常在bean上作第一次請求時的延遲初始化行為 -- 在這里這是很有必要的,因為客戶端從不在ReverseAjaxTracker上調(diào)用方法。
最后,我需要實現(xiàn)從DWR調(diào)用的客戶端JavaScript方法?;卣{(diào)方法updateCoordinate()被傳遞一個JSON形式的 GpsCoord對象,它由DWR的BeanConverter自動序列化。這個方法僅僅從坐標(biāo)提取longitude和latitude域并通過DOM 調(diào)用添加它們到一個列表里。這在列表13里顯示了,同我的頁面的onload方法一起。onload包含對 dwr.engine.setActiveReverseAjax(true),這告訴DWR打開一個到服務(wù)器的持久的連接來等待回調(diào)。
列表13. 反轉(zhuǎn)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);
}
}
現(xiàn)在我可以讓我的瀏覽器訪問跟蹤程序頁面,當(dāng)坐標(biāo)數(shù)據(jù)開始生成時DWR將開始推數(shù)據(jù)到客戶端。這個實現(xiàn)將簡單的輸出一個生成的坐標(biāo)列表,見圖2:
圖2. ReverseAjaxTracker輸出
使用反轉(zhuǎn)Ajax創(chuàng)建一個事件驅(qū)動的Ajax程序是如此簡單。記住,感謝DWR對Jetty Continuations的使用,當(dāng)?shù)却率录竭_時線程不會阻塞在服務(wù)器。
據(jù)此,很容易從Yahoo!或者Google集成一個地圖窗口部件。通過改變客戶端回調(diào)方法,坐標(biāo)可以簡單的傳遞到地圖API,而不是直接添加到頁面。圖3顯示了在這樣的一個地圖組件上DWR反轉(zhuǎn)Ajax GPS跟蹤程序描繪的隨機路線:
圖3. 使用地圖UI的ReverseAjaxTracker
結(jié)論
現(xiàn)在您看到了Jetty Continuations聯(lián)合Comet可以提供一個高效的、可伸縮的事件驅(qū)動Ajax程序的解決方案。我沒有給出Continuations的伸縮性的圖,因為性能在真是世界里取決于許多變數(shù)。服務(wù)器硬件、操作系統(tǒng)的選擇、JVM實現(xiàn)、Jetty配置、您的Web程序的設(shè)計和傳輸效率在負荷下都會影響 Jetty Continuations的性能。盡管如此,Webtide的Greg Wilkins(首要的Jetty開發(fā)者) 發(fā)布了一個比較Jetty 6集成Continuations與不集成Continuations的Comet程序處理10,000并發(fā)請求時的性能的白皮書。在Greg的測試?yán)?,使用Continuations并去掉了線程消費和棧內(nèi)存消費,使用大于10的因數(shù)。
您也看到了使用DWR的反轉(zhuǎn)Ajax技術(shù)實現(xiàn)事件驅(qū)動的Ajax程序是多么容易。DWR不僅節(jié)省您的客戶端和服務(wù)端代碼,反轉(zhuǎn)Ajax也將整個服務(wù)器推機制從您的代碼中抽象出來。您可以隨意轉(zhuǎn)換您的Comet方式:輪詢或者piggyback方式,只需簡單的更改DWR配置。您可以隨意試驗并找到適合您的程序的最佳策略而不會影響您的代碼。
關(guān)于作者
Philip McCarthy是倫敦的一位軟件開發(fā)顧問,專于Java和Web技術(shù)。