服務器向瀏覽器推送信息,除了 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\n\n
data: some text\n\n
data: another message\n
data: with two lines \n\n
2、不同事件的內容之間通過僅包含回車符和換行符的空行(“\n\n”)來分隔 (上文代碼用了兩個”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
