服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。
一、客户端API(EventSource 对象)
各浏览器支持情况: https://caniuse.com/eventsource
前端测试
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" type="text/css" href="static/css/home.css"> <script> window.onload = ()=> { if (window.EventSource) { let source = new EventSource("/mySpringMVC/sse/subscribe"); let s = ''; // source.addEventListener('message', function(e) { document.querySelector("p").innerText = e.data; }) source.addEventListener('open',function(e){ console.log("connect is open"); },false); source.addEventListener('error',function(e){ if(e.readyState == EventSource.CLOSE){ console.log("connect is close"); }else{ console.log(e.readyState); } },false); } else { alert("浏览器不支持EventSource"); } } </script> </head> <body> <p></p> </body> </html>
对应后端
package MySpringMVC; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.WebAsyncTask; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @Controller @RequestMapping(value = "/sse") public class SSEController { // 新建一个容器,保存连接,用于输出返回 private Map<String, PrintWriter> responseMap = new ConcurrentHashMap<>(); // 发送数据给客户端 private void writeData(String id, String msg, boolean over) throws IOException { PrintWriter pw = responseMap.get(id); if (pw == null) { return; } pw.println(msg); pw.println(); pw.flush(); if (over) { responseMap.remove(id); } } // 推送 @RequestMapping(value = "subscribe") @ResponseBody public WebAsyncTask<Void> subscribe(@RequestParam(defaultValue = "id") String id, HttpServletResponse response) { response.setHeader("Content-Type", "text/event-stream"); response.setCharacterEncoding("utf-8"); Callable<Void> callable = () -> { responseMap.put(id, response.getWriter()); writeData(id, "data:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()) , false); return null; }; // 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称 WebAsyncTask<Void> webAsyncTask = new WebAsyncTask<>(30000, callable); // 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的 webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调")); // 这两个返回的内容,最终都会放进response里面去=========== webAsyncTask.onTimeout(() -> { responseMap.remove(id); System.out.println("超时了!!!"); return null; }); // 备注:这个是Spring5新增的 webAsyncTask.onError(() -> { System.out.println("出现异常!!!"); return null; }); return webAsyncTask; } @RequestMapping(value = "subscribe2") public void subscribe2(HttpServletResponse response) { response.setHeader("Content-Type", "text/event-stream"); response.setCharacterEncoding("utf-8"); try { PrintWriter pw = response.getWriter(); pw.println("data:" + new SimpleDateFormat("YYYY-MM-dd hh:mm:ss").format(new Date())); pw.println(); pw.flush(); } catch (Exception e) { throw new RuntimeException(e); } } }
注意(避坑):
1、数据内容用data
字段表示。
上面代码中,事件对象的 "data:"
属性就是服务器端传回的数据(文本格式),只有这种格式EventSource 的message事件才会触发。
: this is a test stream
data: some text
data: another message
data: with two lines
2、不同事件的内容之间通过仅包含回车符和换行符的空行(“ ”)来分隔 (上文代码用了两个”pw.println()“)
data: first event
data: second event
id: 100
event: myevent
data: third event
id: 101
: this is a comment
data: fourth event
data: fourth event continue
效果(每隔三秒刷新当前时间)
详细文档请看文章:http://www.ruanyifeng.com/blog/2017/05/server-sent_events.html