国内精品久久久久久久星辰影视-亚洲天堂久久精品成人-亚洲国产成人综合青青-91精品啪在线看国产网站-日韩一区二区在线观看

?
    開(kāi)發(fā)技術(shù) / Technology

    Servlet 3.0 實(shí)戰:異步 Servlet 與 Comet 風(fēng)格應用程序

    日期:2015年2月6日  作者:zhjw  來(lái)源:互聯(lián)網(wǎng)    點(diǎn)擊:700

    概述

    作為 Java EE 6 體系中重要成員的 JSR 315 規范,將 Servlet API 最新的版本從 2.5 提升到了 3.0,這是近 10 年來(lái) Servlet 版本號最大的一次升級,此次升級中引入了若干項令開(kāi)發(fā)人員興奮的特性,如:

    • 可插拔的 Web 架構(Web framework pluggability)。
    • 通過(guò) Annotations 代替傳統 web.xml 配置文件的 EOD 易于開(kāi)發(fā)特性(ease of development)。
    • Serlvet 異步處理支持。
    • 安全性提升,如 Http Only Cookies、login/logout 機制。
    • 其它改進(jìn),如文件上傳的直接支持等。

    其中,在開(kāi)源社區中討論得最多的就是 Servlet 異步處理的支持,所謂 Servlet 異步處理,包括了非阻塞的輸入/輸出、異步事件通知、延遲 request 處理以及延遲 response 輸出等幾種特性。這些特性大多并非 JSR 315 規范首次提出,譬如非阻塞輸入/輸出,在 Tomcat 6.0 中就提供了 Advanced NIO 技術(shù)以便一個(gè) Servlet 線(xiàn)程能處理多個(gè) Http Request,Jetty、GlassFish 也曾經(jīng)有過(guò)類(lèi)似的支持。但是使用這些 Web 容器提供的高級特性時(shí),因為現有的 Servlet API 沒(méi)有對這類(lèi)應用的支持,所以都必須引入一些 Web 容器專(zhuān)有的類(lèi)、接口或者 Annotations,導致使用了這部分高級特性,就必須與特定的容器耦合在一起,這對很多項目來(lái)說(shuō)都是無(wú)法接受的。因此 JSR 315 將這些特性寫(xiě)入規范,提供統一的 API 支持后,這類(lèi)異步處理特性才真正具備廣泛意義上的實(shí)用性,只要支持 Servlet 3.0 的 Web 容器,就可以不加修改的運行這些 Web 程序。

    JSR 315 中的 Servlet 異步處理系列特性在很多場(chǎng)合都有用武之地,但人們最先看到的,是它們在“服務(wù)端推”(Server-Side Push)方式 —— 也稱(chēng)為 Comet 方式的交互模型中的價(jià)值。在 JCP(Java Community Process)網(wǎng)站上提出的 JSR 315 規范目標列表,關(guān)于異步處理這個(gè)章節的標題就直接定為了“Async and Comet support”(異步與 Comet 支持)。

    本文將詳細介紹 Comet 風(fēng)格應用的實(shí)現方式,以及 Servlet 3.0 中的異步處理特性在 Comet 風(fēng)格程序中的實(shí)際應用。

     

     

    經(jīng)典 Request-Response 交互模型的突破

    “Comet 技術(shù)”、“服務(wù)端推技術(shù)(Server-Side Push)”、“反向 Ajax 技術(shù)”這幾個(gè)名稱(chēng)說(shuō)的是同一件事情,可能您已經(jīng)聽(tīng)說(shuō)過(guò)其中的一項或者幾項。但沒(méi)聽(tīng)說(shuō)過(guò)也沒(méi)有關(guān)系,一句話(huà)就足以表達它們全部的意思:“在沒(méi)有客戶(hù)端請求的情況下,服務(wù)端向客戶(hù)端發(fā)送數據”。

    這句話(huà)聽(tīng)起來(lái)很簡(jiǎn)單很好理解,但是任何一個(gè)長(cháng)期從事 B/S 應用程序開(kāi)發(fā)的程序都清楚,這實(shí)現起來(lái)并不簡(jiǎn)單,甚至很長(cháng)一段時(shí)間內,人們認為這是并不可能的。因為這種做法完全不符合傳統基于 HTTP 協(xié)議的交互思想:只有基于 Socket 層次的應用才能做到 Server 和 Client 端雙方對等通訊,而基于 HTTP 的應用中,Server 只是對來(lái)自 Client 的請求進(jìn)行回應,不關(guān)心客戶(hù)端的狀態(tài),不主動(dòng)向客戶(hù)端請求信息,因此 Http 協(xié)議被稱(chēng)為無(wú)狀態(tài)、單向性協(xié)議,這種交互方式稱(chēng)為 Request-Response 交互模型。

    無(wú)狀態(tài)、單向的經(jīng)典 Request-Response 交互模型有很多優(yōu)點(diǎn),譬如高效率、高可伸縮等。對于被動(dòng)響應用戶(hù)請求為主的應用,像 CMS、MIS、ERP 等非常適合,但是對于另外一些需要服務(wù)端主動(dòng)發(fā)送的需求,像聊天室(用戶(hù)不發(fā)言的時(shí)候也需要把其它用戶(hù)的發(fā)言傳送回來(lái))、日志系統(客戶(hù)端沒(méi)有請求,當服務(wù)端有日志輸出時(shí)主動(dòng)發(fā)送到客戶(hù)端)則處理起來(lái)很困難,或者說(shuō)這類(lèi)應用根本不適合使用經(jīng)典的 Request-Response 交互模型來(lái)處理。當“不適合”與“有需求”同時(shí)存在時(shí),人們就開(kāi)始不斷尋找突破這種限制的方法。

     

     

    Comet 實(shí)現的方法

    • 簡(jiǎn)單輪詢(xún)

      最早期的 Web 應用中,主要通過(guò) JavaScript 或者 Meta HTML 標簽等手段,定時(shí)刷新頁(yè)面來(lái)檢測服務(wù)端的變化。顯然定時(shí)刷新頁(yè)面服務(wù)端仍然在被動(dòng)響應客戶(hù)端的請求,只不過(guò)客戶(hù)端的請求是連續、頻繁的,讓用戶(hù)看起來(lái)產(chǎn)生有服務(wù)端自動(dòng)將信息發(fā)過(guò)來(lái)的錯覺(jué)。這種方式簡(jiǎn)單易行,但缺陷也非常明顯:可能大部分請求都是無(wú)意義的,因為服務(wù)端期待的事件沒(méi)有發(fā)生,實(shí)際上并沒(méi)有需要發(fā)送的信息,而不得不重復的回應著(zhù)頁(yè)面上所有內容給瀏覽器;另外就是當服務(wù)端發(fā)生變化時(shí),并不能“實(shí)時(shí)”的返回,刷新的間隔太短,產(chǎn)生很大的性能浪費,間隔太長(cháng),事件通知又可能晚于用戶(hù)期望的時(shí)間到達。

      當絕大部分瀏覽器提供了 XHR(XmlHttpRequest)對象支持后,Ajax 技術(shù)出現并迅速流行,這一階段做的輪詢(xún)就不必每次都返回都返回整個(gè)頁(yè)面中所有的內容,如果服務(wù)端沒(méi)有事件產(chǎn)生,只需要返回極少量?jì)热莸?http 報文體。Ajax 可以節省輪詢(xún)傳輸中大量的帶寬浪費,但它無(wú)法減少請求的次數,因此 Ajax 實(shí)現的簡(jiǎn)單輪詢(xún)仍然有輪詢(xún)的局限性,對其缺陷只能一定程度緩解,而無(wú)法達到質(zhì)變。

    • 長(cháng)輪詢(xún)(混合輪詢(xún))

      長(cháng)輪詢(xún)與簡(jiǎn)單輪詢(xún)的最大區別就是連接時(shí)間的長(cháng)短:簡(jiǎn)單輪詢(xún)時(shí)當頁(yè)面輸出完連接就關(guān)閉了,而長(cháng)輪詢(xún)一般會(huì )保持 30 秒乃至更長(cháng)時(shí)間,當服務(wù)器上期待的事件發(fā)生,將會(huì )立刻輸出事件通知到客戶(hù)端,接著(zhù)關(guān)閉連接,同時(shí)建立下一個(gè)連接開(kāi)始一次新的長(cháng)輪詢(xún)。

      長(cháng)輪詢(xún)的實(shí)現方式優(yōu)勢在于當服務(wù)端期待事件發(fā)生,數據便立即返回到客戶(hù)端,期間沒(méi)有數據返回,再較長(cháng)的等待時(shí)間內也沒(méi)有新的請求發(fā)生,這樣可以讓發(fā)送的請求減少很多,而事件通知的靈敏度卻大幅提高到幾乎是“實(shí)時(shí)”的程度。

    • Comet 流(Forever Frame)

      Comet 流是按照長(cháng)輪詢(xún)的實(shí)現思路進(jìn)一步發(fā)展的產(chǎn)物。令長(cháng)輪詢(xún)將事件通知發(fā)送回客戶(hù)端后不再關(guān)閉連接,而是一直保持直到超時(shí)事件發(fā)生才重新建立新的連接,這種變體我們就稱(chēng)為 Comet 流??蛻?hù)端可以使用 XmlHttpRequest 對象中的 readyState 屬性來(lái)判斷是 Receiving 還是 Loaded。Comet 流理論上可以使用一個(gè)鏈接來(lái)處理若干次服務(wù)端事件通知,更進(jìn)一步節省了發(fā)送到服務(wù)端的請求次數。

    無(wú)論是長(cháng)輪詢(xún)還是 Comet 流,在服務(wù)端和客戶(hù)端都需要維持一個(gè)比較長(cháng)時(shí)間的連接狀態(tài),這一點(diǎn)在客戶(hù)端不算什么太大的負擔,但是服務(wù)端是要同時(shí)對多個(gè)客戶(hù)端服務(wù)的,按照經(jīng)典 Request-Response 交互模型,每一個(gè)請求都占用一個(gè) Web 線(xiàn)程不釋放的話(huà),Web 容器的線(xiàn)程則會(huì )很快消耗殆盡,而這些線(xiàn)程大部分時(shí)間處于空閑等待的狀態(tài)。這也就是為什么 Comet 風(fēng)格服務(wù)非常期待異步處理的原因,希望 Web 線(xiàn)程不需要同步的、一對一的處理客戶(hù)端請求,能做到一個(gè) Web 線(xiàn)程處理多個(gè)客戶(hù)端請求。

     

     

    實(shí)戰 Servlet 異步處理

    當前已經(jīng)有不少支持 Servlet API 3.0 的 Web 容器,如 GlassFish v3、Tomcat 7.0、Jetty 8.0 等,在本文撰寫(xiě)時(shí),Tomcat 7 和 Jetty 8 都仍然處于測試階段,雖然支持 Servlet 3.0,但是提供的樣例代碼仍然是與容器耦合的 NIO 實(shí)現,GlassFish v3 提供的樣例(玻璃魚(yú)聊天室)則是完全標準的 Servlet 3.0 實(shí)現,如果讀者需要做找參考樣例,不妨優(yōu)先查看 GlassFish 的 example 目錄。本文后一部分會(huì )提供另外一個(gè)更具備實(shí)用性的例子“Web 日志系統”作為 Servlet API 3.0 的實(shí)戰演示進(jìn)行講解。

    Web 日志系統實(shí)戰

    Apache Log4j 是當前最主流的日志處理器,它有許多不同的 Appender 可以將日志輸出到控制臺、文件、數據庫、Email 等等。在大部分應用中用戶(hù)都不可能查看服務(wù)器的控制臺或者日志文件,如果能直接在瀏覽器上“實(shí)時(shí)”的查看日志將會(huì )是給開(kāi)發(fā)維護帶來(lái)方便,在本例中將實(shí)現一個(gè)日志輸出到瀏覽器的 Appender 實(shí)現。

    清單 1. Log4j 異步 Web Appender
     /** 
     * 基于 AsyncContext 支持的 Appender 
     * 
     */ 
     public class WebLogAppender extends WriterAppender { 
         /** 
         * 異步 Servlet 上下文隊列
         */ 
         public static final Queue<AsyncContext> ASYNC_CONTEXT_QUEUE 
         = new ConcurrentLinkedQueue<AsyncContext>(); 
    
         /** 
         * AsyncContextQueue Writer 
         */ 
         private Writer writer = new AsyncContextQueueWriter(ASYNC_CONTEXT_QUEUE); 
    
         public WebLogAppender() { 
             setWriter(writer); 
         } 
    
         public WebLogAppender(Layout layout) { 
             this(); 
             super.layout = layout; 
         } 
     }

    上面是 Appender 類(lèi)的代碼模版,派生自 org.apache.log4j.WriterAppender,Log4j 默認提供的所有 Appender 都從此類(lèi)繼承,子類(lèi)代碼執行的邏輯僅僅是告知 WriterAppender 如何獲取 Writer。而我們最關(guān)心的如何異步將日志信息輸出至瀏覽器,則在 AsyncContextQueueWriter 中完成。

    清單 2:異步上下文隊列 Writer
     /** 
     * 向一個(gè) Queue<AsyncContext> 中每個(gè) Context 的 Writer 進(jìn)行輸出
     * 
     */ 
     public class AsyncContextQueueWriter extends Writer { 
    
         /** 
         * AsyncContext 隊列
         */ 
         private Queue<AsyncContext> queue; 
    
         /** 
         * 消息隊列
         */ 
         private static final BlockingQueue<String> MESSAGE_QUEUE 
         = new LinkedBlockingQueue<String>(); 
    
         /** 
         * 發(fā)送消息到異步線(xiàn)程,最終輸出到 http response 流
         * @param cbuf 
         * @param off 
         * @param len 
         * @throws IOException 
         */ 
         private void sendMessage(char[] cbuf, int off, int len) throws IOException { 
             try { 
                 MESSAGE_QUEUE.put(new String(cbuf, off, len)); 
             } catch (Exception ex) { 
                 IOException t = new IOException(); 
                 t.initCause(ex); 
                 throw t; 
             } 
         } 
    
         /** 
         * 異步線(xiàn)程,當消息隊列中被放入數據,將釋放 take 方法的阻塞,將數據發(fā)送到 http response 流上
         */ 
         private Runnable notifierRunnable = new Runnable() { 
            public void run() { 
                boolean done = false; 
                while (!done) { 
                    String message = null; 
                    try { 
                        message = MESSAGE_QUEUE.take(); 
                        for (AsyncContext ac : queue) { 
                            try { 
                                PrintWriter acWriter = ac.getResponse().getWriter(); 
                                acWriter.println(htmlEscape(message)); 
                                acWriter.flush(); 
                            } catch (IOException ex) { 
                                System.out.println(ex); 
                                queue.remove(ac); 
                            } 
                        } 
                    } catch (InterruptedException iex) { 
                        done = true; 
                        System.out.println(iex); 
                    } 
                } 
            } 
         }; 
    
         /** 
         * @param message 
         * @return 
         */ 
         private String htmlEscape(String message) { 
             return "<script type='text/javascript'>nwindow.parent.update(""
             + message.replaceAll("n", "").replaceAll("r", "") + "");</script>n"; 
         } 
    
         /** 
         * 保持一個(gè)默認的 writer,輸出至控制臺
         * 這個(gè) writer 是同步輸出,其它輸出到 response 流的 writer 是異步輸出
         */ 
         private static final Writer DEFAULT_WRITER = new OutputStreamWriter(System.out); 
    
         /** 
         * 構造 AsyncContextQueueWriter 
         * @param queue 
         */ 
         AsyncContextQueueWriter(Queue<AsyncContext> queue) { 
             this.queue = queue; 
             Thread notifierThread = new Thread(notifierRunnable); 
             notifierThread.start(); 
         } 
    
         @Override 
         public void write(char[] cbuf, int off, int len) throws IOException { 
             DEFAULT_WRITER.write(cbuf, off, len); 
             sendMessage(cbuf, off, len); 
         } 
    
         @Override 
         public void flush() throws IOException { 
             DEFAULT_WRITER.flush(); 
         } 
    
         @Override 
         public void close() throws IOException { 
             DEFAULT_WRITER.close(); 
             for (AsyncContext ac : queue) { 
                 ac.getResponse().getWriter().close(); 
             } 
         } 
     }

    這個(gè)類(lèi)是 Web 日志實(shí)現的關(guān)鍵類(lèi)之一,它繼承至 Writer,實(shí)際上是一組 Writer 的集合,其中包含至少一個(gè)默認 Writer 將數據輸出至控制臺,另包含零至若干個(gè)由 Queue<AsyncContext> 所決定的 Response Writer 將數據輸出至客戶(hù)端。輸出過(guò)程中,控制臺的 Writer 是同步的直接輸出,輸出至 http 客戶(hù)端的則由線(xiàn)程 notifierRunnable 進(jìn)行異步輸出。具體實(shí)現方式是信息放置在阻塞隊列 MESSAGE_QUEUE 中,子線(xiàn)程循環(huán)時(shí)使用到這個(gè)隊列的 take() 方法,當隊列沒(méi)有數據這個(gè)方法將會(huì )阻塞線(xiàn)程直到等到新數據放入隊列為止。

    我們在 Log4j.xml 中修改一下配置,將 Appender 切換為 WebLogAppender,那對 Log4j 本身的擴展就算完成了:

    清單 3:Log4j.xml 配置
       <appender name="CONSOLE" class="org.fenixsoft.log.WebLogAppender"> 
          <param name="Threshold" value="DEBUG"/> 
          <layout class="org.apache.log4j.PatternLayout"> 
             <!-- The default pattern: Date Priority [Category] Messagen --> 
             <param name="ConversionPattern" value="%d %p [%c] %m%n"/> 
          </layout> 
       </appender>

    接著(zhù),建立一個(gè)支持異步的 Servlet,目的是每個(gè)訪(fǎng)問(wèn)這個(gè) Servlet 的客戶(hù)端,都在 ASYNC_CONTEXT_QUEUE 中注冊一個(gè)異步上下文對象,這樣當有 Logger 信息發(fā)生時(shí),就會(huì )輸出到這些客戶(hù)端。同時(shí),將建立一個(gè)針對這個(gè)異步上下文對象的監聽(tīng)器,當產(chǎn)生超時(shí)、錯誤等事件時(shí),將此上下文從隊列中移除。

    清單 4:Web 日志注冊 Servlet
     /** 
     * Servlet implementation class WebLogServlet 
     */ 
     @WebServlet(urlPatterns = { "/WebLogServlet" }, asyncSupported = true) 
     public class WebLogServlet extends HttpServlet { 
    
        /** 
         * serialVersionUID 
         */ 
        private static final long serialVersionUID = -260157400324419618L; 
    
        /** 
         * 將客戶(hù)端注冊到監聽(tīng) Logger 的消息隊列中
         */ 
        @Override 
        protected void doGet(HttpServletRequest req, HttpServletResponse res) 
        throws ServletException, IOException { 
            res.setContentType("text/html;charset=UTF-8"); 
            res.setHeader("Cache-Control", "private"); 
            res.setHeader("Pragma", "no-cache"); 
            req.setCharacterEncoding("UTF-8"); 
            PrintWriter writer = res.getWriter(); 
            // for IE 
            writer.println("<!-- Comet is a programming technique that enables web 
            servers to send data to the client without having any need for the client 
            to request it. -->n"); 
            writer.flush(); 
    
            final AsyncContext ac = req.startAsync(); 
            ac.setTimeout(10 * 60 * 1000); 
            ac.addListener(new AsyncListener() { 
                public void onComplete(AsyncEvent event) throws IOException { 
                    WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); 
                } 
    
                public void onTimeout(AsyncEvent event) throws IOException { 
                    WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); 
                } 
    
                public void onError(AsyncEvent event) throws IOException { 
                    WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); 
                } 
    
                public void onStartAsync(AsyncEvent event) throws IOException { 
                } 
            }); 
            WebLogAppender.ASYNC_CONTEXT_QUEUE.add(ac); 
        } 
     }

    服務(wù)端處理到此為止差不多就結束了,我們再看看客戶(hù)端的實(shí)現。其實(shí)客戶(hù)端我們直接訪(fǎng)問(wèn)這個(gè) Servlet 就可以看到瀏覽器不斷的有日志輸出,并且這個(gè)頁(yè)面的滾動(dòng)條會(huì )一直持續,顯示 http 連接并沒(méi)有關(guān)閉。為了顯示,我們還是對客戶(hù)端進(jìn)行了包裝,通過(guò)一個(gè)隱藏的 frame 去讀取 WebLogServlet 發(fā)出的信息,既 Comet 流方式實(shí)現。

    清單 5:客戶(hù)端頁(yè)面
     <html> 
     <head></head> 
     <script type="text/javascript" src="js/jquery-1.4.min.js"></script> 
     <script type="text/javascript" src="js/application.js"></script> 
     <style> 
         .consoleFont{font-size:9; color:#DDDDDD; font-family:Fixedsys} 
         .inputStyle{font-size:9; color:#DDDDDD; font-family:Fixedsys; width:100%; 
                height:100%; border:0; background-color:#000000;} 
     </style> 
     <body style="margin:0; overflow:hidden" > 
     <table width="100%" height="100%" border="0" cellpadding="0" 
         cellspacing="0" bgcolor="#000000"> 
      <tr> 
        <td colspan="2"><textarea name="result" id="result" readonly="true" wrap="off" 
             style="padding: 10; overflow:auto" class="inputStyle" ></textarea></td> 
      </tr> 
     </table> 
     <iframe id="comet-frame" style="display: none;"></iframe> 
     </body> 
     </html>
    清單 6:客戶(hù)端引用的 application.js
     $(document).ready(function() { 
         var url = '/AsyncServlet/WebLogServlet'; 
         $('#comet-frame')[0].src = url; 
     }); 
    
     function update(data) { 
         var resultArea = $('#result')[0]; 
         resultArea.value = resultArea.value + data + 'n'; 
     }

    為了模擬日志輸出,我們讀取了一個(gè)已有的日志文件,將內容調用 Logger 輸出到瀏覽器,讀者在調試時(shí)直接運行源碼包中的 TestServlet 即可,運行后整體效果如下所示:

    圖 1. 運行效果

    運行效果