<code id="qf3hh"></code>
  • <menuitem id="qf3hh"></menuitem>
  • <strike id="qf3hh"><label id="qf3hh"></label></strike>

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

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

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

      概述

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

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

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

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

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

       

       

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

      “Comet 技術(shù)”、“服務(wù)端推技術(shù)(Server-Side Push)”、“反向 Ajax 技術(shù)”這幾個(gè)名稱說的是同一件事情,可能您已經(jīng)聽說過其中的一項(xiàng)或者幾項(xiàng)。但沒聽說過也沒有關(guān)系,一句話就足以表達(dá)它們?nèi)康囊馑迹?ldquo;在沒有客戶端請(qǐng)求的情況下,服務(wù)端向客戶端發(fā)送數(shù)據(jù)”。

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

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

       

       

      Comet 實(shí)現(xiàn)的方法

      • 簡單輪詢

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

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

      • 長輪詢(混合輪詢)

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

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

      • Comet 流(Forever Frame)

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

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

       

       

      實(shí)戰(zhàn) Servlet 異步處理

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

      Web 日志系統(tǒng)實(shí)戰(zhàn)

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

      清單 1. Log4j 異步 Web Appender
       /** 
       * 基于 AsyncContext 支持的 Appender 
       * 
       */ 
       public class WebLogAppender extends WriterAppender { 
           /** 
           * 異步 Servlet 上下文隊(duì)列
           */ 
           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 類的代碼模版,派生自 org.apache.log4j.WriterAppender,Log4j 默認(rèn)提供的所有 Appender 都從此類繼承,子類代碼執(zhí)行的邏輯僅僅是告知 WriterAppender 如何獲取 Writer。而我們最關(guān)心的如何異步將日志信息輸出至瀏覽器,則在 AsyncContextQueueWriter 中完成。

      清單 2:異步上下文隊(duì)列 Writer
       /** 
       * 向一個(gè) Queue<AsyncContext> 中每個(gè) Context 的 Writer 進(jìn)行輸出
       * 
       */ 
       public class AsyncContextQueueWriter extends Writer { 
      
           /** 
           * AsyncContext 隊(duì)列
           */ 
           private Queue<AsyncContext> queue; 
      
           /** 
           * 消息隊(duì)列
           */ 
           private static final BlockingQueue<String> MESSAGE_QUEUE 
           = new LinkedBlockingQueue<String>(); 
      
           /** 
           * 發(fā)送消息到異步線程,最終輸出到 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; 
               } 
           } 
      
           /** 
           * 異步線程,當(dāng)消息隊(duì)列中被放入數(shù)據(jù),將釋放 take 方法的阻塞,將數(shù)據(jù)發(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è)默認(rèn)的 writer,輸出至控制臺(tái)
           * 這個(gè) writer 是同步輸出,其它輸出到 response 流的 writer 是異步輸出
           */ 
           private static final Writer DEFAULT_WRITER = new OutputStreamWriter(System.out); 
      
           /** 
           * 構(gòu)造 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è)類是 Web 日志實(shí)現(xiàn)的關(guān)鍵類之一,它繼承至 Writer,實(shí)際上是一組 Writer 的集合,其中包含至少一個(gè)默認(rèn) Writer 將數(shù)據(jù)輸出至控制臺(tái),另包含零至若干個(gè)由 Queue<AsyncContext> 所決定的 Response Writer 將數(shù)據(jù)輸出至客戶端。輸出過程中,控制臺(tái)的 Writer 是同步的直接輸出,輸出至 http 客戶端的則由線程 notifierRunnable 進(jìn)行異步輸出。具體實(shí)現(xiàn)方式是信息放置在阻塞隊(duì)列 MESSAGE_QUEUE 中,子線程循環(huán)時(shí)使用到這個(gè)隊(duì)列的 take() 方法,當(dāng)隊(duì)列沒有數(shù)據(jù)這個(gè)方法將會(huì)阻塞線程直到等到新數(shù)據(jù)放入隊(duì)列為止。

      我們?cè)?Log4j.xml 中修改一下配置,將 Appender 切換為 WebLogAppender,那對(duì) Log4j 本身的擴(kuò)展就算完成了:

      清單 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>

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

      清單 4:Web 日志注冊(cè) Servlet
       /** 
       * Servlet implementation class WebLogServlet 
       */ 
       @WebServlet(urlPatterns = { "/WebLogServlet" }, asyncSupported = true) 
       public class WebLogServlet extends HttpServlet { 
      
          /** 
           * serialVersionUID 
           */ 
          private static final long serialVersionUID = -260157400324419618L; 
      
          /** 
           * 將客戶端注冊(cè)到監(jiān)聽 Logger 的消息隊(duì)列中
           */ 
          @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ù)端處理到此為止差不多就結(jié)束了,我們?cè)倏纯纯蛻舳说膶?shí)現(xiàn)。其實(shí)客戶端我們直接訪問這個(gè) Servlet 就可以看到瀏覽器不斷的有日志輸出,并且這個(gè)頁面的滾動(dòng)條會(huì)一直持續(xù),顯示 http 連接并沒有關(guān)閉。為了顯示,我們還是對(duì)客戶端進(jìn)行了包裝,通過一個(gè)隱藏的 frame 去讀取 WebLogServlet 發(fā)出的信息,既 Comet 流方式實(shí)現(xiàn)。

      清單 5:客戶端頁面
       <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:客戶端引用的 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è)已有的日志文件,將內(nèi)容調(diào)用 Logger 輸出到瀏覽器,讀者在調(diào)試時(shí)直接運(yùn)行源碼包中的 TestServlet 即可,運(yùn)行后整體效果如下所示:

      圖 1. 運(yùn)行效果

      運(yùn)行效果

       

      国产一级婬片AAA毛,无码中文精品视视在线观看,欧美日韩a人成v在线动漫,五月丁香青草久久
      <code id="qf3hh"></code>
    • <menuitem id="qf3hh"></menuitem>
    • <strike id="qf3hh"><label id="qf3hh"></label></strike>