為了使用 Continuatins,Jetty 必須配置為使用它的 SelectChannelConnector 處理請(qǐng)求。這個(gè) connector 構(gòu)建在 java.nio API 之上,允許它維持每個(gè)連接開放而不用消耗一個(gè)線程。當(dāng)使用 SelectChannelConnector 時(shí),ContinuationSupport.getContinuation() 提供一個(gè) SelectChannelConnector.RetryContinuation 實(shí)例(但是,您必須針對(duì) Continuation 接口編程)。當(dāng)在 RetryContinuation 上調(diào)用 suspend() 時(shí),它拋出一個(gè)特殊的運(yùn)行時(shí)異常 -- RetryRequest,該異常傳播到 servlet 外并且回溯到 filter 鏈,最后被 SelectChannelConnector 捕獲。但是不會(huì)發(fā)送一個(gè)異常響應(yīng)給客戶端,而是將請(qǐng)求維持在未決 Continuations 隊(duì)列里,則 HTTP 連接保持開放。這樣,用來(lái)服務(wù)請(qǐng)求的線程返回給 ThreadPool,然后又可以用來(lái)服務(wù)其他請(qǐng)求。暫停的請(qǐng)求停留在未決 Continuations 隊(duì)列里直到指定的過(guò)期時(shí)間,或者在它的 Continuation 上調(diào)用 resume() 方法。當(dāng)任何一個(gè)條件觸發(fā)時(shí),請(qǐng)求會(huì)重新提交給 servlet(通過(guò) filter 鏈)。這樣,整個(gè)請(qǐng)求被"重播"直到 RetryRequest 異常不再拋出,然后繼續(xù)按正常情況執(zhí)行。此,在 BlockingServlet 和 ContinuationServlet 兩種情況中,請(qǐng)求被放入隊(duì)列中以訪問(wèn)單個(gè) servlet 線程。然而,雖然 servlet 線程執(zhí)行期間 BlockingServlet 發(fā)生兩秒暫停,SelectChannelConnector 中的 ContinuationServlet 的暫停發(fā)生在 servlet 之外。ContinuationServlet 的總吞吐量更高一些,因?yàn)?servlet 線程沒有將大部分時(shí)間用在 sleep() 調(diào)用中。使 Continuations 變得有用現(xiàn)在您已經(jīng)了解到 Continuations 能夠不消耗線程就可以暫停 servlet 請(qǐng)求,我需要進(jìn)一步解釋 Continuations API 以向您展示如何在實(shí)際應(yīng)用中使用。resume() 方法生成一對(duì) suspend()??梢詫⑺鼈円暈闃?biāo)準(zhǔn)的 Object wait()/notify() 機(jī)制的 Continuations 等價(jià)體。就是說(shuō),suspend() 使 Continuation(因此也包括當(dāng)前方法的執(zhí)行)處于暫停狀態(tài),直到超出時(shí)限,或者另一個(gè)線程調(diào)用 resume()。suspend()/resume() 對(duì)于實(shí)現(xiàn)真正使用 Continuations 的 Comet 風(fēng)格的服務(wù)非常關(guān)鍵。其基本模式是:從當(dāng)前請(qǐng)求獲得 Continuation,調(diào)用 suspend(),等待異步事件的到來(lái)。然后調(diào)用 resume() 并生成一個(gè)響應(yīng)。然而,與 Scheme 這種語(yǔ)言中真正的語(yǔ)言級(jí)別的 continuations 或者是 Java 語(yǔ)言的 wait()/notify() 范例不同的是,對(duì) Jetty Continuation 調(diào)用 resume() 并不意味著代碼會(huì)從中斷的地方繼續(xù)執(zhí)行。正如您剛剛看到的,實(shí)際上和 Continuation 相關(guān)的請(qǐng)求被重新處理。這會(huì)產(chǎn)生兩個(gè)問(wèn)題:重新執(zhí)行 清單 4 中的 ContinuationServlet 代碼,以及丟失狀態(tài):即調(diào)用 suspend() 時(shí)丟失作用域內(nèi)所有內(nèi)容。第一個(gè)問(wèn)題的解決方法是使用 isPending() 方法。如果 isPending() 返回值為 true,這意味著之前已經(jīng)調(diào)用過(guò)一次 suspend(),而重新執(zhí)行請(qǐng)求時(shí)還沒有發(fā)生第二次 suspend() 調(diào)用。換言之,根據(jù) isPending() 條件在執(zhí)行 suspend() 調(diào)用之前運(yùn)行代碼,這樣將確保對(duì)每個(gè)請(qǐng)求只執(zhí)行一次。在 suspend() 調(diào)用具有等冪性之前,最好先對(duì)應(yīng)用程序進(jìn)行設(shè)計(jì),這樣即使調(diào)用兩次也不會(huì)出現(xiàn)問(wèn)題,但是某些情況下無(wú)法使用 isPending() 方法。Continuation 也提供了一種簡(jiǎn)單的機(jī)制來(lái)保持狀態(tài):putObject(Object) 和 getObject() 方法。在 Continuation 發(fā)生暫停時(shí),使用這兩種方法可以保持上下文對(duì)象以及需要保存的狀態(tài)。您還可以使用這種機(jī)制作為在線程之間傳遞事件數(shù)據(jù)的方式,稍后將演示這種方法。DWR 2 最新引入了 Reverse Ajax 概念。這種機(jī)制可以將服務(wù)器端事件 “推入” 到客戶機(jī)。客戶端 DWR 代碼透明地處理已建立的連接并解析響應(yīng),因此從開發(fā)人員的角度來(lái)看,事件是從服務(wù)器端 Java 代碼輕松地發(fā)布到客戶機(jī)中。DWR 經(jīng)過(guò)配置之后可以使用 Reverse Ajax 的三種不同機(jī)制。第一種就是較為熟悉的輪詢方法。第二種稱為 piggyback,這種機(jī)制并不創(chuàng)建任何到服務(wù)器的連接,相反,將一直等待直至發(fā)生另一個(gè) DWR 服務(wù),piggybacks 使事件等待該請(qǐng)求的響應(yīng)。這使它具有較高的效率,但也意味著客戶機(jī)事件通知被延遲到直到發(fā)生另一個(gè)不相關(guān)的客戶機(jī)調(diào)用。最后一種機(jī)制使用長(zhǎng)期的、 Comet 風(fēng)格的連接。最妙的是,當(dāng)運(yùn)行在 Jetty 下時(shí),DWR 能夠自動(dòng)檢測(cè)并切換為使用 Contiuations,實(shí)現(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 更新頁(yè)面如果希望最小化應(yīng)用程序中使用的 JavaScript 代碼的數(shù)量,可以使用 ScriptSession 編寫 JavaScript 回調(diào):將 ScriptSession 實(shí)例封裝在 DWR Util 對(duì)象中。該類將提供直接操作瀏覽器 DOM 的簡(jiǎn)單 Java 方法,并在后臺(tái)自動(dòng)生成所需的腳本。 tomcat5:客戶端連接到達(dá) -> 傳統(tǒng)的SeverSocket.accept接收連接 -> 從線程池取出一個(gè)線程 -> 在該線程讀取文本并且解析HTTP協(xié)議 -> 在該線程生成ServletRequest、ServletResponse,取出請(qǐng)求的Servlet -> 在該線程執(zhí)行這個(gè)Servlet -> 在該線程把ServletResponse的內(nèi)容發(fā)送到客戶端連接 -> 關(guān)閉連接?! ∥乙郧袄斫獾氖褂胣io后的tomcat6:客戶端連接到達(dá) -> nio接收連接 -> nio使用輪詢方式讀取文本并且解析HTTP協(xié)議(單線程) -> 生成ServletRequest、ServletResponse,取出請(qǐng)求的Servlet -> 直接在本線程執(zhí)行這個(gè)Servlet -> 把ServletResponse的內(nèi)容發(fā)送到客戶端連接 -> 關(guān)閉連接。 實(shí)際的tomcat6:客戶端連接到達(dá) -> nio接收連接 -> nio使用輪詢方式讀取文本并且解析HTTP協(xié)議(單線程) -> 生成ServletRequest、ServletResponse,取出請(qǐng)求的Servlet -> 從線程池取出線程,并在該線程執(zhí)行這個(gè)Servlet -> 把ServletResponse的內(nèi)容發(fā)送到客戶端連接 -> 關(guān)閉連接?! 纳蠄D可以看出,BIO與NIO的不同,也導(dǎo)致進(jìn)入客戶端處理線程的時(shí)刻有所不同:tomcat5在接受連接后馬上進(jìn)入客戶端線程,在客戶端線程里解析HTTP協(xié)議,而tomcat6則是解析完HTTP協(xié)議后才進(jìn)入多線程,另外,tomcat6也比5早脫離客戶端線程的環(huán)境。 實(shí)際的tomcat6與我之前猜想的差別主要集中在如何處理servlet的問(wèn)題上。實(shí)際上即使拋開ThreadLocal的問(wèn)題,我之前理解tomcat6只使用一個(gè)線程處理的想法其實(shí)是行不同的。大家都有經(jīng)驗(yàn):servlet是基于BIO的,執(zhí)行期間會(huì)存在堵塞的,例如讀取文件、數(shù)據(jù)庫(kù)操作等等。tomcat6使用了nio,但不可能要求servlet里面要使用nio,而一旦存在堵塞,效率自然會(huì)銳降。
聯(lián)系客服