最近接触到了SSE,先说一下概念:
SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。
不同于WebSocket,SSE 是单向通道,只能服务器向客户端发送消息。但对于一般应用来说,客户端向服务器发消息可以直接GET/POST,因此并不是很迫切;而服务器下发才是有一定应用场景的,可以避免客户端频繁的轮询服务器。
SSE 最大的优势是简单,对服务器端和客户端依赖都很少:
服务端:无额外的引用,只需要实现一个Servlet就可以(Servlet 3.0),10几行代码
客户端:只要浏览器支持EventSource,4、5行代码就可以实现。目前主流浏览器基本都支持:
首先实现服务器端,一个Handler,URL为/sse/demo,其中关键的代码是
request.startAsync(),启动一个异步请求,获得了AsyncContext异步请求的上下文
使用AsyncContext.getResponse()获得输出流向客户端发送数据
数据格式是用回车换行符(\r\n)分隔的多行字符串:
id:消息ID\r\n
event:消息类型\r\n
data:数据\r\n
public class SSEHandler extends Handler { private final static int DEFAULT_TIME_OUT = 30 * 1000; @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { if (target.endsWith("/sse/demo")) { response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true); AsyncContext actx = request.startAsync(request, response); actx.setTimeout(DEFAULT_TIME_OUT); actx.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent arg0) throws IOException { // TODO Auto-generated method stub System.out.println("[echo]event complete:" + ((HttpServletRequest)arg0.getSuppliedRequest()).getSession().getId()); } @Override public void onError(AsyncEvent arg0) throws IOException { // TODO Auto-generated method stub System.out.println("[echo]event has error"); } @Override public void onStartAsync(AsyncEvent arg0) throws IOException { // TODO Auto-generated method stub System.out.println("[echo]event start:" + arg0.getSuppliedRequest().getRemoteAddr()); } @Override public void onTimeout(AsyncEvent arg0) throws IOException { // TODO Auto-generated method stub System.out.println("[echo]event time lost"); } }); new Thread(new AsyncWebService(actx)).start(); isHandled[0] = true; } else { if (next != null) { next.handle(target, request, response, isHandled); } } } } class AsyncWebService implements Runnable { AsyncContext ctx; public AsyncWebService(AsyncContext ctx) { this.ctx = ctx; } public void run() { try { PrintWriter out = ctx.getResponse().getWriter(); out.println("event:demo\r\nid:101\r\ndata:中文" + new Date() + "\r\n"); //js页面EventSource接收数据格式:id:[消息Id]\r\nevent:[事件类型]\r\ndata:[数据]\r\n" out.flush(); ctx.complete(); //等待十秒钟,以模拟业务方法的执行 Thread.sleep(10000); } catch (Exception e) { e.printStackTrace(); } } }
客户端代码:
new EventSource('/sse/demo'),连接到服务器对应的URL
source.addEventListener('demo', function(e) {...}, false),添加监听器
<html> <head> <meta charset="utf-8" /> <script type="text/javascript"> //jsp页面js脚本 if (!!window.EventSource) { //EventSource是SSE的客户端.此时说明浏览器支持EventSource对象 var source = new EventSource('/sse/demo');//发送消息 s = ''; source.addEventListener('demo', function(e) { console.info(e); s += e.data + "<br/>"; document.querySelector("#msgFromPush").innerHTML = (s); }, false);//添加客户端的监听 source.addEventListener('open', function(e) { console.log("连接打开"); }, false); source.addEventListener('error', function(e) { if (e.currentTarget.readyState == EventSource.CLOSED) { console.log("连接关闭"); } else { console.log(e.currentTarget.readyState); } }); } else { console.log("您的浏览器不支持SSE"); } </script> </head> <body> <div id="msgFromPush" /> </body> </html>
以上,完成。
看看效果:
客户端:
服务器端,现在打印的是SessionId,每次调用都不同是因为客户端未登录,如果客户端登录了,SessionId就固定了:
是不是够简单?以后可以做为备选方案了。