SSE(Server-Sent Events):通俗解釋起來就是一種基于HTTP的,以流的形式由服務端持續向客戶端發送數據的技術
應用場景
由于HTTP是無狀態的傳輸協議,每次請求需由客戶端向服務端建立連接,HTTPS還需要交換秘鑰,所以一次請求,建立連接的過程占了很大比例
在http1.1中(1.0也有但未寫入標準),雖然增加了keep-alive來保持和服務器的長連接,省去了很多建立連接的過程,但通信過程仍然是應答式1:1的方式,也就是想要獲得數據,就必須先發送一個request才能得到一個response,所以在實時監控、推送、視頻直播等實時性較高或者帶寬利用較苛刻的場景,仍然不是很合適
SSE技術由于能保持連接,并持續接收服務端的數據,所以彌補了這一缺點,與其他類似技術方案相比,短輪詢、Coment、WebSocket,在大多數時候,SSE仍然是最好的選擇
各技術方案的優缺點
短輪詢
短輪詢很簡單,即客戶端定時的向服務端發送請求,如果服務端有數據返回,則返回數據,否則返回空數據
優點:實現簡單
缺點:如果想實時性好,則必須輪詢間隔短,但會有大量的請求是無效的(返回空數據),如果輪詢間隔長,則實時性不好,數據到達客戶端的延時最大會趨近于輪詢間隔
Coment:一種HACK技術
以即時通信為代表的web應用程序對數據的Low Latency要求,傳統的基于輪詢的方式已經無法滿足,而且也會帶來不好的用戶體驗。于是一種基于http長連接的“服務器推”技術便被hack出來。這種技術被命名為Comet,這個術語由Dojo Toolkit 的項目主管Alex Russell在博文Comet: Low Latency Data for the Browser首次提出,并沿用下來。
Coment技術有兩種實現,分別是長輪詢(long-polling)和基于 Iframe 及 htmlfile 的流(http streaming)方式
1.長輪詢(long-polling)
瀏覽器發出ajax 請求,服務器端接收到請求后,會阻塞請求直到有數據或者超時才返回,瀏覽器JS在處理請求返回信息(超時或有效數據)后再次發出請求,重新建立連接。在此期間服務器端可能已經有新的數據到達,服務器會選擇把數據保存,直到重新建立連接,瀏覽器會把所有數據一次性取回。
優缺點:這種技術沒有明顯的優缺點,如果非要說,就是需要額外的框架支持吧,且在之前服務端異步編程支持程度并不高的時候,(例如java的servlet3.0之前),后端也需要額外的框架支持
2.基于 Iframe 及 htmlfile 的流
Iframe是html標記,這個標記的src屬性會保持對指定服務器的長連接請求,服務器端則可以不停地返回數據,相對于第一種方式,這種方式跟傳統的服務器推則更接近。
在第一種方式中,瀏覽器在收到數據后會直接調用JS回調函數,但是這種方式該如何響應數據呢?可以通過在返回數據中嵌入JS腳本的方式,如“”,服務器端將返回的數據作為回調函數的參數,瀏覽器在收到數據后就會執行這段JS腳本。
缺點:IE、Morzilla Firefox 下端的進度欄都會顯示加載沒有完成,而且 IE 上方的圖標會不停的轉動,表示加載正在進行。
WebSocket
類似TCP socket,參考WebSocket詳解
優點:雙工通信
缺點:需專門定義數據協議,解析數據流,且部分服務器支持不完善,后臺例如java spring boot 2.1.2 僅支持websocket 1.0(最高已達1.3)
SSE
優點:開發簡單,和傳統的http開發幾乎無任何差別,客戶端開發簡單,有標準支持(EventSource)
缺點:和websocket相比,只能單工通信,建立連接后,只能由服務端發往客戶端,且占用一個連接,如需客戶端向服務端通信,需額外打開一個連接
其他
在基于spring的開發中,可以使用SseEmitter類進行通信
@GetMapping(value = "/watch", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public synchronized SseEmitter watch(HttpServletRequest request, @RequestParam("point") String point) throws Exception {
final HttpSession session = request.getSession();
//此處超時時間優先級高于servlet容器的request timeout,PS:此超時時間固定,無法通過心跳等其他手段保持連接,超時后 瀏覽器端默認會重新連接,但SeeEmitter無法復用
SseEmitter emitter = new SseEmitter(300 * 1000L);
String key = String.format("watch:%s", point);
WatchConsumer consumer = new WatchConsumer<>(client, emitter, point);
if (this.client.watch(point, consumer)) {
emitter.onCompletion(() -> session.removeAttribute(key));
emitter.onTimeout(() -> session.removeAttribute(key));
emitter.onError(throwable -> {
throwable.printStackTrace();
session.removeAttribute(key);
});
session.setAttribute(key, consumer);
}
return emitter;
}
也可以利用WebFlux,
@GetMapping("/stream-sse")
public Flux> streamEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> ServerSentEvent. builder()
.id(String.valueOf(sequence))
.event("periodic-event")
.data("SSE - " + LocalTime.now().toString())
.build());
}
但相比之下SseEmitter有OnTimeout和OnCompletion等事件,更加靈活
PS:由于瀏覽器對于同一個domain,有并發數限制,例如chrome最大是6,長連接會持續性的占用一個連接,同時會占用一個服務器端的一個連接