聶永的博客

          記錄工作/學習的點點滴滴。

          Servlet 3.0筆記之異步請求Comet推送iFrame示范

          Servlet3規范提出異步請求,絕對是一巨大歷史進步。之前各自應用服務器廠商紛紛推出自己的異步請求實現(或者稱comet,或者服務器推送支持,或者長連接),諸如Tomcat6中的NIO連接協議支持,Jetty的continuations編程架構,SUN、IBM、BEA等自不用說,商業版的服務器對Comet的支持,自然走在開源應用服務器前面,各自為王,沒有一個統一的編程模型,怎一個亂字了得。相關的comet框架也不少,諸如pushlet、DWR、cometd;最近很熱HTML5也不甘寂寞,推出WebSocket,只是離現實較遠。
          總體來說,在JAVA世界,很亂!缺乏規范,沒有統一的編程模型,會嚴重依賴特定服務器,或特定容器。
          好在Servlet3具有了異步請求規范,各個應用服務器廠商只需要自行實現即可,這樣編寫符合規范的異步Servlet代碼,不用擔心移植了。
          現在編寫支持comet風格servlet,很簡單:
          1. 在注解處標記上 asyncSupported = true;
          2. final AsyncContext ac = request.startAsync();
          這里設定簡單應用環境:一個非常受歡迎博客系統,多人訂閱,終端用戶僅僅需要訪問訂閱頁面,當后臺有新的博客文章提交時,服務器會馬上主動推送到客戶端,新的內容自動顯示在用戶的屏幕上。整個過程中,用戶僅僅需要打開一次頁面(即訂閱一次),后臺當有新的內容時會主動展示用戶瀏覽器上,不需要刷新什么。下面的示范使用到了iFrame,有關Comet Stream,會在以后展開。有關理論不會在本篇深入討論,也會在以后討論。
          這個系統需要一個博文內容功能:
          新的博文后臺處理部分代碼:
           protected void doPost(HttpServletRequest request,
          HttpServletResponse response) throws ServletException, IOException {
          MicBlog blog = new MicBlog();

          blog.setAuthor("發布者");
          blog.setId(System.currentTimeMillis());
          blog.setContent(iso2UTF8(request.getParameter("content")));
          blog.setPubDate(new Date());

          // 放到博文隊列里面去
          NewBlogListener.BLOG_QUEUE.add(blog);

          request.setAttribute("message", "博文發布成功!");

          request.getRequestDispatcher("/WEB-INF/pages/write.jsp").forward(
          request, response);
          }

          private static String iso2UTF8(String str){
          try {
          return new String(str.getBytes("ISO-8859-1"), "UTF-8");
          } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
          }
          return null;
          }
          當用戶需要訂閱博客更新時的界面:
          當前頁面HTML代碼可以說明客戶端的一些情況:
          <html>
          <head>
          <title>comet推送測試</title>
          <meta http-equiv="X-UA-Compatible" content="IE=8" />
          <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
          <meta name="author" content="yongboy@gmail.com"/>
          <meta name="keywords" content="servlet3, comet, ajax"/>
          <meta name="description" content=""/>
          <link type="text/css" rel="stylesheet" href="css/main.css"/>
          <script type="text/javascript" src="js/jquery-1.4.min.js"></script>
          <script type="text/javascript" src="js/comet.js"></script>
          </head>
          <body style="margin: 0; overflow: hidden">
          <div id="showDiv" class="inputStyle"></div>
          </body>
          </html>
          id為“showDiv”的div這里作為一個容器,用于組織顯示最新的信息。
          而客戶端邏輯,則在comet.js文件中具體展示了如何和服務器交互的一些細節:
          /**
          * 客戶端Comet JS 渲染部分
          * @author yongboy@gmail.com
          * @date 2010-10-18
          * @version 1.0
          */
          String.prototype.template=function(){
          var args=arguments;
          return this.replace(/\{(\d+)\}/g, function(m, i){
          return args[i];
          });
          }
          var html = '<div class="logDiv">'
          + '<div class="contentDiv">{0}</div>'
          + '<div class="tipDiv">last date : {1}</div>'
          + '<div class="clear">&nbsp;</div>'
          + '</div>';

          function showContent(json) {
          $("#showDiv").prepend(html.template(json.content, json.date));
          }
          var server = 'blogpush';
          var comet = {
          connection : false,
          iframediv : false,

          initialize: function() {
          if (navigator.appVersion.indexOf("MSIE") != -1) {
          comet.connection = new ActiveXObject("htmlfile");
          comet.connection.open();
          comet.connection.write("<html>");
          comet.connection.write("<script>document.domain = '"+document.domain+"'");
          comet.connection.write("</html>");
          comet.connection.close();
          comet.iframediv = comet.connection.createElement("div");
          comet.connection.appendChild(comet.iframediv);
          comet.connection.parentWindow.comet = comet;
          comet.iframediv.innerHTML = "<iframe id='comet_iframe' src='"+server+"'></iframe>";
          }else if (navigator.appVersion.indexOf("KHTML") != -1 || navigator.userAgent.indexOf('Opera') >= 0) {
          comet.connection = document.createElement('iframe');
          comet.connection.setAttribute('id', 'comet_iframe');
          comet.connection.setAttribute('src', server);
          with (comet.connection.style) {
          position = "absolute";
          left = top = "-100px";
          height = width = "1px";
          visibility = "hidden";
          }
          document.body.appendChild(comet.connection);
          }else {
          comet.connection = document.createElement('iframe');
          comet.connection.setAttribute('id', 'comet_iframe');
          with (comet.connection.style) {
          left = top = "-100px";
          height = width = "1px";
          visibility = "hidden";
          display = 'none';
          }
          comet.iframediv = document.createElement('iframe');
          comet.iframediv.setAttribute('onLoad', 'comet.frameDisconnected()');
          comet.iframediv.setAttribute('src', server);
          comet.connection.appendChild(comet.iframediv);
          document.body.appendChild(comet.connection);
          }
          },
          frameDisconnected: function() {
          comet.connection = false;
          $('#comet_iframe').remove();
          //setTimeout("chat.showConnect();",100);
          },
          showMsg:function(data){
          showContent(data);
          },
          timeout:function(){
          var url = server + "?time=" + new Date().getTime();
          if (navigator.appVersion.indexOf("MSIE") != -1) {
          comet.iframediv.childNodes[0].src = url;
          } else if (navigator.appVersion.indexOf("KHTML") != -1 || navigator.userAgent.indexOf('Opera') >= 0) {
          document.getElementById("comet_iframe").src = url;
          } else {
          comet.connection.removeChild(comet.iframediv);
          document.body.removeChild(comet.connection);
          comet.iframediv.setAttribute('src', url);
          comet.connection.appendChild(comet.iframediv);
          document.body.appendChild(comet.connection);
          }
          },
          onUnload: function() {
          if (comet.connection) {
          comet.connection = false;
          }
          }
          }

          if (window.addEventListener) {
          window.addEventListener("load", comet.initialize, false);
          window.addEventListener("unload", comet.onUnload, false);
          } else if (window.attachEvent) {
          window.attachEvent("onload", comet.initialize);
          window.attachEvent("onunload", comet.onUnload);
          }
          需要注意的是comet這個對象在初始化(initialize)和超時(timeout)時的處理方法,能夠在IE以及火狐下面表現的完美,不會出現正在加載中標志。當然超時方法(timeout),是在服務器端通知客戶端調用。在Chrome和Opera下面一直有進度條顯示,暫時沒有找到好的解決辦法。
          后臺處理客戶端請求請求代碼:
          /**
          * 負責客戶端的推送
          * @author yongboy
          * @date 2011-1-13
          * @version 1.0
          */
          @WebServlet(urlPatterns = { "/blogpush" }, asyncSupported = true)
          public class BlogPushAction extends HttpServlet {
          private static final long serialVersionUID = 8546832356595L;
          private static final Log log = LogFactory.getLog(BlogPushAction.class);

          protected void doGet(HttpServletRequest request,
          HttpServletResponse response) throws ServletException, IOException {

          response.setHeader("Cache-Control", "private");
          response.setHeader("Pragma", "no-cache");
          response.setContentType("text/html;charset=UTF-8");
          response.setCharacterEncoding("UTF-8");
          final PrintWriter writer = response.getWriter();

          // 創建Comet Iframe
          writer.println("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">");
          writer.println("<script type=\"text/javascript\">var comet = window.parent.comet;</script>");
          writer.flush();

          final AsyncContext ac = request.startAsync();
          ac.setTimeout(10 * 60 * 1000);// 10分鐘時間;tomcat7下默認為10000毫秒

          ac.addListener(new AsyncListener() {
          public void onComplete(AsyncEvent event) throws IOException {
          log.info("the event : " + event.toString()
          + " is complete now !");
          NewBlogListener.ASYNC_AJAX_QUEUE.remove(ac);
          }

          public void onTimeout(AsyncEvent event) throws IOException {
          log.info("the event : " + event.toString()
          + " is timeout now !");

          // 嘗試向客戶端發送超時方法調用,客戶端會再次請求/blogpush,周而復始
          log.info("try to notify the client the connection is timeout now ...");
          String alertStr = "<script type=\"text/javascript\">comet.timeout();</script>";
          writer.println(alertStr);
          writer.flush();
          writer.close();

          NewBlogListener.ASYNC_AJAX_QUEUE.remove(ac);
          }

          public void onError(AsyncEvent event) throws IOException {
          log.info("the event : " + event.toString() + " is error now !");
          NewBlogListener.ASYNC_AJAX_QUEUE.remove(ac);
          }

          public void onStartAsync(AsyncEvent event) throws IOException {
          log.info("the event : " + event.toString()
          + " is Start Async now !");
          }
          });

          NewBlogListener.ASYNC_AJAX_QUEUE.add(ac);
          }
          }
          每一個請求都需要request.startAsync(request,response)啟動異步處理,得到AsyncContext對象,設置超時處理時間(這里設置10分鐘時間),注冊一個異步監聽器。
          異步監聽器可以在異步請求于啟動、完成、超時、錯誤發生時得到通知,屬于事件傳遞機制,從而更好對資源處理等。
          在長連接超時(onTimeout)事件中,服務器會主動通知客戶端再次進行請求注冊。
          若中間客戶端非正常關閉,在超時后,服務器端推送數量就減少了無效的連接。在真正應用中,需要尋覓一個較為理想的值,以保證服務器的有效連接數,又不至于浪費多余的連接。
          每一個異步請求會被存放在一個高效并發隊列中,在一個線程中統一處理,具體邏輯代碼:
          /**
          * 監聽器單獨線程推送到客戶端
          * @author yongboy
          * @date 2011-1-13
          * @version 1.0
          */
          @WebListener
          public class NewBlogListener implements ServletContextListener {
          private static final Log log = LogFactory.getLog(NewBlogListener.class);
          public static final BlockingQueue<MicBlog> BLOG_QUEUE = new LinkedBlockingDeque<MicBlog>();
          public static final Queue<AsyncContext> ASYNC_AJAX_QUEUE = new ConcurrentLinkedQueue<AsyncContext>();
          private static final String TARGET_STRING = "<script type=\"text/javascript\">comet.showMsg(%s);</script>";

          private String getFormatContent(MicBlog blog) {
          return String.format(TARGET_STRING, buildJsonString(blog));
          }

          public void contextDestroyed(ServletContextEvent arg0) {
          log.info("context is destroyed!");
          }

          public void contextInitialized(ServletContextEvent servletContextEvent) {
          log.info("context is initialized!");
          // 啟動一個線程處理線程隊列
          new Thread(runnable).start();
          }

          private Runnable runnable = new Runnable() {
          public void run() {
          boolean isDone = true;

          while (isDone) {
          if (!BLOG_QUEUE.isEmpty()) {
          try {
          log.info("ASYNC_AJAX_QUEUE size : "
          + ASYNC_AJAX_QUEUE.size());
          MicBlog blog = BLOG_QUEUE.take();

          if (ASYNC_AJAX_QUEUE.isEmpty()) {
          continue;
          }

          String targetJSON = getFormatContent(blog);

          for (AsyncContext context : ASYNC_AJAX_QUEUE) {
          if (context == null) {
          log.info("the current ASYNC_AJAX_QUEUE is null now !");
          continue;
          }
          log.info(context.toString());
          PrintWriter out = context.getResponse().getWriter();

          if (out == null) {
          log.info("the current ASYNC_AJAX_QUEUE's PrintWriter is null !");
          continue;
          }

          out.println(targetJSON);
          out.flush();
          }
          } catch (Exception e) {
          e.printStackTrace();
          isDone = false;
          }
          }
          }
          }
          };

          private static String buildJsonString(MicBlog blog) {
          Map<String, Object> info = new HashMap<String, Object>();
          info.put("content", blog.getContent());
          info.put("date",
          DateFormatUtils.format(blog.getPubDate(), "HH:mm:ss SSS"));

          JSONObject jsonObject = JSONObject.fromObject(info);

          return jsonObject.toString();
          }
          }
          異步請求上下文AsyncContext獲取輸出對象(response),向客戶端傳遞JSON格式化序列對象,具體怎么解析、顯示,由客戶端(見comet.js)決定。
          鑒于Servlet為單實例多線程,最佳實踐建議是不要在servlet中啟動單獨的線程,本文放在ServletContextListener監聽器中,以便在WEB站點啟動時中,創建一個獨立線程,在有新的博文內容時,遍歷推送所有已注冊客戶端
          整個流程梳理一下:
          1. 客戶端請求 blog.html
          2. blog.html的comet.js開始注冊啟動事件
          3. JS產生一個iframe,在iframe中請求/blogpush,注冊異步連接,設定超時為10分鐘,注冊異步監聽器
          4. 服務器接收到請求,添加異步連接到隊列中
          5. 客戶端處于等待狀態(長連接一直建立),等待被調用
          6. 后臺發布新的博客文章
          7. 博客文章被放入到隊列中
          8. 一直在守候的獨立線程處理博客文章隊列;把博客文章格式化成JSON對象,一一輪詢推送到客戶端
          9. 客戶端JS方法被調用,進行JSON對象解析,組裝HTML代碼,顯示在當前頁面上
          10. 超時發生時,/blogpush通知客戶端已經超時,調用超時(timeout)方法;同時從異步連接隊列中刪除
          11. 客戶端接到通知,對iframe進行操作,再次進行連接請求,重復步驟2
          大致流程圖,如下:
          diagram2

          其連接模型,偷懶,借用IBM上一張圖片說明:

          posted on 2011-01-10 10:57 nieyong 閱讀(7586) 評論(5)  編輯  收藏 所屬分類: Servlet3

          評論

          # re: Servlet 3.0筆記之異步請求Comet推送iFrame示范 2011-04-29 10:53 RonQi

          很好的文章啊,博主原創的吧。我目前在做一個服務器端推消息的程序,類似客服系統那樣的,服務端推給頁面客服人員問題,客服回答完后提交給服務端。我覺得可以參考樓主的程序,不知樓主是否在正式環境下使用過Servlet3的推技術呢,現在這方面的資料還不是很多  回復  更多評論   

          # re: Servlet 3.0筆記之異步請求Comet推送iFrame示范 2011-05-04 14:02 nieyong

          @RonQi
          暫無在正式環境下使用Servlet3的推送。
          不過在現實環境下,有人已經使用golang做服務器,采用長輪詢做推送,在實際環境中長輪詢使用較多一些。
          有關輪詢還是推送,可以參考
          《Push Or Pull?》
          http://rdc.taobao.com/team/jm/archives/918

          里面對推送和輪詢分別存在的問題,分析的很透徹。  回復  更多評論   

          # re: Servlet 3.0筆記之異步請求Comet推送iFrame示范 2011-09-24 04:08 wxh0800

          很有啟發的代碼, 推薦!!!
          但是樓主還沒有完善線程同步機制,CPU占用太高,實際環境不能用。
          不過還是非常感謝,相應的文章并不多  回復  更多評論   

          # re: Servlet 3.0筆記之異步請求Comet推送iFrame示范 2012-09-09 08:50 lee_jie1001@foxmail.com

          請問下,異步連接只能響應一次啊!我做了兩個頁面,一個get請求與服務器建立異步連接,一個post請求更新消息隊列,客戶先使用get與服務器建立連接,讓后我用另外的頁面通過post發消息,只有第一個消息能收到!  回復  更多評論   

          # re: Servlet 3.0筆記之異步請求Comet推送iFrame示范[未登錄] 2012-10-09 10:06 iloveyou

          ie、ff下異步推動時,瀏覽器請求圖標總在不停轉動,用戶體驗不太好啊,不知樓主是否也發現這個問題??  回復  更多評論   

          公告

          所有文章皆為原創,若轉載請標明出處,謝謝~

          新浪微博,歡迎關注:

          導航

          <2011年1月>
          2627282930311
          2345678
          9101112131415
          16171819202122
          23242526272829
          303112345

          統計

          常用鏈接

          留言簿(58)

          隨筆分類(130)

          隨筆檔案(151)

          個人收藏

          最新隨筆

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 余江县| 马山县| 梁河县| 紫阳县| 舒兰市| 中宁县| 乌兰浩特市| 隆德县| 肥城市| 新竹市| 登封市| 灵台县| 曲周县| 上蔡县| 肥城市| 澄迈县| 秦安县| 蒲江县| 中方县| 武胜县| 垦利县| 茌平县| 瓦房店市| 巴马| 临潭县| 鱼台县| 东宁县| 中西区| 丰顺县| 云林县| 丰城市| 台湾省| 望城县| 林口县| 四川省| 吴堡县| 山西省| 莲花县| 绥宁县| 华容县| 钟祥市|