Servlet 3.0 实战:异步 Servlet 与 Comet 风格应用程序简介: 自 JSR 315 规范(即 Servlet 3.0)的草案公开发布以来,最新一代 Servlet 规范的各种新特性被越来越多的开发人员所关注。规范中提到的一系列高级目标:如可插拔的 Web 框架、便捷开发特性、增强安全性支持等都令人期待。但其中关注程度最高的,毫无疑问是异步 Servlet。本文将详细介绍 Comet 风格应用的实现方式,以及 Servlet 3.0 中的异步处理特性在 Comet 风格程序中的实际应用。 最早期的 Web 应用中,主要通过 JavaScript 或者 Meta HTML 标签等手段,定时刷新页面来检测服务端的变化。显然定时刷新页面服务端仍然在被动响应客户端的请求,只不过客户端的请求是连续、频繁的,让用户看起来产生有服务端自动将信息发过来的错觉。这种方式简单易行,但缺陷也非常明显:可能大部分请求都是无意义的,因为服务端期待的事件没有发生,实际上并没有需要发送的信息,而不得不重复的回应着页面上所有内容给浏览器;另外就是当服务端发生变化时,并不能“实时”的返回,刷新的间隔太短,产生很大的性能浪费,间隔太长,事件通知又可能晚于用户期望的时间到达。 当绝大部分浏览器提供了 XHR(XmlHttpRequest)对象支持后,Ajax 技术出现并迅速流行,这一阶段做的轮询就不必每次都返回都返回整个页面中所有的内容,如果服务端没有事件产生,只需要返回极少量内容的 http 报文体。Ajax 可以节省轮询传输中大量的带宽浪费,但它无法减少请求的次数,因此 Ajax 实现的简单轮询仍然有轮询的局限性,对其缺陷只能一定程度缓解,而无法达到质变。 长轮询与简单轮询的最大区别就是连接时间的长短:简单轮询时当页面输出完连接就关闭了,而长轮询一般会保持 30 秒乃至更长时间,当服务器上期待的事件发生,将会立刻输出事件通知到客户端,接着关闭连接,同时建立下一个连接开始一次新的长轮询。 长轮询的实现方式优势在于当服务端期待事件发生,数据便立即返回到客户端,期间没有数据返回,再较长的等待时间内也没有新的请求发生,这样可以让发送的请求减少很多,而事件通知的灵敏度却大幅提高到几乎是“实时”的程度。 Comet 流是按照长轮询的实现思路进一步发展的产物。令长轮询将事件通知发送回客户端后不再关闭连接,而是一直保持直到超时事件发生才重新建立新的连接,这种变体我们就称为 Comet 流。客户端可以使用 XmlHttpRequest 对象中的 readyState 属性来判断是 Receiving 还是 Loaded。Comet 流理论上可以使用一个链接来处理若干次服务端事件通知,更进一步节省了发送到服务端的请求次数。 /** * 基于 AsyncContext 支持的 Appender * @author zzm */ public class WebLogAppender extends WriterAppender { /** * 异步 Servlet 上下文队列 */ public static final Queue<AsyncContext> ASYNC_CONTEXT_QUEUE = new ConcurrentLinkedQueue<AsyncContext>(); /** * AsyncContextQueue Writer */ private Writer writer = new AsyncContextQueueWriter(ASYNC_CONTEXT_QUEUE); public WebLogAppender() { setWriter(writer); } public WebLogAppender(Layout layout) { this(); super.layout = layout; } } /** * 向一个 Queue<AsyncContext> 中每个 Context 的 Writer 进行输出 * @author zzm */ public class AsyncContextQueueWriter extends Writer { /** * AsyncContext 队列 */ private Queue<AsyncContext> queue; /** * 消息队列 */ private static final BlockingQueue<String> MESSAGE_QUEUE = new LinkedBlockingQueue<String>(); /** * 发送消息到异步线程,最终输出到 http response 流 * @param cbuf * @param off * @param len * @throws IOException */ private void sendMessage(char[] cbuf, int off, int len) throws IOException { try { MESSAGE_QUEUE.put(new String(cbuf, off, len)); } 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); } } } }; /** * @param message * @return */ private String htmlEscape(String message) { return "<script type='text/javascript'>\nwindow.parent.update(\"" + message.replaceAll("\n", "").replaceAll("\r", "") + "\");</script>\n"; } /** * 保持一个默认的 writer,输出至控制台 * 这个 writer 是同步输出,其它输出到 response 流的 writer 是异步输出 */ private static final Writer DEFAULT_WRITER = new OutputStreamWriter(System.out); /** * 构造 AsyncContextQueueWriter * @param queue */ AsyncContextQueueWriter(Queue<AsyncContext> queue) { this.queue = queue; Thread notifierThread = new Thread(notifierRunnable); notifierThread.start(); } @Override public void write(char[] cbuf, int off, int len) throws IOException { DEFAULT_WRITER.write(cbuf, off, len); sendMessage(cbuf, off, len); } @Override public void flush() throws IOException { DEFAULT_WRITER.flush(); } @Override public void close() throws IOException { DEFAULT_WRITER.close(); for (AsyncContext ac : queue) { ac.getResponse().getWriter().close(); } } } <appender name="CONSOLE" class="org.fenixsoft.log.WebLogAppender"> <param name="Threshold" value="DEBUG"/> <layout class="org.apache.log4j.PatternLayout"> <!-- The default pattern: Date Priority [Category] Message\n --> <param name="ConversionPattern" value="%d %p [%c] %m%n"/> </layout> </appender> /** * Servlet implementation class WebLogServlet */ @WebServlet(urlPatterns = { "/WebLogServlet" }, asyncSupported = true) public class WebLogServlet extends HttpServlet { /** * serialVersionUID */ private static final long serialVersionUID = -260157400324419618L; /** * 将客户端注册到监听 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(); // for IE 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. -->\n"); writer.flush(); final AsyncContext ac = req.startAsync(); ac.setTimeout(10 * 60 * 1000); ac.addListener(new AsyncListener() { public void onComplete(AsyncEvent event) throws IOException { WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); } public void onTimeout(AsyncEvent event) throws IOException { WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); } public void onError(AsyncEvent event) throws IOException { WebLogAppender.ASYNC_CONTEXT_QUEUE.remove(ac); } public void onStartAsync(AsyncEvent event) throws IOException { } }); WebLogAppender.ASYNC_CONTEXT_QUEUE.add(ac); } } <html> <head></head> <script type="text/javascript" src="js/jquery-1.4.min.js"></script> <script type="text/javascript" src="js/application.js"></script> <style> .consoleFont{font-size:9; color:#DDDDDD; font-family:Fixedsys} .inputStyle{font-size:9; color:#DDDDDD; font-family:Fixedsys; width:100%; height:100%; border:0; background-color:#000000;} </style> <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="inputStyle" ></textarea></td> </tr> </table> <iframe id="comet-frame" style="display: none;"></iframe> </body> </html> $(document).ready(function() { var url = '/AsyncServlet/WebLogServlet'; $('#comet-frame')[0].src = url; }); function update(data) { var resultArea = $('#result')[0]; resultArea.value = resultArea.value + data + '\n'; } 描述 | 名字 | 大小 | 下载方法 | 本文用到的示例程序源码 | AsyncServlet.rar | 377 KB | HTTP | |
|
来自: CevenCheng > 《JAVAEE6》