<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    聶永的博客

    記錄工作/學(xué)習(xí)的點(diǎn)點(diǎn)滴滴。

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

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

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

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

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

    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;
    }
    當(dāng)用戶需要訂閱博客更新時的界面:
    當(dāng)前頁面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文件中具體展示了如何和服務(wù)器交互的一些細(xì)節(jié):
    /**
    * 客戶端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以及火狐下面表現(xiàn)的完美,不會出現(xiàn)正在加載中標(biāo)志。當(dāng)然超時方法(timeout),是在服務(wù)器端通知客戶端調(diào)用。在Chrome和Opera下面一直有進(jìn)度條顯示,暫時沒有找到好的解決辦法。
    后臺處理客戶端請求請求代碼:
    /**
    * 負(fù)責(zé)客戶端的推送
    * @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();

    // 創(chuàng)建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下默認(rèn)為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 !");

    // 嘗試向客戶端發(fā)送超時方法調(diào)用,客戶端會再次請求/blogpush,周而復(fù)始
    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對象,設(shè)置超時處理時間(這里設(shè)置10分鐘時間),注冊一個異步監(jiān)聽器。
    異步監(jiān)聽器可以在異步請求于啟動、完成、超時、錯誤發(fā)生時得到通知,屬于事件傳遞機(jī)制,從而更好對資源處理等。
    在長連接超時(onTimeout)事件中,服務(wù)器會主動通知客戶端再次進(jìn)行請求注冊。
    若中間客戶端非正常關(guān)閉,在超時后,服務(wù)器端推送數(shù)量就減少了無效的連接。在真正應(yīng)用中,需要尋覓一個較為理想的值,以保證服務(wù)器的有效連接數(shù),又不至于浪費(fèi)多余的連接。
    每一個異步請求會被存放在一個高效并發(fā)隊(duì)列中,在一個線程中統(tǒng)一處理,具體邏輯代碼:
    /**
    * 監(jiān)聽器單獨(dú)線程推送到客戶端
    * @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!");
    // 啟動一個線程處理線程隊(duì)列
    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為單實(shí)例多線程,最佳實(shí)踐建議是不要在servlet中啟動單獨(dú)的線程,本文放在ServletContextListener監(jiān)聽器中,以便在WEB站點(diǎn)啟動時中,創(chuàng)建一個獨(dú)立線程,在有新的博文內(nèi)容時,遍歷推送所有已注冊客戶端
    整個流程梳理一下:
    1. 客戶端請求 blog.html
    2. blog.html的comet.js開始注冊啟動事件
    3. JS產(chǎn)生一個iframe,在iframe中請求/blogpush,注冊異步連接,設(shè)定超時為10分鐘,注冊異步監(jiān)聽器
    4. 服務(wù)器接收到請求,添加異步連接到隊(duì)列中
    5. 客戶端處于等待狀態(tài)(長連接一直建立),等待被調(diào)用
    6. 后臺發(fā)布新的博客文章
    7. 博客文章被放入到隊(duì)列中
    8. 一直在守候的獨(dú)立線程處理博客文章隊(duì)列;把博客文章格式化成JSON對象,一一輪詢推送到客戶端
    9. 客戶端JS方法被調(diào)用,進(jìn)行JSON對象解析,組裝HTML代碼,顯示在當(dāng)前頁面上
    10. 超時發(fā)生時,/blogpush通知客戶端已經(jīng)超時,調(diào)用超時(timeout)方法;同時從異步連接隊(duì)列中刪除
    11. 客戶端接到通知,對iframe進(jìn)行操作,再次進(jìn)行連接請求,重復(fù)步驟2
    大致流程圖,如下:
    diagram2

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

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

    評論

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

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

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

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

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

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

    很有啟發(fā)的代碼, 推薦!!!
    但是樓主還沒有完善線程同步機(jī)制,CPU占用太高,實(shí)際環(huán)境不能用。
    不過還是非常感謝,相應(yīng)的文章并不多  回復(fù)  更多評論   

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

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

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

    ie、ff下異步推動時,瀏覽器請求圖標(biāo)總在不停轉(zhuǎn)動,用戶體驗(yàn)不太好啊,不知樓主是否也發(fā)現(xiàn)這個問題??  回復(fù)  更多評論   

    公告

    所有文章皆為原創(chuàng),若轉(zhuǎn)載請標(biāo)明出處,謝謝~

    新浪微博,歡迎關(guān)注:

    導(dǎo)航

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

    統(tǒng)計(jì)

    常用鏈接

    留言簿(58)

    隨筆分類(130)

    隨筆檔案(151)

    個人收藏

    最新隨筆

    搜索

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 亚洲精品动漫人成3d在线 | 国产成人免费福利网站| 免费无码一区二区三区蜜桃 | 嫖丰满老熟妇AAAA片免费看| 亚洲欧美综合精品成人导航| 亚洲综合AV在线在线播放| 久久久久亚洲AV无码麻豆| a国产成人免费视频| 一级黄色毛片免费看| 亚洲日韩AV一区二区三区中文| 亚洲人成网网址在线看| 亚洲最大福利视频网站| 亚洲综合久久1区2区3区| 99re在线视频免费观看| 免费观看在线禁片| 久久这里只精品99re免费| 九九免费观看全部免费视频| 亚洲精品无码av片| 美女被羞羞网站免费下载| 国产亚洲人成在线影院| 国产精品免费在线播放| 成人免费一区二区三区| 久久久久久影院久久久久免费精品国产小说 | 午夜老司机永久免费看片| 性xxxxx免费视频播放| 午夜私人影院免费体验区| 亚洲国产精品尤物YW在线观看| 有色视频在线观看免费高清在线直播| 日韩精品视频在线观看免费| 久久免费美女视频| 四虎www免费人成| a级毛片免费在线观看| 五月婷婷免费视频| 久九九精品免费视频| 亚洲欧洲日本在线| 亚洲国产系列一区二区三区| 免费一级毛suv好看的国产网站 | 亚洲免费中文字幕| h视频在线免费观看| 天天操夜夜操免费视频| 亚洲欧洲在线观看|