场景1:当有新邮件的时候,网页自动弹出提示信息而无需用户手动的刷新收件箱。
场景2:当用户的手机扫描完成页面中的二维码以后,页面会自动跳转。
场景3:在类似聊天室的环境中有任何人发言,所有登录用户都可以即时看见信息。
与传统的MVC模型请求必须从客户端发起由服务器响应相比,使用反向Ajax能够模拟服务器端主动向客户端推送事件从而提高用户体验。本文将分两个部分讨论反向Ajax技术,包括:Comet和WebSocket。文章旨在演示如何实现以上两种技术手段,Struts2或SpringMVC中的应用并未涉及。此外,Servlet的配置也采用注解的方式,相关知识大家可以参考其它资料。
一、Comet(最佳的兼容手段)
Comet本质上则是这样的一种概念:能够从服务器端向客户端发送数据。在一个标准的 HTTP Ajax 请求中,数据是发送给服务器端的,反向 Ajax 以某些特定的方式来模拟发出一个 Ajax 请求,这样的话,服务器就可以尽可能快地向客户端发送事件。由于普通HTTP请求往往会伴随页面的跳转,而推送事件则需要浏览器停留在同一个页面或者框架下,因此Comet的实现只能够通过Ajax来完成。
它的实现过程如下:页面加载的时候随即向服务器发送一条Ajax请求,服务器端获取请求并将它保存在一个线程安全的容器中(通常为队列)。同时服务器端仍然可以正常响应其他请求。当需要推送的事件到来的时候,服务器遍历容器中的请求在返回应答后删除。于是所有停留在页面中的浏览器都会获得该应答,并再次发送Ajax请求,重复上述过程。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "https://www.jb51.net/"; %> <!DOCTYPE html> <html lang="en"> <base href="https://www.jb51.net/<%=basePath%>"> <head> <title>WebSocket</title> <script type="text/javascript" src="https://www.jb51.net/static/jquery-1.9.1.min.js"></script> <script type="text/javascript"> $(function() { connect(); $("#btn").click(function() { var value = $("#message").val(); $.ajax({ url : "longpolling?method=onMessage&msg=" + value, cache : false, dataType : "text", success : function(data) { } }); }); }); function connect() { $.ajax({ url : "longpolling?method=onOpen", cache : false, dataType : "text", success : function(data) { connect(); alert(data); } }); } </script> </head> <body> <h1>LongPolling</h1> <input type="text" /> <input type="button" value="发送" /> </body> </html>
我们注意到,由btn发送的请求其实并不需要获取应答。整个过程的关键是需要客户端始终让服务器保持connect()的请求。而服务器端首先需要支持这种异步的响应方式,幸运的是目前为止绝大部分的Servlet容器都已经提供了良好的支持。下面以Tomcat为例:
package servlet; import java.io.IOException; import java.io.PrintWriter; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(value="/longpolling", asyncSupported=true) public class Comet extends HttpServlet { private static final Queue<AsyncContext> CONNECTIONS = new ConcurrentLinkedQueue<AsyncContext>(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getParameter("method"); if (method.equals("onOpen")) { onOpen(req, resp); } else if (method.equals("onMessage")) { onMessage(req, resp); } } private void onOpen(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { AsyncContext context = req.startAsync(); context.setTimeout(0); CONNECTIONS.offer(context); } private void onMessage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = req.getParameter("msg"); broadcast(msg); } private synchronized void broadcast(String msg) { for (AsyncContext context : CONNECTIONS) { HttpServletResponse response = (HttpServletResponse) context.getResponse(); try { PrintWriter out = response.getWriter(); out.print(msg); out.flush(); out.close(); context.complete(); CONNECTIONS.remove(context); } catch (IOException e) { e.printStackTrace(); } } } }