`
huibin
  • 浏览: 750995 次
  • 性别: Icon_minigender_1
  • 来自: 郑州
社区版块
存档分类
最新评论

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上一张图片说明:
分享到:
评论
1 楼 lycgxy 2011-09-10  
那个监听器会出现死循环,占cpu

相关推荐

    Servlet3.0 异步处理 页面推送 Comet 实例

    本实例主要关注的是如何利用Servlet3.0的异步处理来实现页面推送技术——Comet。 Comet是一种Web应用程序架构,用于创建持久连接,允许服务器向客户端(通常是浏览器)实时推送数据。在传统的HTTP协议中,服务器...

    servlet 3.0 异步 推送 聊天室

    Servlet 3.0的异步推送(也称为Comet技术)通过在服务器端维持一个打开的连接,可以在有新消息时立即推送到客户端,提高了用户体验。 在这个聊天室示例中,"application.js"文件可能是用于处理前端交互逻辑,如接收...

    servlet3.0推送聊天室

    本文将深入探讨如何利用Servlet 3.0的异步功能来构建一个推送信息至客户端的聊天室。 首先,我们需要了解Servlet 3.0的异步处理。在Servlet 2.x版本中,每次HTTP请求都会绑定到一个线程,直到请求处理完成。这种...

    Servlet3.0

    1. **传统Ajax与反向Ajax**: 传统的Ajax是一种客户端向服务器发送请求获取数据的技术,而反向Ajax则是服务器主动向客户端推送数据,它解决了实时性需求,如聊天、股票更新、在线游戏等场景。Servlet 3.0通过异步支持...

    dwr3.0+spring2.5实现Comet消息推送DEMO

    实现Comet消息推送功能,根据登陆人定向推送,解决刷新页面原有ScriptSession不能及时销毁的问题,DEMO比较简陋,请先进入login.jsp页面登陆。根据登陆名称判断推送目标,可登陆多个用户进行测试。

    comet demo 向客户端推送例子

    在Web应用中,通常的HTTP请求是客户端发起的,而Comet打破了这种模式,使得服务器可以在适当的时候主动向客户端推送信息,极大地提升了实时性。 这个"comet demo"是一个展示如何在Java环境下利用Tomcat服务器实现...

    [转]Servlet3特征 异步Servlet

    此外,异步处理可以方便地与WebSocket、 Comet等技术结合,实现更高效的数据推送。 在实际开发中,异步Servlet适用于那些处理时间较长但不需要保持HTTP连接的状态的场景,如文件上传、大数据计算或外部系统通信。...

    comet4j推送Demo

    Comet4j是一个Java库,专门用于实现服务器向客户端的实时推送技术,它基于HTTP长连接,能够在Tomcat这样的Servlet容器环境下运行。在Web开发中,实时推送是让服务器能够主动将新数据推送给客户端,而不是传统的...

    ssm.rar_comet_java comet_java comet推送_聊天 JAVA SSM

    在描述中提到的"Java comet服务器推送(聊天)实现代码",很可能包含了这些Comet技术的具体实现,包括服务器端的Servlet代码和可能的客户端JavaScript代码,用于处理推送的逻辑和展示消息。 SSM框架与Comet结合,可以...

    基于Comet推送技术的实时图形控件

    本篇文章将详细探讨基于Comet推送技术实现的asp.net实时图形控件及其特点。 首先,我们需要理解Comet技术。Comet是一种Web通信模式,它打破了传统的HTTP请求-响应模型,允许服务器向客户端主动推送数据,而不是等待...

    web推送 comet技术

    Comet4J提供了多种推送模式,如轮询、流、隐藏IFrame等,以适应不同的网络环境和浏览器兼容性需求。 在MyEclipse集成开发环境中,你可以创建一个新的Web项目,导入`comet4j`库,然后按照Comet4J的API和文档编写...

    Comet Web 应用

    Servlet 3.0引入了异步处理能力,允许Servlet在请求处理完成后继续运行,这样服务器可以在准备好数据后直接推送给客户端,而无需等待新的请求。Jetty在较早的版本中就支持Comet,并且Jetty 7预览版实现了Servlet 3.0...

    comet4j 自己写的消息推送 觉得实用

    【标题】"comet4j 自己写的消息推送 觉得实用" 提示我们讨论的是一个自定义实现的基于 Comet4j 的消息推送系统。Comet4j 是一个开源的 Java 框架,用于实现实时的、双向的、基于 HTTP 长连接的消息推送服务,它解决...

    comet-iframe.rar

    Comet技术是一种基于HTTP长连接的实时Web通信技术,它能够实现服务器向客户端推送数据,而无需客户端频繁发起请求。在传统的HTTP交互模式中,客户端需要不断地轮询服务器以获取新数据,这种方式效率低且浪费资源。...

    javaweb实现后台向前台的消息推送 comet4j

    使用Comet4j,我们需要创建一个特殊的Servlet,这个Servlet会保持与客户端的连接,直到有新的消息需要推送或者连接超时。以下是一些关键步骤: 1. 引入Comet4j依赖:在项目中添加Comet4j的jar包,确保在类路径下...

    多种方式模拟服务器推送客户端

    在JAVA中,可以通过Servlet 3.0及以上版本的异步处理特性来实现。服务器在接收到请求后不立即响应,而是将请求挂起,当有新数据时再唤醒并返回。这种方式减少了网络交互次数,提高了效率,但对服务器资源消耗较大。 ...

    comet4j的示范项目Demo

    在传统的HTTP协议中,服务器通常只能在客户端发起请求时响应,而Comet技术则打破了这一限制,使得服务器能持续地、异步地向浏览器推送信息,这种模式在实时聊天、股票更新、在线游戏等场景中尤为适用。 `comet4j`的...

    comet4j-tomcat7后台信息推送jar包

    在这个场景中,"comet4j-tomcat7后台信息推送jar包"是一个专门为Tomcat7服务器设计的实现Comet技术的组件。 Tomcat7是Apache软件基金会的Apache Tomcat服务器的一个版本,它是一个开源的、实现了Java Servlet和Java...

    Comet服务器推送技术

    Comet服务器推送技术是一种在Web应用中实现服务器主动向客户端推送数据的技术,它解决了传统HTTP协议下只能由客户端发起请求的局限。随着Ajax技术的普及,开发者希望在浏览器环境中实现更接近桌面应用的实时交互,而...

Global site tag (gtag.js) - Google Analytics