Servlet3.0 异步 servlet Comet 技术

概述

“Comet 技术”、“服务端推技术(Server-Side Push)”、“反向 Ajax 技术”这几个名称说的是同一件事情,可能您已经听说过其中的一项或者几项。但没听说过也没有关系,一句话就足以表达它们全部的意思:“在没有客户端请求的情况下,服务端向客户端发送数据”。

这句话听起来很简单很好理解,但是任何一个长期从事 B/S 应用程序开发的程序都清楚,这实现起来并不简单,甚至很长一段时间内,人们认为这是并不可能的。因为这种做法完全不符合传统基于 HTTP 协议的交互思想:只有基于 Socket 层次的应用才能做到 Server 和 Client 端双方对等通讯,而基于 HTTP 的应用中,Server 只是对来自 Client 的请求进行回应,不关心客户端的状态,不主动向客户端请求信息,因此 Http 协议被称为无状态、单向性协议,这种交互方式称为 Request-Response 交互模型。

无状态、单向的经典 Request-Response 交互模型有很多优点,譬如高效率、高可伸缩等。对于被动响应用户请求为主的应用,像 CMS、MIS、ERP 等非常适合,但是对于另外一些需要服务端主动发送的需求,像聊天室(用户不发言的时候也需要把其它用户的发言传送回来)、日志系统(客户端没有请求,当服务端有日志输出时主动发送到客户端)则处理起来很困难,或者说这类应用根本不适合使用经典的 Request-Response 交互模型来处理。当“不适合”与“有需求”同时存在时,人们就开始不断寻找突破这种限制的方法。

Comet 实现的方法

  • 简单轮询

    最早期的 Web 应用中,主要通过 JavaScript 或者 Meta HTML 标签等手段,定时刷新页面来检测服务端的变化。显然定时刷新页面服务端仍然在被动响应客户端的请求,只不过客户端的请求是连续、频繁的,让用户看起来产生有服务端自动将信息发过来的错觉。这种方式简单易行,但缺陷也非常明显:可能大部分请求都是无意义的,因为服务端期待的事件没有发生,实际上并没有需要发送的信息,而不得不重复的回应着页面上所有内容给浏览器;另外就是当服务端发生变化时,并不能“实时”的返回,刷新的间隔太短,产生很大的性能浪费,间隔太长,事件通知又可能晚于用户期望的时间到达。

    当绝大部分浏览器提供了 XHR(XmlHttpRequest)对象支持后,Ajax 技术出现并迅速流行,这一阶段做的轮询就不必每次都返回都返回整个页面中所有的内容,如果服务端没有事件产生,只需要返回极少量内容的 http 报文体。Ajax 可以节省轮询传输中大量的带宽浪费,但它无法减少请求的次数,因此 Ajax 实现的简单轮询仍然有轮询的局限性,对其缺陷只能一定程度缓解,而无法达到质变。

  • 长轮询(混合轮询)

    长轮询与简单轮询的最大区别就是连接时间的长短:简单轮询时当页面输出完连接就关闭了,而长轮询一般会保持 30 秒乃至更长时间,当服务器上期待的事件发生,将会立刻输出事件通知到客户端,接着关闭连接,同时建立下一个连接开始一次新的长轮询。

    长轮询的实现方式优势在于当服务端期待事件发生,数据便立即返回到客户端,期间没有数据返回,再较长的等待时间内也没有新的请求发生,这样可以让发送的请求减少很多,而事件通知的灵敏度却大幅提高到几乎是“实时”的程度。

  • Comet 流(Forever Frame)

    Comet 流是按照长轮询的实现思路进一步发展的产物。令长轮询将事件通知发送回客户端后不再关闭连接,而是一直保持直到超时事件发生才重新建立新的连接,这种变体我们就称为 Comet 流。客户端可以使用 XmlHttpRequest 对象中的 readyState 属性来判断是 Receiving 还是 Loaded。Comet 流理论上可以使用一个链接来处理若干次服务端事件通知,更进一步节省了发送到服务端的请求次数。

无论是长轮询还是 Comet 流,在服务端和客户端都需要维持一个比较长时间的连接状态,这一点在客户端不算什么太大的负担,但是服务端是要同时对多个客户端服务的,按照经典 Request-Response 交互模型,每一个请求都占用一个 Web 线程不释放的话,Web 容器的线程则会很快消耗殆尽,而这些线程大部分时间处于空闲等待的状态。这也就是为什么 Comet 风格服务非常期待异步处理的原因,希望 Web 线程不需要同步的、一对一的处理客户端请求,能做到一个 Web 线程处理多个客户端请求。


示例程序

本示例模拟一个web页面接收服务器推送数据,服务器 开启 一个线程,循环不停第往所有接入的浏览器推送数据,本示例使用Servlet3.0规范定义的异步Servlet功能(即高级Comet支持),其实对应客户端浏览器来说,并不指定服务器端是否使用了异步Servlet(Comet技术),对应浏览器来说只是一个请求发过去就一直没有结束,因为服务器并没有发送结束标志,只是不断第发送数据过来,通常浏览器在使用Comet技术的时候是要使用一个 隐藏的iframe,以避免请求被I/O阻塞,服务器返回来的数据可以是一段script脚本,这样就能出发浏览器预先定义的方法,本示例就是如此:

package async;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;

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

    public static final Queue ASYNC_CONTEXT_QUEUE = new ConcurrentLinkedQueue();
    private static AsyncContextQueueWriter writer = new AsyncContextQueueWriter(ASYNC_CONTEXT_QUEUE);

    static {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        writer.sendMessage(UUID.randomUUID().toString());
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    /**
     * 将客户端注册到监听 Logger 的消息队列中
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        res.setContentType("text/html;charset=UTF-8");
        res.setHeader("Cache-Control", "private");
        res.setHeader("Pragma", "no-cache");
        req.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();
        writer.println("Comet is a programming technique that enables " +
                "web servers to send data to the client without having any need for the client to request it.");
        writer.flush();

        final AsyncContext ac = req.startAsync();
        ac.setTimeout(60 * 1000);
        ac.addListener(new AsyncListener() {
            public void onComplete(AsyncEvent event) throws IOException {
                ASYNC_CONTEXT_QUEUE.remove(ac);
            }

            public void onTimeout(AsyncEvent event) throws IOException {
                ASYNC_CONTEXT_QUEUE.remove(ac);
            }

            public void onError(AsyncEvent event) throws IOException {
                ASYNC_CONTEXT_QUEUE.remove(ac);
            }

            public void onStartAsync(AsyncEvent event) throws IOException {
            }
        });
        ASYNC_CONTEXT_QUEUE.add(ac);
    }
}
package async;

import javax.servlet.AsyncContext;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 向一个 Queue 中每个 Context 的 Writer 进行输出
 */
public class AsyncContextQueueWriter {
    private Queue queue;
    private static final BlockingQueue MESSAGE_QUEUE = new LinkedBlockingQueue();

    AsyncContextQueueWriter(Queue queue) {
        this.queue = queue;
        Thread notifierThread = new Thread(notifierRunnable);
        notifierThread.start();
    }

    public void sendMessage(String msg) throws IOException {
        try {
            MESSAGE_QUEUE.put(msg);
        } catch (Exception ex) {
            IOException t = new IOException();
            t.initCause(ex);
            throw t;
        }
    }

    /**
     * 异步线程,当消息队列中被放入数据,将释放 take 方法的阻塞,将数据发送到 http response 流上
     */
    private Runnable notifierRunnable = new Runnable() {
        public void run() {
            boolean done = false;
            while (!done) {
                String message = null;
                try {
                    message = MESSAGE_QUEUE.take();
                    for (AsyncContext ac : queue) {
                        try {
                            PrintWriter acWriter = ac.getResponse().getWriter();
                            acWriter.println(htmlEscape(message));
                            acWriter.flush();
                        } catch (IOException ex) {
                            System.out.println(ex);
                            queue.remove(ac);
                        }
                    }
                } catch (InterruptedException iex) {
                    done = true;
                    System.out.println(iex);
                }
            }
        }
    };

    // 因为浏览器的iframe相对于主页面是一个子页面,这里使用了 window.parent,调用其 update 方法,将数据闯入进去
    private String htmlEscape(String message) {
        return "\n";
    }

    public void close() throws IOException {
        for (AsyncContext ac : queue) {
            ac.getResponse().getWriter().close();
        }
    }
}

页面 JSP 代码:

<%
    String ctxPath = request.getContextPath();
%>









访问jsp页面,就可以看到后端服务器不断第向浏览器发送信息,触发浏览器的update方法更新页面。

通过使用异步Servlet(Comet)技术,可以方便第实现聊天室(后端管理所有的AsyncContext对象,将用户的http聊天信息发送到对应AsyncContext对象的response中),传统的同步Servlet也可以做到不断地向浏览器发送数据,但必需维持在Servlet的service方法内部以保护response不会提交,通过response.getWrite().println和flush方法不断第向浏览器发送数据,但是这种模式会占住当前工作线程不放,试想一下如果是每个连接都占用一个线程是多么可怕的一件事(线程资源被耗尽,这些线程可能都处于饥饿状态,处于等待状态),另外一个难题就是response的管理问题,我们很难将客户端所有的response管理起来,这样我们也就很难向指定的response内发送信息,那么聊天之类的程序就比较难实现(不考虑轮询),而异步Servlet的优势就在于原始的Servlet放弃了response的控制权(不再主动处理response提交,而是见api暴露出到AsyncContext中),这样我们就很容易管理各自客户端的response,可以向指定的response中发送消息。

PS:随着JEE7规范的出现,实时通信有了更好的选择,即JSR356规范定义的 WebSocket 1.0,JEE7的应用服务器实现了WebSocket 1.0规范,用户可以非常方便地通过注解的方式实现二进制消息/文本消息的 全双工 传输(它的实现原理是从浏览器到服务器直接建立TCP通信,没有http协议开销,而且可以维持连接不放,相比沦陷资源消耗也非常小),不向Comet会阻塞主客户端请求,WebSocket在任意时刻运行客户端或服务器向对方发送消息,即实现了全双工通信,像聊天室这样的需求,WebSocket绝对是不二之选,唯一的不足是 WebSocket是HTML5技术定义的协议,需要客户端浏览器支持 HTML5。


摘:http://www.ibm.com/developerworks/cn/java/j-lo-comet/index.html

你可能感兴趣的:(其它)