1.介紹
server端向瀏覽器client傳送通知這種通訊模式在J2EE套用中很常見,通常使用採用RMI、CORBA或者自定義TCP/IP信息的applet來實現。這些技術往往由於複雜而產生諸多不利之處:技術難以實現、存在防火牆限制(因為需要打開非HTTP的通訊連線埠)、需要額外的server開發和維護。並且除了刷新整個頁面或者完全採用applet展示內容之外,很難找到別的方法將client端applet的狀態和瀏覽器的頁面內容集成在一起。
Pushlet是一種comet實現:在Servlet機制下,數據從server端的Java對象直接推送(push)到(動態)HTML頁面,而無需任何Java applet或者外掛程式的幫助。它使server端可以周期性地更新client的web頁面,這與傳統的request/response方式相悖。瀏覽器client為兼容JavaScript1.4版本以上的瀏覽器(如Internet Explorer、FireFox),並使用JavaScript/Dynamic HTML特性。而底層實現使用一個servlet通過Http連線到JavaScript所在的瀏覽器,並將數據推送到後者。有關JavaScript版本的知識請參看Mozilla開發中心提供的《JavaScript核心參考》和Stephen Chapman編寫的《What Version of Javascript》。
這種機制是輕量級的,它使用server端的servlet連線管理、執行緒工具、javax.servlet API,並通過標準Java特性中Object的wait()和notify()實現的生產者/消費者機制。原則上,Pushlet框架能夠運行在任何支持servlet的server上、防火牆的後面。當在client中使用JavaScript/DHTML時,Pushlet提供了通過腳本快速建立套用、使用HTML/CSS特性集成和布局新內容的便利方法。
2.動機
目前越來越多的servlet和JSP用來部署web,於是便出現了在頁面已經裝載完畢後由於server端某些對象的狀態變化而產生對client瀏覽器進行通知和同步的需要。
這些狀態變化的原因很複雜:可能由於用戶通過訪問servlet或者修改資料庫記錄、更新EJB造成,或是在多用戶套用(比如聊天室和共享白板)中的事件導致數據狀態變化。這些類型的套用常常使用一種分散式的MVC模板:模式層位於server上(可能快取在client中),控制層和視圖層位於client中(這兩個層可能合為一體)。
當然,這裡也存在需要訂閱server端動態內容的套用:那些動態內容不停地從server端推送過來。例如股票實時情報、系統狀態報告、天氣情況或者其它的監測套用。它遵循觀察者(Observer)模板(也稱為發布/訂閱模板),這種模板中的遠程client註冊成為關注於server端對象變化的觀察者。關於設計模板的知識請看Matrix Wiki上的介紹。
那么在HTML頁面已經被裝載後如何通知瀏覽器客戶端?或者如果有選擇地更新頁面中一些部分的話,那該怎么做?比如只更新在HTML Table中的那些價格發生變化的股票列?
3.問題解決方案
讓我們對套用進行這樣的假設:擁有一個Java Web Server或者Java套用server,我們試圖從server傳送通知給client端瀏覽器。這裡的解決方案可以分為:“輪詢(polling)”、“服務端回調(server-side callbacks)”和“訊息(messaging)”三類。
輪詢最簡單的解決方案便是“定時刷新頁面”。在HTML文檔的頭部使用HTML META標籤,頁面便可以每隔N秒自動reload一次。如果在此期間server端數據發生變化,那么我們可以獲得新的內容,否則將得到相同的頁面。雖然方法很簡單,但是如何設定刷新間隔是讓人頭疼的大問題。
伺服器回調因為我們是“身經百戰”的Java開發老手,所以經常會用到“服務端回調”。這種方式通過RMI或者CORBA將Server端的對象傳輸到Java applet客戶端。
訊息(MOM)這種解決方案採用一個作為client的applet,它使用TCP/IP或者無連線的UDP、甚至多播協定來建立與訊息中間件server的通訊,然後由server推送訊息給client。你可以從例如SoftWired的ibus、IBM的MQSeries、BEA的WebLogic Event這些訊息產品中直接挑選,或者自己使用基於socket的java.io.ObjectStream定製開發訊息軟體。
討論(MOM)上面三種解決方案在複雜性、安全性、性能、可測量性、瀏覽器兼容性、防火牆限制上各有優勢、劣勢。最佳解決方案依賴於你的套用需求。例如,在共享白板套用中,用戶需要直接與“狀態”互動,那么server端回調或者訊息很可能會大顯身手。
但在瀏覽器環境下,除非完全使用applet作為整個client套用,否則把來自於server的更新信息集成到頁面中並非易事。如何在applet收到回調或者訊息時變更頁面相關內容?一個很“痛快”而又“痛苦”的解決方案就是在回調方法中使用AppletContext.showDocument(URL)方法來刷新整個頁面。
由於HTML代碼可以直接影響頁面布局,直接使用來自server的數據更改HTML部分內容不是更好嗎?這是web套用的理想方案,在server上內容動態改變時,從用戶到server所需的互動是最小化的。作為對上面的解決方案的補充,我開發了Pushlet這種輕量級、瘦客戶端的技術,它無需applet或者外掛程式而直接與腳本/HTML集成在一起、使用標準HTTP連線、理論上可以部署到任何支持Java servlet的server上。但這並不意味著它將替換對前面解決方案,而是在你的開發“工具箱”中添加另一種選擇。作為Java構架者/開發者,你可以自行權衡、選擇、決定哪種適合套用的解決方案。
4.pushlet原理
Pushlet的基本使用形式是極為簡單的。後面的一些示例會說明這一點。
Pushlet基於HTTP流,這種技術常常用在多媒體視頻、通訊套用中,比如QuickTime。與裝載HTTP頁面之後馬上關閉HTTP連線的做法相反,Pushlet採用HTTP流方式將新變動的數據主動地推送到client(客戶端),再此期間HTTP連線一直保持打開。有關如何在Java中實現這種Keep-alive的長連線請參看Sun提供的《HTTP Persistent Connection》和W3C的《HTTP1.1規範》。
示例1
我們利用HTTP流開發一個JSP頁面(因為它易於部署,而且它在web server中也是作為servlet對待的),此頁面在一個定時器循環中不斷地傳送新的HTML內容給client:
<%
int i = 1
try { while (true) { out.print("<h1>"+(i++)+"</h1>")
out.flush()
try { Thread.sleep(3000)
} catch (InterruptedException e) { out.print("<h1>"+e+"</h1>")
} } } catch (Exception e) { out.print("<h1>"+e+"</h1>")
}%>
在Pushlet原始碼中提供了此頁面(examples/basics/push-html-stream.jsp)。上面的頁面並不是十分有用,因為在我們刷新頁面時,新內容機械地、持續不斷地被添加到頁面中,而不是server端更新的內容。
示例2
現在讓我們步入Pushlet工作機理中一探究竟。通過運行Pushlet的示例原始碼(examples/basics/ push-js-stream.html),我們會看到這個每3秒刷新一次的頁面。那么它是如何實現的呢?
此示例中包含了三個檔案:push-js-stream.html、push-js-stream-pusher.jsp、push-js-stream-display.html。
其中push-js-stream.html是主框架檔案,它以HTML Frame的形式包含其它兩個頁面。
push-js-stream-pusher.jsp是一個JSP,它執行在server端,此檔案內容如下:
7: <%
8: /** Start a line of JavaScript with a function call to parent frame. */
9: String jsFunPre = "<script language=JavaScript >parent.push('";
10:
11: /** End the line of JavaScript */
12: String jsFunPost = "')</script> ";
13:
14: int i = 1;
15: try {
16:
17: // Every three seconds a line of JavaScript is pushed to the client
18: while (true) {
19:
20: // Push a line of JavaScript to the client
21: out.print(jsFunPre+"Page "+(i++)+jsFunPost);
22: out.flush();
23:
24: // Sleep three SECS
25: try {
26: Thread.sleep(3000);
27: } catch (InterruptedException e) {
28: // Let client display exception
29: out.print(jsFunPre+"InterruptedException: "+e+jsFunPost);
30: }
31: }
32: } catch (Exception e) {
33: // Let client display exception
34: out.print(jsFunPre+"Exception: "+e+jsFunPost);
35: }
36: %>
注意在示例1和示例2中使用JSP時都存在一個問題:一些servlet引擎在某個client離開時會“吃掉”IOException,以至於JSP頁面將永不拋出此異常。所以在這種情況下,頁面循環將會永遠執行下去。而這正是Pushlet實現採用servlet的原因之一:可以捕獲到IOException。
在上面代碼的第21行中可以看到在一個定時器循環(3秒/周期)中列印了一些HTML並將它們輸出到client瀏覽器。請注意,這裡推送的並非HTML而是Javascript!這樣做的意義何在?它把類似“parent.push('Page 4')”的一行代碼推送到瀏覽器;而具有JavaScript引擎的瀏覽器可以直接執行收到的每一行代碼,並調用parent.push()函式。而代碼中的Parent便是瀏覽器頁面中所在Frame的Parent,也就是push-js-stream.html。讓我們看看都發生了什麼?
<script LANGUAGE="JavaScript">
var pageStart="<HTML><HEAD></HEAD><BODY BGCOLOR=blue TEXT=white><H2>Server pushes: <para>";
var pageEnd="</H2></BODY></HTML>";
// Callback function with message from server.
// This function is called from within the hidden JSP pushlet frame
function push(content) {
// Refresh the display frame with the content received
window.frames['displayFrame'].document.writeln(pageStart+content+pageEnd);
window.frames['displayFrame'].document.close();
}</script>
<!-- frame to display the content pushed by the pushlet -->
<!-- Hidden frame with the pushlet that pushes lines of JavaScript-->
</FRAMESET>
可以看到push-js-stream.html中的push()函式被名為pushletFrame的JSP Frame調用:把傳入的參數值寫入到displayFrame(此Frame為push-js-stream-display.html)。這是動態HTML的一個小技巧:使用document對象的writeln方法刷新某個Frame或者Window的內容。
於是displayFrame成為了用於顯示內容的、真正的視圖。displayFrame初始化為黑色背景並顯示“wait…”直到來自server的內容被推送過來:
WAIT...
這便是Pushlet的基本做法:我們從servlet(或者從示例中的JSP)把JavaScript代碼作為HTTP流推送到瀏覽器。這些代碼被瀏覽器的JavaScript引擎解釋並完成一些有趣的工作。於是便輕鬆地完成了從server端的Java到瀏覽器中的JavaScript的回調。
上面的示例展示了Pushlet原理,但這裡存在一些等待解決的問題和需要增添的特性。於是我建立了一個小型的server端Pushlet框架(其類結構圖表將會展示在下面),添加了一些用在client中的JavaScript庫。由於client需要依賴更多的DHTML特性(比如Layers),我們將首先粗略地溫習一些DHTML知識。示例代碼見examples/dhtml。
DHTML(動態HTML)
DHTML(動態HTML)提供了在瀏覽器中維護內容、進行用戶互動的擴展能力。就像Java開發者使用servlet和JSP那樣,DHTML也應該是你的工具箱中的一部分。
DHTML涉及到HTML、級聯樣式表(CSS)、JavaScript和DOM。傳統的頁面只能通過重新裝載來自server新頁面的方式進行更新。DHTML提供了在頁面被裝載完畢後對瀏覽器內的HTML文檔的完全控制。你應該見過一些帶有“圖像翻滾”、彈出內容、可收縮選單功能的web頁面,它們便是使用DHTML技術實現的。儘管存在一些標準上的差異(見下面的“跨瀏覽器DHTML”),多數兼容JavaScript1.4版本的瀏覽器(後面將簡稱為“版本4的瀏覽器”)都支持DHTML。
從開發者的角度審視瀏覽器中的整個文檔,比如Frame、圖片、表格等,它們都可以表示為具有層次的對象模式――DOM。通過使用JavaScript可以維護DOM的成員,不但可以改變文檔的內容和外觀,而且還可以捕捉例如滑鼠移動、form提交這些用戶事件,而後對DOM進行相應修改。例如滑鼠移動到圖片的上方可以產生“mouse-over”事件,這時通過顯示高亮版本的圖片或者彈出解釋性文字的方式修改頁面外觀。這聽起來不錯吧!我們現在就熟悉一下DHTML標準!但是誰定義了DHTML標準?
這是一些DHTML初學者首先遇到的問題。首先,你需要一個版本4以上的瀏覽器。DHTML相關規範的官方標準出自World Wide Web Consortium (W3)。但是微軟和Netscape出品的版本4以上的瀏覽器都有一些私有的DHTML擴展,這是你必須注意的。
幸運的是大多數用戶都有版本4以上的瀏覽器,而且一些開發者(Dannymen、Dan Steinman和Danny Goodman)建造了跨越瀏覽器的、可重用的DHTML庫。
作為一名Java開發者,你要接受這個事實:你應該適當地明白基於對象、甚至面向對象的JavaScript編程。在我的DHTML中你將找到一些示例,但了解更多的DHTML資源也是很值得的。尤其在使用跨越瀏覽器的DHTML庫對付那些頑固的瀏覽器問題時,一切都變得有趣、而不是枯燥。
就如Java獲得在廣闊的server端市場、DHTML在client領域具有許多強大特性那樣,Pushlet以一種直接的方式將這兩項偉大的技術捆綁在一起。下一個章節將詳細討論Pushlet這個server端輕量級框架和client端DHTML庫。
5.Pushlet框架的設計
注意:本章節僅反映了Pushlet server端框架的1.0版本(隨著版本升級可能還會重新構造)。
Pushlet框架允許client訂閱在server端的主題(subject),而server則接收訂閱,然後在server端的訂閱主題所對應的數據變化時推送數據到client。此框架的基本設計模板是發布/訂閱(Publish/Subscrib),也被稱為觀察者(Observer)。它具有server和client兩部分組建而成:
Server端:
由圍繞(見下面的UML類設計圖表)。
Client端:
腳本與頁面:可重用的JavaScript庫(pushlet.js)和用來在DHTML client(這裡指瀏覽器)中接收事件的HTML(pushlet.html)組成。
Client端Java類:
JavaPushletClient.java和JavaPushletClientListener.java,負責在Java client中接收事件。
跨越瀏覽器的DHTML工具庫:
layer.js, layer-grid.js, layer-region.js,用來在DHTML層中顯示數據內容。
最後,還有用於測試事件的生成工具類EventGenerators.java以及一些示例套用。
server端類設計
關鍵的類:Pushlet、Publisher類、Subscriber接口和Event類。通過HTTP請求調用Pushlet這個servlet,client訂閱事件並接收事件。
Client傳送訂閱請求時需要表明的內容如下:
1.訂閱事件的主題
2.接收事件所採用的格式:默認為JavaScript調用,還有XML或者Java序列化對象者三種。目前Pushlet 2.0.2版已經支持AJAX。
3.使用哪種接收協定(將來實現):TCP/IP、UDP、Multicast。
主題(subject)表示為具有層次的“主題樹”(topic-tree)形式。例如:“/stocks”表示與股票價格相關的所有事件,而“/stocks/aex”表示Amsterdam Exchange公司的股票價格。“/”表示所有事件。這並不是硬性規定,而是由開發者根據套用自行定義。
當前只有接收方協定是傳送到client的HTTP回應流(response stream)。在將來的擴展版本中,接收方協定能夠提供多種選擇,比如TCP、UDP、RMI、HTTP POST甚至只SMTP。
Event(事件)類:僅僅是name/value的字元串對(使用java.util.Properties實現)的集合。
產生Event的方式:Publisher類為生成的Event提供了發布接口,它內部保存了訂閱者(那些實現Subscriber接口的類)列表,並把每個Event傳送給那些主題與Event匹配的訂閱者。Event在server端也可以通過能夠偵聽外部Event的EventGenerators類來生成。另外client可以通過基於HTTP通訊的Postlet類來發布Event。
在上面的圖表中,為了適配不同請求源(瀏覽器、Java client程式),PushletSubscriber以及它所包含的那些類提供了多種訂閱者的實現。
Pushlet作為servlet,通過doGet/doPost方法被調用。由於多個client可以同時調用同一個Pushlet,所以Pushlet本身不能作為訂閱者。取而代之的是,它派發所有的訂閱:在每一次調用doGet()/doPost()時,新建PushletSubscriber對象、並使之運行直至事件循環(eventLoop)結束。PushletSubscriber作為一個實現Subscriber接口的對象,通過join()方法向Publisher類進行註冊的方式將自身添加到Publisher的內部列表。
面對不同的client類型和協定,PushletSubscriber建立一個相對的ClientAdapter對象,在這個場景中是BrowserPushletAdapter對象。而對於支持Multipart MIME的瀏覽器,將建立MultipartBrowserClientAdapter對象。
最後的deQueue()調用是一個“等待Event的循環”,deQueue的意思為入隊。注意此方法將掛起當前執行緒直到PushletSubscriber的GuardedQueue佇列中存入有效的Event。
每個PushletSubscriber對象都有一個GuardedQueue對象,在其中以佇列的形式保存著調用send()方法時傳入的Event。那么它為什麼不直接將Event推送給BrowserPushletAdapter呢?最重要的原因是我們期望掛起BrowserPushletAdapter執行緒,直到GuardedQueue中存在有效的Event,這樣就避免了“忙於等待”或者“輪詢”方式所帶來的負面影響。第二原因是Publisher可以通知多個client,如果在執行同步的send()調用時,某個慢速的client可能會堵塞所有其它正在等待通知的client。這正是我在RMI或者CORBA提供的一組client進行同步回調的示例中所看到的設計缺陷。
GuardedQueue是個工具類,它使用了讀/寫模板(readers-writers pattern),此模板採取java.lang.Object.wait()/notifyAll()方法實現可被監控的掛起。通過使用讀/寫模板,使GuardedQueue類具有進行對象入隊/出隊(enqueue/dequeue)操作的能力。當佇列為空時,GuardedQueue調用deQueue()方法時,此時調用執行緒將被掛起,直到有對象入隊為止。相反,當佇列已滿時調用enQueue(),執行緒也將掛起。在BrowserPushletSubscriber獲得出隊的event對象後,它將調用BrowserPushletAdapter的push()方法,後者將格式化Event為JavaScript代碼或者XML以及其它格式),並將它傳送到瀏覽器。比如Philips股票價格為123.45的JavaScript代碼格式如下:
<SCRIPT language=JavaScript >parent.push('subject', '/stocks/aex', 'philips', '123.45') </SCRIPT>
6.Pushlet套用
Pushlet可以開發多種類型的web套用。由於此框架允許client主動更新事件(通過Pushlet),所以套用就並不只是被動地推送數據了。每個Pushlet套用都可以根據下面進行分類:
事件由server發起、還是client發起或者兩者都有可能;狀態是否保持在server、還是在client或者兩者都有可能。
由於事件不但被做成了對JavaScript有效,而且也是其它腳本化的外掛程式能夠接收實時的事件更新。例如你可以腳本化Macromedia Flash或者VRML套用。
為了說明Pushlet套用的範圍,下面提供了一些簡單的demo。
例如股票、天氣、投票、機場到達系統,這些套用都可以採用Pushlet對實時數據進行監控。
遊戲從象棋到描述危機和壟斷者的遊戲。
分散式MVC這涉及到了在用戶接口框架(例如Java Swing和微軟MFC)中常見的設計模板。在分散式MVC的各種變體中,模式層位於server,而client控制著是視圖層和控制層。Client通過控制進而修改模式,然後模式將通知所有依附的視圖,而視圖將進行自我刷新。
一些套用具有web前端(front end),其數據存放在server上可被多個用戶更新。比如預訂系統和登記系統。如果一個client完成一次更新,而其它client卻不能馬上見到變化直至刷新頁面。在某些情況下,這是很簡單、可行的解決方案,但同時也存在著用戶需要同步變化的情況。這種情況下,套用可以使用Pushlet簡單地將URL作為單一事件推送到client,client接收到這個URL後將刷新頁面。
另外一點值得注意的示例是爭議頗多的EJB。儘管Java client能夠直接和EJB對話(通過RMI或者CORBA),但多數情況下則是由servlet和作為client前端的JSP來完成。在這種情況下,“通知”工作變得很艱難。使用Pushlet,EJB可以在其狀態發生改變時通知依附於它的web client。
在放棄使用PowerPonit作Java課程講解工具後,我開發了一個基於XML的內容管理框架。由於在某些情形下,教室沒有“捲軸工”,但是所有的學生人手一台網路計算機,所以我開發了這個簡單的套用,它使我能夠同步改變學生和我的頁面內容。
用戶輔助這種類型的套用可用於call center、銀行、幫助桌面、電子商務web套用。當你由於問題而撥打call center電話時,代理程式可以使你通過上網的方式瀏覽解決方案、供貨等信息。
使用EJB作為後台和JSP作為前台,client可以買/賣外幣。一個“AutoTrader”對象自動提供處理,如果自動處理失敗或者client請求人工處理時,一個“處理干預”將發生,處理者將被通知並提供相應的服務。
這是一種多用戶參加實時會話的套用。我正在計畫擴充Pushlet框架,使其支持多用戶session的特性。目前可以實現簡單的web聊天,我稱之為WCQ,大家可以在Pushlet原始碼的example中見到它。