基于Servlet3.0 comet http长连接

基于 HTTP 长连接的“服务器推”技术

Comet 简介

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。 在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。


图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较
基于Servlet3.0 comet http长连接

“服务器推”是一种很早就存在的技术,以前在实现上主要是通过客户端的套接口,或是服务器端的远程调用。因为浏览器技术的 发展比较缓慢,没有为“服务器推”的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现“服务器推”并用于商业程序。最近几年,因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。关于 Comet 技术最新的发展状况请参考关于 Comet 的 wiki。

下面将介绍两种 Comet 应用的实现模型。

基于 AJAX 的长轮询(long-polling)方式

图 1 所示,AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP 请求,JavaScript 响应处理函数根据服务器返回的信息对 HTML 页面的显示进行更新。使用 AJAX 实现“服务器推”与传统的 AJAX 应用不同之处在于:

  1. 服务器端会阻塞请求直到有数据传递或超时才返回。
  2. 客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
  3. 当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。


图 2. 基于长轮询的服务器推模型
基于Servlet3.0 comet http长连接

一些应用及示例如 “Meebo”, “Pushlet Chat” 都采用了这种长轮询的方式。相对于“轮询”(poll),这种长轮询方式也可以称为“拉”(pull)。因为这种方案基于 AJAX,具有以下一些优点:请求异步发出;无须安装插件;IE、Mozilla FireFox 都支持 AJAX。

在这种长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭。Mozilla Firefox 提供了对 Streaming AJAX 的支持, 即 readystate 为 3 时(数据仍在传输中),客户端可以读取数据,从而无须关闭连接,就能读取处理服务器端返回的信息。IE 在 readystate 为 3 时,不能读取服务器返回的数据,目前 IE 不支持基于 Streaming AJAX。

基于 Iframe 及 htmlfile 的流(streaming)方式

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。


图 3. 基于流方式的服务器推模型
基于Servlet3.0 comet http长连接

上节提到的 AJAX 方案是在 JavaScript 里处理 XMLHttpRequest 从服务器取回的数据,然后 Javascript 可以很方便的去控制 HTML 页面的显示。同样的思路用在 iframe 方案的客户端,iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。

图 3 可以看到,每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth's of Google's amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。(请参见 参考资源

@WebServlet(urlPatterns = { "/course/comet" }, asyncSupported = true)
public class CourseCometServlet extends HttpServlet {

    private PublishService publishService = SpringBeanLocator.getInstance().getBean(PublishService.class);

    //  private final Logger log = LoggerFactory.getLogger(getClass());

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

        //  response.reset();
        response.setContentType("text/html; charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache, must-revalidate");
        response.setHeader("Expires", "Mon, 27 Jul 1997 05:00:00 GMT");
        response.setCharacterEncoding("UTF-8");
        final int sid = DataUtil.toInt(request.getParameter("sid"));

        final PrintWriter writer = response.getWriter();

        // 创建Comet Iframe
        writer.println("<!DOCTYPE html>");
        String ua = request.getHeader("User-Agent");
        if (ua.contains("Safari")) {
            // 多打几个,因为Safari至少需要1k数据才会开始渲染。
            for (int i = 0; i < 100; i++) {
                writer.println("<span></span>");
            }
        }
        writer.println("<script>var comet = window.parent.comet;</script>");
        writer.flush();

        try {
            final AsyncContext ac = request.startAsync();
            publishService.add(sid, ac);
            ac.setTimeout(20 * 60 * 1000);// 10分钟时间;tomcat7下默认为10000毫秒
            ac.addListener(new AsyncListener() {
                public void onComplete(AsyncEvent event) throws IOException {

                    System.out.println("the event : " + event.toString() + " is complete now !");
                    publishService.remove(sid, event.getAsyncContext());
                }

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

                    // 尝试向客户端发送超时方法调用,客户端会再次请求/blogpush,周而复始

                    //                String alertStr = "<script>comet.timeout();</script>";
                    //                writer.println(alertStr);
                    //                writer.flush();
                    //                writer.close();

                    publishService.remove(sid, event.getAsyncContext());
                    // 重新dispatch
                    event.getAsyncContext().dispatch();
                }

                public void onError(AsyncEvent event) throws IOException {

                    System.err.println(sid + "the event : " + event.toString() + " is error now !");
                    publishService.remove(sid, event.getAsyncContext());

                }

                public void onStartAsync(AsyncEvent event) throws IOException {
                    System.out.println("the event : " + event.toString() + " is Start Async now !");
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}
var comet = {
		showMsg : function(data) {
			//$("textarea").append(JSON.stringify(data)).append("<hr/>");
			if (console) {
				console.log(data);
			}
			if (data.name == 'question') {
				appendQuestion(data);
			} else {
				append(data);
			}
			scrollBottom('.lecture-main', '.lecture-main-wrap');

		},
		timeout : function() {
			$("#comet").attr("src",
					"/course/comet?sid=${subject.sid }&r=" + Math.random());

		}			
};

   注意事项:

 1 除了servlet 需要 配置 asyncSupported = true 外, filter 也需要配置

<filter>
		<filter-name>characterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<async-supported>true</async-supported>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>

 2, 使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。ie 解决方案

// we were served from child.example.com but 
  // have already set document.domain to example.com
  var currentDomain = "http://exmaple.com/"; 
  var dataStreamUrl = currentDomain+"path/to/server.cgi";
  var transferDoc = new ActiveXObject("htmlfile"); // !?!
  // make sure it's really scriptable
  transferDoc.open();
  transferDoc.write("<html>");
  transferDoc.write("<script>document.domain='"+currentDomain+"';</script>");
  transferDoc.write("</html>");
  transferDoc.close();
  // set the iframe up to call the server for data
  var ifrDiv = transferDoc.createElement("div");
  transferDoc.appendChild(ifrDiv);
  // start communicating
  ifrDiv.innerHTML = "<iframe src='"+dataStreamUrl+"'></iframe>";

3 tomcat comet 配置

Usage of these features requires using the APR or NIO HTTP connectors. The classic java.io HTTP connector and the AJP connectors do not support them,
实际是使用,需要增加对NIO的支持,要做的仅仅是在server.xml里边修改connector:

<connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="8080" redirectport="8443" connectiontimeout="20000">
<Connector 
            asyncTimeout="90000"
               port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />

  参考资料:

http://www.ibm.com/developerworks/cn/web/wa-lo-comet

 http://www.cnblogs.com/haoxin_li/archive/2009/05/15/1457978.html

http://infrequently.org/2006/02/what-else-is-burried-down-in-the-depths-of-googles-amazing-javascript/

相关推荐