服務器推送技術 java
下面介紹在ARP之上的一個非常熱門的技術實現:服務器推送技術。
服務器推送技術(Server Push)是最近Web技術中最熱門的一個流行術語,它的別名叫Comet(彗星)。它是繼AJAX之后又一個倍受追捧的Web技術。服務器推送技術最近的流行與AJAX有著密切的關系。
隨著Web技術的流行,越來越多的應用從原有的C/S模式轉變為B/S模式,享受著Web技術所帶來的各種優勢(例如跨平臺、免客戶端維護、跨越防火墻、擴展性好等)。但是基于瀏覽器的應用,也有它不足的地方。主要在于界面的友好性和交互性。由于瀏覽器中的頁面每次需要全部刷新才能從服務器端獲得最新的數據或向服務器傳送數據,這樣產生的延遲所帶來的視覺感受非常糟糕。因此很多的桌面應用為了獲得更友好的界面放棄了Web技術,或者采用瀏覽器的插件技術(ActiveX、Applet、Flash等)。但是瀏覽器插件技術本身又有許多問題,例如跨平臺問題和插件版本兼容性問題。
隨著AJAX技術的興起,讓廣大開發人員又一次看到了使用瀏覽器來替代桌面應用的機會,并且這次機會非常大。AJAX將整個頁面的刷新變成頁面局部的刷新,并且數據的傳送是以異步方式進行,這使得網絡延遲帶來的視覺差異將會消失。AJAX還利用DHTML和豐富的JavasSript語言來模擬桌面系統的各種事件和響應過程,以及平滑滾動和拖拽的效果。還不止這些,更有一些IT巨頭(Google、Sun、Oracle等)提供了非常豐富的AJAX開發工具,使得開發和調試AJAX應用變得簡單高效,并且開發的AJAX應用還可以跨越各種瀏覽器和操作系統。在這種情況下基于AJAX的Web應用迅速涌起,吞噬著原有桌面系統的份額。聊天工具、郵件閱讀器、博客編輯器,甚至是Office辦公軟件和文字處理軟件在瀏覽器中都有著美麗的外觀和幾乎可以與桌面系統媲美的交互界面。Google更是提出“有了瀏覽器和Google,就不需要微軟”的口號和策略。在AJAX的世界中,除了傳統的CAD設計軟件和大型游戲軟件等因為對系統硬件的苛刻需求,還離不開桌面系統以外,似乎其他所有的應用都可以變成Web應用了。
但是,在瀏覽器中的AJAX應用中存在一個致命的缺陷無法滿足傳統桌面系統的需求。那就是“服務器發起的消息傳遞(Server-Initiated Message Delivery)”。在很多的應用當中,服務器軟件需要向客戶端主動發送消息或信息。因為服務器掌握著系統的主要資源,能夠最先獲得系統的狀態變化和事件的發生。當這些變化發生的時候,服務器需要主動地向客戶端實時地發送消息。例如股票的變化。在傳統的桌面系統中,這種需求沒有任何問題,因為客戶端和服務器之間通常存在著持久的連接,這個連接可以雙向傳遞各種數據。而基于HTTP協議的Web應用卻不行。上節中也提到過,在Web世界中,服務器永遠是被動地發送數據,前提是客戶端必須先發送請求。瀏覽器其實并不知道服務器的信息什么時候會有改變,為了模擬實時的交流,或者不想錯過某些信息,只能通過輪詢(Polling)技術不斷刷新頁面來獲得最新的數據(見圖18-5)。這種方式不但浪費服務器的資源,最重要的是每次建立(或關閉)新的HTTP連接都有一定的延遲,這種延遲使得頻繁信息傳遞的應用無法忍受。于是就產生了“服務器推送技術”。
圖18-5 Web請求的輪詢技術
“服務器推送技術”在很久以前就出現過。例如Netscape曾經推出適用于Push技術的專用瀏覽器和經過修改的HTML語言。但是這僅僅在特定的瀏覽器中才能使用,其他流行的瀏覽器(IE等)就不兼容這種技術。
現在的“服務器推送技術”是保持原有的HTTP協議不變,在服務器端改變處理方式,使得服務器能夠使用瀏覽器已經打開的HTTP連接,主動向瀏覽器發送消息(見圖18-6)。這里關鍵的技術是要保持原有的HTTP連接不斷。一旦擁有持久的連接,服務器就可以根據自己的數據更新,隨時地向客戶端發送最新的信息。
圖18-6 服務器數據推送技術
在GlassFish中,Grizzly通過NIO的技術實現了異步請求服務(ARP),并在ARP之上擴展了服務器推送技術的實現,將其也命名為“Comet”。因為使用了NIO,Grizzly才可以在保持HTTP連接的同時,并不會綁定固定的線程,使得GlassFish具有很好的擴展性,可以很好地同時支持大量的Comet請求。下面我們來分析Grizzly中對Comet的實現。
18.2.1 Comet實現的分析
如圖18-7所示,Comet的實現是基于ARP之上的,因此整個框架結構仍然符合ARP的模式。讀者可以與“新郵件提醒功能”做一個比較,大部分的代碼都相類似。最大的不同就是“新郵件提醒功能”是Grizzly的一個擴展,而Comet卻已經是Grizzly的一部分,它與其他Grizzly的核心Java包位于同樣重要的位置。所有的Comet的實現都在com.sun. enterprise.web.connector.grizzly.comet包中。
因為是ARP的擴展,所以它的入口仍然是AsyncFilter接口的實現。Comet對AsyncFilter接口的實現是CometAsyncFilter類。這個類的注冊比“新郵件提醒功能”要簡單,只需要在GlassFish的啟動配置文件(domain.xml)中加上<property name="cometSupport" value="true"/>就行了,在SelectorThreadConfig類中就會讀取到(見例18.12),并且調用SelectorThread中的enableCometSupport方法(見例18.13)將CometAsyncFilter類注冊到系統。
圖18-7 Comet實現類結構圖
【例18.12】在SelectorThreadConfig類中打開Comet功能:
if (System.getProperty(ENABLE_COMET_SUPPORT) != null){
?? selectorThread.enableCometSupport(
?? Boolean.valueOf(System.getProperty(ENABLE_COMET_SUPPORT)).booleanValue());
}
【例18.13】SelectorThread中的enableCometSupport方法:
protected void enableCometSupport(boolean enableComet){
??? if ( enableComet ){
??????? asyncExecution = true;
??????? setBufferResponse(false);???
??????? isFileCacheEnabled = false;
??????? isLargeFileCacheEnabled = false;
??????? asyncHandler = new DefaultAsyncHandler();
??????? asyncHandler.addAsyncFilter(new CometAsyncFilter());
??????? SelectorThread.logger()
.log(Level.INFO,"Enabling Grizzly ARP Comet support.");
??? } else {
??????? asyncExecution = false;
??? }
}
在CometAsyncFilter類中,最重要的方法就是doFilter,它是Comet與異步請求處理(ARP)框架的接口。
【例18.14】CometAsyncFilter中的doFilter方法:
public boolean doFilter(AsyncExecutor asyncExecutor) {
AsyncProcessorTask apt =
(AsyncProcessorTask) asyncExecutor.getAsyncTask();
??? CometEngine cometEngine = CometEngine.getEngine();???????????????
??? try{
??????? if (!cometEngine.handle(apt)) {
??????????? return true;
??????? }
??? } catch (IOException ex){
??????? logger.log(Level.SEVERE,"CometAsyncFilter",ex);
??? }
?? return false;
}
從例18.14可以看出,在doFilter方法之中,所有的操作都交給CometEngine的handle方法。
CometEngine是Comet應用中最先接觸的類。如果一個Servlet或JSP頁面要想成為Comet請求,那么在編程的時候需要經過以下幾步。
(1)?? 獲得CometEngine的實例對象,并將需要成為Comet請求的路徑注冊:
CometEngine cometEngine = CometEngine.getEngine();
CometContext cometContext = cometEngine.register(contextPath);
(2)?? 注冊一個CometHandler:
cometContext.addCometHandler(handler);
(3)?? 最后,如果有消息發送,可以通過下面的方法通知所有注冊的通道:
cometContext.notify(handler);
有關CometContext和CometHandler類,在下面的內容會進行稍微詳細的描述。
當請求處理交給CometEngine對象以后,CometEngine以及其他幾個類(CometContext和CometHandler等)就會對這個請求的生命周期負起全部的責任。Comet請求和其他的請求不一樣,它需要長時間地保持HTTP連接,來保證服務器端能夠利用這些連接主動發送消息給瀏覽器客戶端。因此CometEngine并沒有使用主線程的Selector(在SelectorThread中運行的Selector,而是使用了自己的Selector對象:CometSelector,而讓主線程的Selector負責其他類型的請求讀取和處理。CometSelector的主要職責是負責已經注冊的Comet請求的生命周期:哪些Comet請求的連接被用戶關閉或異常關閉,哪些Comet請求根據配置已經超時。在這些情況下,需要系統釋放相應的資源,使得系統更加穩定和健壯。
而CometContext的作用則是應用程序和Comet實現之間的橋梁。CometHandler可以利用它來注冊,因此CometContext掌握了當前Comet應用中所有注冊了的頻道。這樣當其中有一個頻道利用CometContext來發送消息時,CometContext能夠將消息主動發送給所有注冊的Handler。這些對象的關系,可以通過一個典型的例子的講解更加清楚的展現出來。
18.2.2 Comet實例講解——“聊天室”應用
“聊天室”是一個非常典型的Comet應用。通常的“聊天室”至少需要包含兩個基本的功能:發送本人的消息和接受顯示別人的消息。這里的Comet應用主要是指接受別人的消息。因為別人什么時候發送了消息瀏覽器是不會知道的,只有聊天服務器本身知道,如果想要將各種消息實時地通知各個客戶端,就需要服務器推送技術。
現有的很多“聊天室”大多使用輪詢(Polling)技術,來使得瀏覽器不斷自動刷新以獲得最新的消息。這種實現方法在并發用戶不太多的情況下還能接受。如果并發用戶非常多,服務器的負擔就會大大地增加。另外每次重新建立連接所帶來的延遲也使得用戶不能非常及時地獲得最新的消息。綜合這些因此,對“聊天室”的最佳實現應該使用Comet技術,也就是“服務器推送技術”。
下面來講解一個使用GlassFish的Comet來實現的“聊天室”。在本書所附的CD中有詳細的代碼和步驟來部署和運行“聊天室”應用。
在“聊天室”中,只有一個Servlet和幾個JSP頁面文件。JSP頁面非常簡單,只是簡單的HTML。所有的請求處理都在Servlet中。
【例18.15】Servlet中的init方法:
...
public void init(ServletConfig config) throws ServletException {
??? super.init(config);
??? contextPath = config.getServletContext().getContextPath() + "/chat";
??? CometEngine cometEngine = CometEngine.getEngine();???????????? ?? // [1]
??? CometContext context = cometEngine.register(contextPath);???? ?? // [2]
??? context.setExpirationDelay(20*1000);?????? // [3]
}
...
從例18.15的代碼可以看出,Servlet在初始化的時候做了以下三件事情。
(1)?? 獲得了一個CometEngine的實例對象。上文已經解釋過,CometEngine對象是Comet應用的入口。任何Comet應用都需要CometEngine對象來注冊Comet請求的路徑。
(2)?? 將當前的路徑向CometEngine進行注冊。顯然,當Comet功能打開的時候,GlassFish不會將所有的請求都認為是Comet請求,而是僅僅當請求的路徑和將注冊的路徑相匹配的時候才會進行Comet處理。注冊成功的結果是返回一個CometContext對象。上文已經解釋過,CometContext是每個用戶之間交流的橋梁。
(3)?? 設置當前Comet應用的超時的閥值。
【例18.16】Servlet的doPost方法中的部分代碼(一):
...
public void doPost(HttpServletRequest request,
??????????? HttpServletResponse response)
??????????? throws ServletException, IOException
{
??? String action = request.getParameter("action");
??? CometEngine cometEngine = CometEngine.getEngine();
??? CometContext cometContext = cometEngine.getCometContext(contextPath);
...
}
從例18.16的代碼中可以看出,在處理Comet請求,與其他用戶交互的時候,是需要先獲得CometContext的。需要指出的是,CometEngine對象是一個單例對象(Singleton),只會存在一個實例,因此任何時候調用getEngine的方法都會獲得同一個實例。
【例18.17】Servlet的doPost方法中的部分代碼(二):
...
if (action != null) {
if ("login".equals(action)) {
??? String username = request.getParameter("username");
??? request.getSession(true).setAttribute("username", username);
??? if (firstServlet != -1){
??????? cometContext.notify("User " + username
??????????? + " from " + request.getRemoteAddr()
??????????? + " is joining the chat.<br/>",CometEvent.NOTIFY,firstServlet);
??? }
...
從例18.17的代碼可以看出,當用戶登錄成功后,除了將用戶信息保存到session中之外,還會通過cometContext向所有其他用戶發出“新用戶登錄”的信息。
【例18.18】Servlet的doPost方法中的部分代碼(三):
...
else if ("post".equals(action)){
String username = (String) request.getSession(true)
??????????????? .getAttribute("username");
String message = request.getParameter("message");
cometContext.notify("[ " + username + " ] " + message + "<br/>");
response.sendRedirect("post.jsp");
return;
...
例18.18的代碼是在處理用戶“說話”的情況。如果用戶在自己的發送消息框中向其他在線的用戶發送了一些消息,Servlet在處理的時候就是通過cometContext來通知所有的在線用戶。
【例18.19】Servlet的doPost方法中的部分代碼(四):
...
else if ("openchat".equals(action)) {
??? response.setContentType("text/html");
??? String username = (String) request.getSession(true)
??????????? ??????????????????????? .getAttribute("username");
??? response.getWriter().println("<h2>Welcome "+ username + " </h2>");
??? CometRequestHandler handler = new CometRequestHandler();
??? handler.clientIP = request.getRemoteAddr();
??? handler.attach(response.getWriter());
??? cometContext.addCometHandler(handler);??????
??? return;
...
例18.19的代碼演示的是“聊天消息顯示”的功能,這才是真正Comet的請求,這個請求的連接是一直保持打開著的,等待著服務器主動將最新的信息發送到瀏覽器。這段代碼中最主要的內容就是向CometContext注冊了一個CometHandler。注冊之后,這個Handler就會等待服務器端的回調,來完成向瀏覽器輸出的功能。
【例18.20】CometRequestHandler類的onEvent方法:
public class CometRequestHandler implements CometHandler<PrintWriter>{
public void onEvent(CometEvent event) throws IOException{??
??? ???? try{
??????? ???? if (firstServlet != -1 && this.hashCode() != firstServlet){
??????????? ???? event.getCometContext().notify("User " + clientIP
??????????? ???? + " is getting a new message.<br/>",CometEvent.NOTIFY,
??????????? ???? firstServlet);
??????? ???? }
??????? ???? if (event.getType() != CometEvent.READ){
??????????? ???? printWriter.println(event.attachment());
??????????? ???? printWriter.flush();
????? ?????? }
??? ???? } catch (Throwable t){
??????? ???? t.printStackTrace();
??? ???? }
}
...
}?? ???????
例18.19的代碼解釋了CometRequestHandler類在收到了系統的函數回調之后,進入到onEvent方法。在onEvent方法的處理中,僅僅是簡單地將系統傳遞過來的消息通過一直保持的HTTP連接向客戶傳過去。
當“聊天室”應用運行的時候,用戶界面如圖18-8所示。其中下半部分是發送消息的部分,它的處理代碼對應于例18-18。上半部分是對話消息顯示的部分,它的處理代碼對應于例18.19。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
