所谓Servlet 异步处理,包括了非阻塞的输入/输出、异步事件通知、延迟request 处理以及延迟response 输出等几种特性。这些特性大多并非JSR 315 规范首次提出,譬如非阻塞输入/输出,在Tomcat 6.0 中就提供了Advanced NIO 技术以便一个Servlet 线程能处理多个HttpRequest,Jetty、GlassFish 也曾经有过类似的支持。但是使用这些Web 容器提供的高级特性时,因为现有的Servlet API 没有对这类应用的支持,所以都必须引入一些Web 容器专有的类、接口或者Annotations,导致使用了这部分高级特性,就必须与特定的容器耦合在一起,这对很多项目来说都是无法接受的。因此JSR 315 将这些特性写入规范,提供统一的API 支持后,这类异步处理特性才真正具备广泛意义上的实用性,只要支持Servlet 3.0 的 Web 容器,就可以不加修改的运行这些Web 程序。
JSR 315 中的Servlet 异步处理系列特性在很多场合都有用武之地,但人们最先看到的,是它们在“服务端推”
(Server-Side Push)方式—— 也称为Comet 方式的交互模型中的价值。在JCP(Java Community Process)网
站上提出的JSR 315 规范目标列表,关于异步处理这个章节的标题就直接定为了“Async and Comet support”(异步与Comet 支持)。
下面将详细介绍Comet 风格应用的实现方式,以及Servlet 3.0 中的异步处理特性在Comet 风格程序中的实际应用。
当前已经有不少支持Servlet API 3.0 的 Web 容器,如GlassFish v3、Tomcat 7.0、Jetty 8.0 等,下面将以实际代码展示Servlet API 3.0如何实现服务端调用web客户端的方法。
对比参考:Comt4j消息推送实例 与 Pushlet实例解析
package org.autocomet; import java.io.IOException; import java.io.PrintWriter; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.servlet.AsyncContext; /** * 服务端通过发送消息到http response流,实现服务端调用客户端 . * @author ices * @version 1.0.0 2013-1-18 上午9:34:49 * @see * @since JDK 1.6.0 */ public class ClientResponseService { /** * 异步Servlet上下文队列. */ private final Queue new ConcurrentLinkedQueue /** * 消息队列. */ private final BlockingQueue new LinkedBlockingQueue /** * 单一实例. */ private static ClientResponseService instance = new ClientResponseService(); /** * 构造函数,创建发送消息的异步线程. * @author ices 2013-1-18 上午10:30:30 */ private ClientResponseService() { new Thread(this.notifierRunnable).start(); } /** * 单一实例. * @author ices 2013-1-18 上午10:31:22 * @return MessageSendService */ public static ClientResponseService getInstance() { return instance; } /** * 注册异步Servlet上下文. * @author ices 2013-1-18 上午10:32:06 * @param asyncContext 异步Servlet上下文. */ public void addAsyncContext(final AsyncContext asyncContext) { ASYNC_CONTEXT_QUEUE.add(asyncContext); } /** * 删除异步Servlet上下文. * @author ices 2013-1-18 上午10:32:35 * @param asyncContext 异步Servlet上下文. */ public void removeAsyncContext(final AsyncContext asyncContext) { ASYNC_CONTEXT_QUEUE.remove(asyncContext); } /** * 调用web客户端. * 发送消息到异步线程,最终输出到http response 流 . * * @author ices 2013-1-18 上午10:26:55 * @param script 发送给客户端的消息. * 实例:"window.parent.update(\"message info\");" */ public void callClient(final String script) { try { MESSAGE_QUEUE.put(script); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * 异步线程,当消息队列中被放入数据,将释放take方法的阻塞,将数据发送到http response流上. */ private Runnable notifierRunnable = new Runnable() { public void run() { boolean done = false; while (!done) { try { final String script = MESSAGE_QUEUE.take(); for (AsyncContext ac : ASYNC_CONTEXT_QUEUE) { try { PrintWriter acWriter = ac.getResponse().getWriter(); acWriter.println(htmlEscape(script)); acWriter.flush(); } catch (IOException ex) { ASYNC_CONTEXT_QUEUE.remove(ac); throw new RuntimeException(ex); } } } catch (InterruptedException iex) { done = true; iex.printStackTrace(); } } } }; /** * 组装web客户端调用脚本. * @param script js脚本. * @return 可执行的script脚本. */ private String htmlEscape(String script) { return "\n"; } } |
信息放置在阻塞队列MESSAGE_QUEUE 中,子线程循环时使用到这个队列的take() 方法,当队列没有数据这个方法将会阻塞线程直到等到新数据放入队列为止。
package org.autocomet; import java.io.IOException; 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; /** * 注册客户端到服务端的监听队列. * @author ices * @version 1.0.0 2013-1-18 上午9:32:16 * @see HttpServlet * @since JDK 1.6.0 */ @WebServlet(urlPatterns = { "/AsyncContextServlet" }, asyncSupported = true) public class AsyncContextServlet extends HttpServlet { /** * 序列化ID. * @author ices 2013-1-18 上午9:33:53 */ private static final long serialVersionUID = 1L; /** * {@inheritDoc} * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) * @author ices 2013-1-18 上午9:33:24 */ @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"); // 将客户端注册到发送消息的监听队列中 final AsyncContext ac = req.startAsync(); ac.setTimeout(10 * 60 * 1000); ac.addListener(new AsyncListener() { public void onComplete(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } public void onTimeout(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } public void onError(AsyncEvent event) throws IOException { ClientResponseService.getInstance().removeAsyncContext(ac); } public void onStartAsync(AsyncEvent event) throws IOException { } }); ClientResponseService.getInstance().addAsyncContext(ac); } } |
建立一个支持异步的Servlet,目的是每个访问这个Servlet 的客户端,都在ASYNC_CONTEXT_QUEUE 中注册一个异步上下文对象,这样当服务端需要调用客户端时,就会输出到这些客户端。同时,将建立一个针对这个异步上下文对象的监听器,当产生超时、错误等事件时,将此上下文从队列中移除。
在客户端我们直接访问这个Servlet 就可以看到浏览器不断的有服务端触发给客户端的信息输出,并且这个页面的滚动条会一直持续,显示http 连接并没有关闭。为了显示,对客户端进行了包装,通过一个隐藏的frame 去读取这个异步Servlet 发出的信息,既Comet 流方式实现。
<html> <head> <script type="text/javascript" src="js/jquery-1.4.min.js">script> <script type="text/javascript" src="js/application.js">script> <style> .resultStyle { font-size:9; color:#DDDDDD; font-family:Fixedsys; width:100%; height:100%; border:0; background-color:#000000; } style> head> <body style="margin:0; overflow:hidden" > <table width="100%" height="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#000000"> <tr> <td colspan="2"> <textarea name="result" id="result" readonly="true" wrap="off" style="padding: 10; overflow:auto" class="resultStyle" >textarea> td> tr> table> <iframe id="autoCometFrame" style="display: none;">iframe> body> html> |
application.js:
$(document).ready(function() { var url = '/AutoComet/AsyncContextServlet'; $('#autoCometFrame')[0].src = url; }); function update(data) { var resultArea = $('#result')[0]; resultArea.value = resultArea.value + data + '\n'; } |
package org.autocomet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 测试的Servlet. * @author ices * @version 1.0.0 2013-1-18 上午10:41:43 * @see * @since JDK 1.6.0 */ @WebServlet("/Test") public class Test extends HttpServlet { /** * 序列化ID. */ private static final long serialVersionUID = 8095181906918852254L; /** * 每隔1秒钟调用一次客户端方法 */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { for (int i = 0; i < 10; i++) { final String script = "window.parent.update(\"" + String.valueOf(i) + "\");"; ClientResponseService.getInstance().callClient(script); Thread.sleep(1 * 1000); if (i == 5) { break; } } } catch (InterruptedException e) { e.printStackTrace(); } } } |
为了模拟输出,服务端提供一个Test Servlet每间隔1秒钟调用一次客户端方法。
首先在浏览器运行:http://IP:8080/AutoComet/,从下面的网络请求可以看出,Servlet异步通信并没有通过轮询的方式实现服务端信息推送。
然后在浏览器运行Test Servlet:http://IP:8080/AutoComet/Test,运行后在上一个页面可以看到服务端把信息推送到浏览器。