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="[email protected]"/>
<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 [email protected]
 * @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

大致流程图,如下:

其连接模型,偷懒,借用IBM上一张图片说明:

时间: 2024-12-25 02:14:03

Servlet 3.0笔记之异步请求Comet推送iFrame示范的相关文章

Asp.net MVC Comet推送

一.简介 在Asp.net MVC实现的Comet推送的原理很简单. 服务器端:接收到服务器发送的AJAX请求,服务器端并不返回,而是将其Hold住,待到有东西要通知客户端时,才将这个请求返回. 客户端:请求异步Action,当接收到一个返回时,立即又再发送一个. 缺点:会长期占用一个Asp.net处理线程.但相比于轮询,其节省了带宽. 示例: 新建一个Controller如下: //Comet服务器推送控制器(需设置NoAsyncTimeout,防止长时间请求挂起超时错误) [NoAsyncT

【转】Asp.net MVC Comet推送

原文链接:http://www.cnblogs.com/kissdodog/p/4283485.html 一.简介 在Asp.net MVC实现的Comet推送的原理很简单. 服务器端:接收到服务器发送的AJAX请求,服务器端并不返回,而是将其Hold住,待到有东西要通知客户端时,才将这个请求返回. 客户端:请求异步Action,当接收到一个返回时,立即又再发送一个. 缺点:会长期占用一个Asp.net处理线程.但相比于轮询,其节省了带宽. 示例: 新建一个Controller如下: //Com

Asp.net MVC Comet 推送

一.简介 在Asp.net MVC实现的Comet推送的原理很简单. 服务器端:接收到服务器发送的AJAX请求,服务器端并不返回,而是将其Hold住,待到有东西要通知客户端时,才将这个请求返回. 客户端:请求异步Action,当接收到一个返回时,立即又再发送一个. 缺点:会长期占用一个Asp.net处理线程.但相比于轮询,其节省了带宽. 示例: 新建一个Controller如下: //Comet服务器推送控制器(需设置NoAsyncTimeout,防止长时间请求挂起超时错误) [NoAsyncT

Servlet3.0中的异步请求

package com.itheima.async; import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; i

学习笔记12JS异步请求

*一般用JS来监听按钮事件,都应该先监听页面OnLoad事件. *Js写在哪里,就会在页面解析到哪里执行. 异步请求:所谓异步请求,就是使用JS来监听按钮点击事件,并且发送请求,等到回复后,再使用JS来进行页面跳转,或动态改变页面.使用场合:当请求是ashx是,都可以使用异步方法,页面就无需刷到ashx的一个空白页面或者不用于展示的页面了. *使用jquery发送异步请求:$("#按钮ID").Click(fuction(){ $.get( "页面URL.ashx"

comet 推送消息到客户端

weiconfig: 1 <system.web> 2 <httpHandlers> 3 <add path="comet_broadcast.ashx" type="AsnyHandler" verb="POST,GET"/> 4 </httpHandlers> 5 <compilation debug="true" targetFramework="4.0&q

APNS push server端 SSL3.0 转 TLS (iPhone苹果推送服务)

(转载此文,请说明原文出处) 苹果的官方公布 Update to the Apple Push Notification Service October 22, 2014 The Apple Push Notification service will be updated and changes to your servers may be required to remain compatible. In order to protect our users against a recent

APNS push 服务器端 SSL3.0 转 TLS (iPhone苹果推送服务)

苹果的官方发布 Update to the Apple Push Notification Service October 22, 2014 The Apple Push Notification service will be updated and changes to your servers may be required to remain compatible. In order to protect our users against a recently discovered s

#研发中间件介绍#异步消息可靠推送Notify

郑昀 基于朱传志的设计文档 最后更新于2014/11/11 关键词: 异步消息 .订阅者集群.可伸缩.Push模式.Pull模式 本文档适用人员:研发 电商系统为什么需要 NotifyServer? 如子柳所说,电商系统『 需要两种中间件系统,一种是实时调用的中间件(淘宝的HSF,高性能服务框架).一种是异步消息通知的中间件(淘宝的Notify)』.那么用传统的 ActiveMQ/RabbitMQ 来实现 异步消息发布和订阅 不行吗? 2013年之前我们确实用的是 ActiveMQ,当然主要是订