實時數(shù)據(jù)推送并非只有WebSocket一種選擇
環(huán)境:SpringBoot2.7.16
概述
在Web應用中,有幾種實時數(shù)據(jù)推送的選擇方案,包括SSE(Server-Sent Events)、WebSocket、長輪詢等。
SSE是一種基于HTTP協(xié)議的服務器向客戶端推送數(shù)據(jù)的技術(shù)。它的優(yōu)點是實現(xiàn)簡單、輕量級,對現(xiàn)有服務器軟件兼容性好。但是,由于SSE是單向通信模型,只能由服務器向客戶端推送數(shù)據(jù),對于需要客戶端向服務器發(fā)送數(shù)據(jù)的場景,SSE就無法滿足需求。
WebSocket是一種雙向通信模型,允許客戶端和服務器之間互相發(fā)送消息。它的優(yōu)點是實時性強、延遲低,但是需要服務器端支持對應的協(xié)議棧,實現(xiàn)起來相對復雜一些。
長輪詢是對短輪詢的一種改進版本,通過在盡可能減少對服務器資源浪費的同時,保證消息的相對實時性。長輪詢在客戶端發(fā)起請求時,服務器會保持連接打開,等待一定時間后再返回響應。這樣可以減少客戶端頻繁的請求,節(jié)省帶寬和服務器資源。但是,如果服務器沒有新的消息產(chǎn)生,客戶端會一直等待響應,實時性就會受到一定影響。
根據(jù)實際應用場景和需求,可以選擇適合的實時數(shù)據(jù)推送方案。如果只需要服務器向客戶端推送數(shù)據(jù),且對實時性要求不是特別高,可以選擇SSE。如果需要客戶端向服務器發(fā)送數(shù)據(jù),或者對實時性要求較高,可以選擇WebSocket或長輪詢。當然,也可以根據(jù)實際情況將這幾種方案結(jié)合起來使用,以滿足不同的需求。
SSE與WebSocket對比
SSE(Server-Sent Events)和WebSocket都是用于實現(xiàn)實時通信的技術(shù),存在關鍵差異。
通信模型:SSE是單向通信模型,只能由服務器向客戶端推送數(shù)據(jù)。而WebSocket是雙向通信模型,客戶端和服務器可以互相發(fā)送消息。
連接性:SSE使用長輪詢或HTTP流技術(shù),需要頻繁地發(fā)起HTTP請求來獲取數(shù)據(jù)。而 WebSocket只需在握手階段建立一次連接,然后保持連接打開,減少了頻繁建立連接的開銷。
實時性:WebSocket提供了更低的延遲和更高的實時性,因為它支持雙向通信,可以立即將數(shù)據(jù)推送給客戶端。SSE雖然也可以實現(xiàn)實時性,但由于其單向通信模型,需要服務器定期發(fā)送數(shù)據(jù)。
協(xié)議特性:SSE是部署在HTTP協(xié)議之上的,現(xiàn)有的服務器軟件都支持。而WebSocket是一個新的協(xié)議,需要服務器端支持對應的協(xié)議棧。
復雜性:SSE相對WebSocket來說更輕量級,實現(xiàn)更簡單。WebSocket協(xié)議較復雜,實現(xiàn)相對困難一些。
總體來說,SSE和WebSocket都有各自的優(yōu)點和適用場景。SSE輕量級且對現(xiàn)有服務器軟件兼容性好,而WebSocket則提供了更強的雙向通信能力和更高的實時性。
SSE簡介
SSE(Server-Sent Events)是一種用于實現(xiàn)服務器向客戶端實時推送數(shù)據(jù)的Web技術(shù)。與傳統(tǒng)的輪詢和長輪詢相比,SSE提供了更高效和實時的數(shù)據(jù)推送機制。
SSE基于HTTP協(xié)議,允許服務器將數(shù)據(jù)以事件流(Event Stream)的形式發(fā)送給客戶端。客戶端通過建立持久的HTTP連接,并監(jiān)聽事件流,可以實時接收服務器推送的數(shù)據(jù)。
SSE的主要特點包括:
簡單易用:SSE使用基于文本的數(shù)據(jù)格式,如純文本、JSON等,使得數(shù)據(jù)的發(fā)送和解析都相對簡單。
單向通信:SSE支持服務器向客戶端的單向通信,服務器可以主動推送數(shù)據(jù)給客戶端。
實時性:SSE建立長時間的連接,使得服務器可以實時地將數(shù)據(jù)推送給客戶端,而無需客戶端頻繁地發(fā)起請求。
服務端開發(fā)
依賴管理
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件
spring:
mvc:
static-path-pattern: /**
web:
resources:
#靜態(tài)文件目錄index.html
static-locations: classpath:/templates/
接口開發(fā)
@RestController
@RequestMapping("/sse")
public class SseController {
// 該集合用來管理所有客戶端的連接
private final Map<String, SseEmitter> sse = new ConcurrentHashMap<>() ;
// 創(chuàng)建連接接口,同時指定了消息類型為text/event-stream
@GetMapping(path="/events/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter createConnect(@PathVariable("id") String id) throws IOException {
SseEmitter emitter = new SseEmitter(0L);
// 每一個客戶端保存到Map中
sse.put(id, emitter) ;
// 當發(fā)生錯誤的回調(diào)
emitter.onError(ex -> {
System.err.printf("userId: %s, error: %s%n", id, ex.getMessage()) ;
sse.remove(id) ;
}) ;
// 異步請求完成后的回調(diào)
emitter.onCompletion(() -> {
sse.remove(id) ;
System.out.printf("%s, 請求完成...") ;
}) ;
// 異步請求超時回調(diào)
emitter.onTimeout(() -> {
System.err.println("超時...") ;
}) ;
return emitter;
}
// 該接口用來進行消息的發(fā)送
// 由客戶端發(fā)起請求,然后根據(jù)id獲取相應的SseEmitter進行消息的發(fā)送
@GetMapping("/sender/{id}")
public String sender(@PathVariable("id") String id) throws Exception {
SseEmitter emitter = this.sse.get(id) ;
if (emitter != null) {
try {
emitter.send( "隨機消息 - " + new Random().nextInt(10000000)) ;
} catch (Exception e) {
System.err.println("%s%n", e.getMessage()) ;
}
}
return "success" ;
}
}
前端開發(fā)
前端比較簡單就是一個index.html頁面
<html>
<head>
<title>SSE</title>
</head>
<body>
<button type="button" onclick="closeSse()">Close</button>
<hr style="margin: 2px; padding: 0px 0px;"/>
<ul id="list"></ul>
</body>
<script>
const evtSource = new EventSource(`/sse/events/${Date.now()}`) ;
evtSource.onmessage = (event) => {
const newElement = document.createElement("li") ;
const eventList = document.getElementById("list") ;
newElement.innerHTML = "接收到消息: " + event.data ;
eventList.appendChild(newElement) ;
};
evtSource.onopen = (event) => {
console.log('建立連接...')
};
evtSource.onerror = (event) => {
console.error("發(fā)生錯誤:", event) ;
};
function closeSse() {
evtSource.close() ;
}
</script>
</html>
以上就是前后端的開發(fā),代碼非常的簡單;也就簡單的實現(xiàn)了由服務端實時數(shù)據(jù)推送。
EventSource對象的readyState有3個狀態(tài)值:
0 — connecting
1 — open
2 — closed
測試
圖片
調(diào)用消息發(fā)送接口
圖片
圖片
自定義事件類型
修改消息發(fā)送接口
@GetMapping("/sender/{id}")
public String sender(@PathVariable("id") String id) throws Exception {
SseEmitter emitter = this.sse.get(id) ;
if (emitter != null) {
SseEventBuilder builder = SseEmitter.event() ;
// 指定事件類型
builder.name("chat") ;
String msg = "隨機消息 - " + new Random().nextInt(10000000);
builder.data(msg) ;
try {
emitter.send(builder) ;
} catch (Exception e) {
e.printStackTrace();
}
}
return "success" ;
}
前端監(jiān)聽具體事件類型消息
// 監(jiān)聽指定事件類型消息
evtSource.addEventListener("chat", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.innerHTML = "chat message: " + event.data;
eventList.appendChild(newElement);
});
注意:默認是“message”事件,因為它可以捕獲沒有 event 字段的事件, * 以及具有特定類型 `event:message` 的事件。* 它不會觸發(fā)任何其他類型的事件。