為了使用 Continuatins,Jetty 必須配置為使用它的 SelectChannelConnector 處理請求。這個 connector 構建在 java.nio API 之上,允許它維持每個連接開放而不用消耗一個線程。當使用 SelectChannelConnector 時,ContinuationSupport.getContinuation() 提供一個 SelectChannelConnector.RetryContinuation 實例(但是,您必須針對 Continuation 接口編程)。當在 RetryContinuation 上調用 suspend() 時,它拋出一個特殊的運行時異常 -- RetryRequest,該異常傳播到 servlet 外并且回溯到 filter 鏈,最后被 SelectChannelConnector 捕獲。但是不會發(fā)送一個異常響應給客戶端,而是將請求維持在未決 Continuations 隊列里,則 HTTP 連接保持開放。這樣,用來服務請求的線程返回給 ThreadPool,然后又可以用來服務其他請求。暫停的請求停留在未決 Continuations 隊列里直到指定的過期時間,或者在它的 Continuation 上調用 resume() 方法。當任何一個條件觸發(fā)時,請求會重新提交給 servlet(通過 filter 鏈)。這樣,整個請求被"重播"直到 RetryRequest 異常不再拋出,然后繼續(xù)按正常情況執(zhí)行。
此,在 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ù)的方式,稍后將演示這種方法。
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。
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);
}
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 方法,并在后臺自動生成所需的腳本。
tomcat5:客戶端連接到達 -> 傳統(tǒng)的SeverSocket.accept接收連接 -> 從線程池取出一個線程 -> 在該線程讀取文本并且解析HTTP協(xié)議 -> 在該線程生成ServletRequest、ServletResponse,取出請求的Servlet -> 在該線程執(zhí)行這個Servlet -> 在該線程把ServletResponse的內容發(fā)送到客戶端連接 -> 關閉連接。
我以前理解的使用nio后的tomcat6:客戶端連接到達 -> nio接收連接 -> nio使用輪詢方式讀取文本并且解析HTTP協(xié)議(單線程) -> 生成ServletRequest、ServletResponse,取出請求的Servlet -> 直接在本線程執(zhí)行這個Servlet -> 把ServletResponse的內容發(fā)送到客戶端連接 -> 關閉連接。
實際的tomcat6:客戶端連接到達 -> nio接收連接 -> nio使用輪詢方式讀取文本并且解析HTTP協(xié)議(單線程) -> 生成ServletRequest、ServletResponse,取出請求的Servlet -> 從線程池取出線程,并在該線程執(zhí)行這個Servlet -> 把ServletResponse的內容發(fā)送到客戶端連接 -> 關閉連接。
從上圖可以看出,BIO與NIO的不同,也導致進入客戶端處理線程的時刻有所不同:tomcat5在接受連接后馬上進入客戶端線程,在客戶端線程里解析HTTP協(xié)議,而tomcat6則是解析完HTTP協(xié)議后才進入多線程,另外,tomcat6也比5早脫離客戶端線程的環(huán)境。
實際的tomcat6與我之前猜想的差別主要集中在如何處理servlet的問題上。實際上即使拋開ThreadLocal的問題,我之前理解tomcat6只使用一個線程處理的想法其實是行不同的。大家都有經(jīng)驗:servlet是基于BIO的,執(zhí)行期間會存在堵塞的,例如讀取文件、數(shù)據(jù)庫操作等等。tomcat6使用了nio,但不可能要求servlet里面要使用nio,而一旦存在堵塞,效率自然會銳降。
|