開始
在本文中,我將展示如何使用各種不同的 Java 技術(shù)構(gòu)建一些簡單的 Comet 風(fēng)格的 Web 應(yīng)用程序。讀者對 Java Servlet、Ajax 和 JavaScript 應(yīng)該有一定的了解。我們將考察 Tomcat 和 Jetty 中一些支持 Comet 的特性,因此需要使用這兩個產(chǎn)品的最新版本。本文使用 Tomcat 6.0.14 和 Jetty 6.1.14.另外還需要一個支持 Java 5 或更高版本的 JDK.本文使用 JDK 1.5.0-16.此外還需要看看 Jetty 7 的預(yù)發(fā)布版,因?yàn)樗鼘?shí)現(xiàn)了 Servlet 3.0 規(guī)范,我們將在本文中研究該規(guī)范。
理解 Comet
您可能已經(jīng)聽說過 Comet,因?yàn)樗罱艿搅艘欢ǖ年P(guān)注。Comet 有時也稱反向 Ajax 或服務(wù)器端推技術(shù)(server-side push)。其思想很簡單:將數(shù)據(jù)直接從服務(wù)器推到瀏覽器,而不必等到瀏覽器請求數(shù)據(jù)。聽起來簡單,但是如果熟悉 Web 應(yīng)用程序,尤其是 HTTP 協(xié)議,那么您就會知道,這絕不簡單。實(shí)現(xiàn) Comet 風(fēng)格的 Web 應(yīng)用程序,同時保證在瀏覽器和服務(wù)器上的可伸縮性,這只是在最近幾年才成為可能。在本文的后面,我們將看看一些流行的 Java Web 服務(wù)器如何支持可伸縮的 Comet 架構(gòu),但首先我們來看看為什么要創(chuàng)建 Comet 應(yīng)用程序,以及用于實(shí)現(xiàn)它們的常見設(shè)計(jì)模式。
使用 Comet 的動機(jī)
HTTP 協(xié)議的成功毋庸置疑。它是 Internet 上大部分信息交換的基礎(chǔ)。然而,它也有一些局限性。特別是,它是無狀態(tài)、單向的協(xié)議。請求被發(fā)送到 Web 服務(wù)器,服務(wù)器處理請求并發(fā)回一個響應(yīng) — 僅此而已。請求必須由客戶機(jī)發(fā)出,而服務(wù)器則只能在對請求的響應(yīng)中發(fā)送數(shù)據(jù)。這至少會影響很多類型的 Web 應(yīng)用程序的實(shí)用性。典型的例子就是聊天程序。另外還有一些例子,例如比賽的比分、股票行情或電子郵件程序。
HTTP 的這些局限性也是它取得一定成功的原因。請求/響應(yīng)周期使它成為了經(jīng)典的模型,即每個連接使用一個線程。只要能夠快速為請求提供服務(wù),這種方法就有巨大的可伸縮性。每秒鐘可以處理大量的請求,只需使用少量的服務(wù)器就可以處理很大數(shù)量的用戶。對于很多經(jīng)典的 Web 應(yīng)用程序,例如內(nèi)容管理系統(tǒng)、搜索應(yīng)用程序和電子商務(wù)站點(diǎn)等等而言,這非常適合。在以上任何一種 Web 應(yīng)用程序中,服務(wù)器提供用戶請求的數(shù)據(jù),然后關(guān)閉連接,并釋放那個線程,使之可以為其他請求服務(wù)。如果提供初始數(shù)據(jù)之后仍可能存在交互,那么將連接保持為打開狀態(tài),因此線程就不能釋放出來,服務(wù)器也就不能為很多用戶服務(wù)。
但是,如果想在對請求做出響應(yīng)并發(fā)送初始數(shù)據(jù)之后,仍然保持與用戶的交互呢?在 Web 早期,這一點(diǎn)常使用 meta 刷新實(shí)現(xiàn)。這將自動指示瀏覽器在指定秒數(shù)之后重新裝載頁面,從而支持簡陋的輪詢(polling)。這不僅是一種糟糕的用戶體驗(yàn),而且通常效率非常低下。如果沒有新的數(shù)據(jù)要顯示在頁面上呢?這時不得不重新呈現(xiàn)同樣的頁面。如果對頁面的更改很少,并且頁面的大部分沒有變化呢?同樣,不管是否有必要,都得重新請求和獲取頁面上的一切內(nèi)容。
Ajax 的發(fā)明和流行改變了上述狀況。現(xiàn)在,服務(wù)器可以異步通信,因此不必重新請求整個頁面。現(xiàn)在可以進(jìn)行增量式的更新。只需使用 XMLHttpRequest 輪詢服務(wù)器。這項(xiàng)技術(shù)通常被稱作 Comet.這項(xiàng)技術(shù)存在一些變體,每種變體具有不同的性能和可伸縮性。我們來看看這些不同風(fēng)格的 Comet.
Comet 風(fēng)格
Ajax 的出現(xiàn)使 Comet 成為可能。HTTP 的單向性質(zhì)可以有效地加以規(guī)避。實(shí)際上有一些不同的方法可以繞過這一點(diǎn)。您可能已經(jīng)猜到,支持 Comet 的最容易的方式是輪詢(poll)。使用 XMLHttpRequest 向服務(wù)器發(fā)出調(diào)用,返回后,等待一段固定的時間(通常使用 JavaScript 的 setTimeout 函數(shù)),然后再次調(diào)用。這是一項(xiàng)非常常見的技術(shù)。例如,大多數(shù) webmail 應(yīng)用程序就是通過這種技術(shù)在電子郵件到達(dá)時顯示電子郵件的。
這項(xiàng)技術(shù)有優(yōu)點(diǎn)也有缺點(diǎn)。在這種情況下,您期望快速返回響應(yīng),就像任何其他 Ajax 請求一樣。在請求之間必須有一段暫停。否則,連續(xù)不斷的請求會沖垮服務(wù)器,并且這種情況下顯然不具有可伸縮性。這段暫停使應(yīng)用程序產(chǎn)生一個延時。暫停的時間越長,服務(wù)器上的新數(shù)據(jù)就需要越多的時間才能到達(dá)客戶機(jī)。如果縮短暫停時間,又將重新面臨沖垮服務(wù)器的風(fēng)險(xiǎn)。但是另一方面,這顯然是最簡單的實(shí)現(xiàn) Comet 的方式。
現(xiàn)在應(yīng)該指出,很多人認(rèn)為輪詢并不屬于 Comet.相反,他們認(rèn)為 Comet 是對輪詢的局限性的一個解決方案。最常見的 “真正的” Comet 技術(shù)是輪詢的一種變體,即長輪詢(long polling)。輪詢與長輪詢之間的主要區(qū)別在于服務(wù)器花多長的時間作出響應(yīng)。長輪詢通常將連接保持一段較長的時間 — 通常是數(shù)秒鐘,但是也可能是一分鐘甚至更長。當(dāng)服務(wù)器上發(fā)生某個事件時,響應(yīng)被發(fā)送并隨即關(guān)閉,輪詢立即重新開始。
長輪詢相對于一般輪詢的優(yōu)點(diǎn)在于,數(shù)據(jù)一旦可用,便立即從服務(wù)器發(fā)送到客戶機(jī)。請求可能等待較長的時間,期間沒有任何數(shù)據(jù)返回,但是一旦有了新的數(shù)據(jù),它將立即被發(fā)送到客戶機(jī)。因此沒有延時。如果您使用過基于 Web 的聊天程序,或者聲稱 “實(shí)時” 的任何程序,那么它很可能就是使用了這種技術(shù)。
長輪詢有一種變體,這是第三種風(fēng)格的 Comet.這通常被稱為流(streaming)。按照這種風(fēng)格,服務(wù)器將數(shù)據(jù)推回客戶機(jī),但是不關(guān)閉連接。連接將一直保持開啟,直到過期,并導(dǎo)致重新發(fā)出請求。XMLHttpRequest 規(guī)范表明,可以檢查 readyState 的值是否為 3 或 Receiving(而不是 4 或 Loaded),并獲取正從服務(wù)器 “流出” 的數(shù)據(jù)。和長輪詢一樣,這種方式也沒有延時。當(dāng)服務(wù)器上的數(shù)據(jù)就緒時,該數(shù)據(jù)被發(fā)送到客戶機(jī)。這種方式的另一個優(yōu)點(diǎn)是可以大大減少發(fā)送到服務(wù)器的請求,從而避免了與設(shè)置服務(wù)器連接相關(guān)的開銷和延時。不幸的是,XMLHttpRequest 在不同的瀏覽器中有很多不同的實(shí)現(xiàn)。這項(xiàng)技術(shù)只能在較新版本的 Mozilla Firefox 中可靠地使用。對于 Internet Explorer 或 Safari,仍需使用長輪詢。
至此,您可能會想,長輪詢和流都有一個很大的問題。請求需要在服務(wù)器上存在一段較長的時間。這打破了每個請求使用一個線程的模型,因?yàn)橛糜谝粋€請求的線程一直沒有被釋放。更糟糕的是,除非要發(fā)回?cái)?shù)據(jù),否則該線程一直處于空閑狀態(tài)。這顯然不具有可伸縮性。幸運(yùn)的是,現(xiàn)代 Java Web 服務(wù)器有很多方式可以解決這個問題。
Java 中的 Comet
現(xiàn)在有很多 Web 服務(wù)器是用 Java 構(gòu)建的。一個原因是 Java 有一個豐富的本地線程模型。因此實(shí)現(xiàn)典型的每個連接一個線程的模型便非常簡單。該模型對于 Comet 不大適用,但是,Java 對此同樣有解決的辦法。為了有效地處理 Comet,需要非阻塞 IO,Java 通過它的 NIO 庫提供非阻塞 IO.兩種最流行的開源服務(wù)器 Apache Tomcat 和 Jetty 都利用 NIO 增加非阻塞 IO,從而支持 Comet.然而,這兩種服務(wù)器中的實(shí)現(xiàn)卻各不相同。我們來看看 Tomcat 和 Jetty 對 Comet 的支持。
Tomcat 和 Comet
對于 Apache Tomcat,要使用 Comet,主要需要做兩件事。首先,需要對 Tomcat 的配置文件 server.XML 稍作修改。默認(rèn)情況下啟用的是更典型的同步 IO 連接器。現(xiàn)在只需將它切換成異步版本,如清單 1 所示。
清單 1. 修改 Tomcat 的 server.xml
1
2
3
4
5
|
<!-- This is the usual Connector, comment it out and add the NIO one --> <!-- Connector URIEncoding="utf-8" connectionTimeout="20000" port="8084" protocol="HTTP/1.1" redirectPort="8443"/ --> < Connector connectionTimeout = "20000" port = "8080" protocol="org.apache. coyote.http11.Http11NioProtocol" redirectPort = "8443" /> |
Servlet.這顯然是 Tomcat 特有的一個接口。清單 2 顯示了一個這樣的例子。
清單 2. Tomcat Comet servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class TomcatWeatherServlet extends HttpServlet implements CometProcessor { private MessageSender messageSender = null ; private static final Integer TIMEOUT = 60 * 1000 ; @Override public void destroy() { messageSender.stop(); messageSender = null ; } @Override public void init() throws ServletException { messageSender = new MessageSender(); Thread messageSenderThread = new Thread(messageSender, "MessageSender[" + getServletContext() .getContextPath() + "]" ); messageSenderThread.setDaemon( true ); messageSenderThread.start(); } public void event( final CometEvent event) throws IOException, ServletException { HttpServletRequest request = event.getHttpServletRequest(); HttpServletResponse response = event.getHttpServletResponse(); if (event.getEventType() == CometEvent.EventType.BEGIN) { request.setAttribute( "org.apache.tomcat.comet.timeout" , TIMEOUT); log( "Begin for session: " + request.getSession( true ).getId()); messageSender.setConnection(response); Weatherman weatherman = new Weatherman( 95118 , 32408 ); new Thread(weatherman).start(); } else if (event.getEventType() == CometEvent.EventType.ERROR) { log( "Error for session: " + request.getSession( true ).getId()); event.close(); } else if (event.getEventType() == CometEvent.EventType.END) { log( "End for session: " + request.getSession( true ).getId()); event.close(); } else if (event.getEventType() == CometEvent.EventType.READ) { throw new UnsupportedOperationException("This servlet does not accept data"); } } } |
CometProcessor 接口要求實(shí)現(xiàn) event 方法。這是用于 Comet 交互的一個生命周期方法。Tomcat 將使用不同的 CometEvent 實(shí)例調(diào)用。通過檢查 CometEvent 的 eventType,可以判斷正處在生命周期的哪個階段。當(dāng)請求第一次傳入時,即發(fā)生 BEGIN 事件。READ 事件表明數(shù)據(jù)正在被發(fā)送,只有當(dāng)請求為 POST 時才需要該事件。遇到 END 或 ERROR 事件時,請求終止。
在清單 2 的例子中,Servlet 使用一個 MessageSender 類發(fā)送數(shù)據(jù)。這個類的實(shí)例是在 servlet 的 init 方法中在其自身的線程中創(chuàng)建,并在 servlet 的 destroy 方法中銷毀的。清單 3 顯示了 MessageSender.
清單 3. MessageSender
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
private class MessageSender implements Runnable { protected boolean running = true ; protected final ArrayList messages = new ArrayList(); private ServletResponse connection; private synchronized void setConnection(ServletResponse connection){ this .connection = connection; notify(); } public void send(String message) { synchronized (messages) { messages.add(message); log( "Message added #messages=" + messages.size()); messages.notify(); } } public void run() { while (running) { if (messages.size() == 0 ) { try { synchronized (messages) { messages.wait(); } } catch (InterruptedException e) { // Ignore } } String[] pendingMessages = null ; synchronized (messages) { pendingMessages = messages.toArray( new String[ 0 ]); messages.clear(); } try { if (connection == null ){ try { synchronized ( this ){ wait(); } } catch (InterruptedException e){ // Ignore } } PrintWriter writer = connection.getWriter(); for ( int j = 0 ; j < pendingMessages.length; j++) { final String forecast = pendingMessages[j] + " "; writer.println(forecast); log( "Writing:" + forecast); } writer.flush(); writer.close(); connection = null ; log( "Closing connection" ); } catch (IOException e) { log( "IOExeption sending message" , e); } } } } |
這個類基本上是樣板代碼,與 Comet 沒有直接的關(guān)系。但是,有兩點(diǎn)要注意。這個類含有一個 ServletResponse 對象。回頭看看清單 2 中的 event 方法,當(dāng)事件為 BEGIN 時,response 對象被傳入到 MessageSender 中。在 MessageSender 的 run 方法中,它使用 ServletResponse 將數(shù)據(jù)發(fā)送回客戶機(jī)。注意,一旦發(fā)送完所有排隊(duì)等待的消息后,它將關(guān)閉連接。這樣就實(shí)現(xiàn)了長輪詢。如果要實(shí)現(xiàn)流風(fēng)格的 Comet,那么需要使連接保持開啟,但是仍然刷新數(shù)據(jù)。
回頭看清單 2 可以發(fā)現(xiàn),其中創(chuàng)建了一個 Weatherman 類。正是這個類使用 MessageSender 將數(shù)據(jù)發(fā)送回客戶機(jī)。這個類使用 Yahoo RSS feed 獲得不同地區(qū)的天氣信息,并將該信息發(fā)送到客戶機(jī)。這是一個特別設(shè)計(jì)的例子,用于模擬以異步方式發(fā)送數(shù)據(jù)的數(shù)據(jù)源。清單 4 顯示了它的代碼。
清單 4. Weatherman
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
private class Weatherman implements Runnable{ private final List zipCodes; private final String YAHOO_WEATHER = "http://weather.yahooapis.com/forecastrss?p=" ; public Weatherman(Integer... zips) { zipCodes = new ArrayList(zips.length); for (Integer zip : zips) { try { zipCodes.add( new URL(YAHOO_WEATHER + zip)); } catch (Exception e) { // dont add it if it sucks } } } public void run() { int i = 0 ; while (i >= 0 ) { int j = i % zipCodes.size(); SyndFeedInput input = new SyndFeedInput(); try { SyndFeed feed = input.build( new InputStreamReader(zipCodes.get(j) .openStream())); SyndEntry entry = (SyndEntry) feed.getEntries().get( 0 ); messageSender.send(entryToHtml(entry)); Thread.sleep(30000L); } catch (Exception e) { // just eat it, eat it } i++; } } private String entryToHtml(SyndEntry entry){ StringBuilder html = new StringBuilder(" "); html.append(entry.getTitle()); html.append(" "); html.append(entry.getDescription().getValue()); return html.toString(); } } |
這個類使用 Project Rome 庫解析來自 Yahoo Weather 的 RSS feed.如果需要生成或使用 RSS 或 Atom feed,這是一個非常有用的庫。此外,這個代碼中只有一個地方值得注意,那就是它產(chǎn)生另一個線程,用于每過 30 秒鐘發(fā)送一次天氣數(shù)據(jù)。最后,我們再看一個地方:使用該 Servlet 的客戶機(jī)代碼。在這種情況下,一個簡單的 JSP 加上少量的 JavaScript 就足夠了。清單 5 顯示了該代碼。
清單 5. 客戶機(jī) Comet 代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
"http://www.w3.org/TR/html4/loose.dtd" > var request = new XMLHttpRequest(); request.open( "GET" , url, true ); request.setRequestHeader( "Content-Type" , "application/x-javascript; " ); request.onreadystatechange = function () { if (request.readyState == 4) { if (request.status == 200){ if (request.responseText) { document.getElementById( "forecasts" ).innerHTML = request.responseText; } } go(); } }; request.send( null ); } |
該代碼只是在用戶單擊 Go 按鈕時開始長輪詢。注意,它直接使用 XMLHttpRequest 對象,所以這在 Internet Explorer 6 中將不能工作。您可能需要使用一個 Ajax 庫解決瀏覽器差異問題。除此之外,惟一需要注意的是回調(diào)函數(shù),或者為請求的 onreadystatechange 函數(shù)創(chuàng)建的閉包。該函數(shù)粘貼來自服務(wù)器的新的數(shù)據(jù),然后重新調(diào)用 go 函數(shù)。
現(xiàn)在,我們看過了一個簡單的 Comet 應(yīng)用程序在 Tomcat 上是什么樣的。有兩件與 Tomcat 密切相關(guān)的事情要做:一是配置它的連接器,二是在 Servlet 中實(shí)現(xiàn)一個特定于 Tomcat 的接口。您可能想知道,將該代碼 “移植” 到 Jetty 有多大難度。接下來我們就來看看這個問題。
Jetty 和 Comet
Jetty 服務(wù)器使用稍微不同的技術(shù)來支持 Comet 的可伸縮的實(shí)現(xiàn)。Jetty 支持被稱作 continuations 的編程結(jié)構(gòu)。其思想很簡單。請求先被暫停,然后在將來的某個時間點(diǎn)再繼續(xù)。規(guī)定時間到期,或者某種有意義的事件發(fā)生,都可能導(dǎo)致請求繼續(xù)。當(dāng)請求被暫停時,它的線程被釋放。
可以使用 Jetty 的 org.mortbay.util.ajax.ContinuationSupport 類為任何 HttpServletRequest 創(chuàng)建 org.mortbay.util.ajax.Continuation 的一個實(shí)例。這種方法與 Comet 有很大的不同。但是,continuations 可用于實(shí)現(xiàn)邏輯上等效的 Comet.清單 6 顯示清單 2 中的 weather servlet “移植” 到 Jetty 后的代碼。
清單 6. Jetty Comet servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class JettyWeatherServlet extends HttpServlet { private MessageSender messageSender = null ; private static final Integer TIMEOUT = 5 * 1000 ; public void begin(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { request.setAttribute( "org.apache.tomcat.comet" , Boolean.TRUE); request.setAttribute( "org.apache.tomcat.comet.timeout" , TIMEOUT); messageSender.setConnection(response); Weatherman weatherman = new Weatherman( 95118 , 32408 ); new Thread(weatherman).start(); } public void end(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { synchronized (request) { request.removeAttribute( "org.apache.tomcat.comet" ); Continuation continuation = ContinuationSupport.getContinuation (request, request); if (continuation.isPending()) { continuation.resume(); } } } public void error(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { end(request, response); } public boolean read(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { throw new UnsupportedOperationException(); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { synchronized (request) { Continuation continuation = ContinuationSupport.getContinuation (request, request); if (!continuation.isPending()) { begin(request, response); } Integer timeout = (Integer) request.getAttribute ( "org.apache.tomcat.comet.timeout" ); boolean resumed = continuation.suspend(timeout == null ? 10000 : timeout.intValue()); if (!resumed) { error(request, response); } } } public void setTimeout(HttpServletRequest request, HttpServletResponse response, int timeout) throws IOException, ServletException, UnsupportedOperationException { request.setAttribute( "org.apache.tomcat.comet.timeout" , new Integer(timeout)); } } |
這里最需要注意的是,該結(jié)構(gòu)與 Tomcat 版本的代碼非常類似。begin、read、end 和 error 方法都與 Tomcat 中相同的事件匹配。該 Servlet 的 service 方法被覆蓋為在請求第一次進(jìn)入時創(chuàng)建一個 continuation 并暫停該請求,直到超時時間已到,或者發(fā)生導(dǎo)致它重新開始的事件。上面沒有顯示 init 和 destroy 方法,因?yàn)樗鼈兣c Tomcat 版本是一樣的。該 servlet 使用與 Tomcat 相同的 MessageSender.因此不需要修改。注意 begin 方法如何創(chuàng)建 Weatherman 實(shí)例。對這個類的使用與 Tomcat 版本中也是完全相同的。甚至客戶機(jī)代碼也是一樣的。只有 servlet 有更改。雖然 servlet 的變化比較大,但是與 Tomcat 中的事件模型仍是一一對應(yīng)的。
希望這足以鼓舞人心。雖然完全相同的代碼不能同時在 Tomcat 和 Jetty 中運(yùn)行,但是它是非常相似的。當(dāng)然,JavaEE 吸引人的一點(diǎn)是可移植性。大多數(shù)在 Tomcat 中運(yùn)行的代碼,無需修改就可以在 Jetty 中運(yùn)行,反之亦然。因此,毫不奇怪,下一個版本的 Java Servlet 規(guī)范包括異步請求處理(即 Comet 背后的底層技術(shù))的標(biāo)準(zhǔn)化。 我們來看看這個規(guī)范:Servlet 3.0 規(guī)范。
Servlet 3.0 規(guī)范
在此,我們不深究 Servlet 3.0 規(guī)范的全部細(xì)節(jié),只看看 Comet servlet 如果在 Servlet 3.0 容器中運(yùn)行,可能會是什么樣子。注意 “可能” 二字。該規(guī)范已經(jīng)發(fā)布公共預(yù)覽版,但在撰寫本文之際,還沒有最終版。因此,清單 7 顯示的是遵從公共預(yù)覽規(guī)范的一個實(shí)現(xiàn)。
清單 7. Servlet 3.0 Comet
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@WebServlet (asyncSupported= true , asyncTimeout= 5000 ) public class WeatherServlet extends HttpServlet { private MessageSender messageSender; // init and destroy are the same as other @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext async = request.startAsync(request, response); messageSender.setConnection(async); Weatherman weatherman = new Weatherman( 95118 , 32444 ); async.start(weatherman); ; } } |
值得高興的是,這個版本要簡單得多。平心而論,如果不遵從 Tomcat 的事件模型,在 Jetty 中可以有類似的實(shí)現(xiàn)。這種事件模型似乎比較合理,很容易在 Tomcat 以外的容器(例如 Jetty)中實(shí)現(xiàn),只是沒有相關(guān)的標(biāo)準(zhǔn)。
回頭看看清單 7,注意它的標(biāo)注聲明它支持異步處理,并設(shè)置了超時時間。startAsync 方法是 HttpServletRequest 上的一個新方法,它返回新的 javax.servlet.AsyncContext 類的一個實(shí)例。注意,MessageSender 現(xiàn)在傳遞 AsynContext 的引用,而不是 ServletResponse 的引用。在這里,不應(yīng)該關(guān)閉響應(yīng),而是調(diào)用 AsyncContext 實(shí)例上的 complete 方法。還應(yīng)注意,Weatherman 被直接傳遞到 AsyncContext 實(shí)例的 start 方法。這樣將在當(dāng)前 ServletContext 中開始一個新線程。
而且,盡管與 Tomcat 或 Jetty 相比都有較大的不同,但是修改相同風(fēng)格的編程來處理 Servlet 3.0 規(guī)范提議的 API 并不是太難。還應(yīng)注意,Jetty 7 是為實(shí)現(xiàn) Servlet 3.0 而設(shè)計(jì)的,目前處于 beta 狀態(tài)。但是,在撰寫本文之際,它還沒有實(shí)現(xiàn)該規(guī)范的最新版本。
結(jié)束語
Comet 風(fēng)格的 Web 應(yīng)用程序可以為 Web 帶來全新的交互性。它為大規(guī)模地實(shí)現(xiàn)這些特性帶來一些復(fù)雜的挑戰(zhàn)。但是,領(lǐng)先的 Java Web 服務(wù)器正在為實(shí)現(xiàn) Comet 提供成熟、穩(wěn)定的技術(shù)。在本文中,您看到了 Tomcat 和 Jetty 上當(dāng)前風(fēng)格的 Comet 的不同點(diǎn)和相似點(diǎn),以及正在進(jìn)行的 Servlet 3.0 規(guī)范的標(biāo)準(zhǔn)化。Tomcat 和 Jetty 使如今構(gòu)建可伸縮的 Comet 應(yīng)用程序成為可能,并且明確了未來面向 Servlet 3.0 標(biāo)準(zhǔn)化的升級路線。