簡介
Server-Sent Events(SSE)是一種基於HTTP的單向推送技術,允許服務器主動向客户端發送數據。與WebSocket不同,SSE是單向通信,使用標準HTTP連接,實現簡單,特別適合服務器推送實時數據的場景。
本教程將詳細介紹如何在Spring Boot中實現SSE功能。
一、SSE基礎概念
1.1 SSE的特點
- 單向通信:只有服務器可以向客户端發送數據
- 基於HTTP:使用標準的HTTP協議,無需額外協議升級
- 自動重連:瀏覽器內置重連機制
- 低延遲:相比輪詢方式,延遲更低
- 簡單易用:實現相對簡單,適合快速開發
1.2 SSE vs WebSocket vs 長輪詢
|
特性
|
SSE
|
WebSocket
|
長輪詢
|
|
通信方向
|
單向
|
雙向
|
單向
|
|
協議
|
HTTP
|
HTTP升級
|
HTTP
|
|
自動重連
|
是
|
否
|
否
|
|
性能消耗
|
低
|
中
|
高
|
|
實現難度
|
低
|
中
|
低
|
二、項目環境準備
2.1 依賴配置
在pom.xml中添加Spring Boot Web依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.2 Maven版本
- Java 8+
- Spring Boot 2.5+
- Maven 3.6+
三、後端實現
3.1 創建SSE控制器
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@RestController
public class SseController {
// 存儲客户端連接
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
/**
* 建立SSE連接
*/
@GetMapping("/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
SseEmitter emitter = new SseEmitter(60000L);
emitters.add(emitter);
// 連接建立時發送初始消息
try {
emitter.send(SseEmitter.event()
.id("1")
.name("connected")
.data("SSE connection established")
.reconnectTime(5000)
.build());
} catch (IOException e) {
e.printStackTrace();
emitters.remove(emitter);
}
// 連接關閉時處理
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
return emitter;
}
/**
* 向所有客户端推送消息
*/
@PostMapping("/broadcast")
public ResponseEntity<?> broadcast(@RequestParam String message) {
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
.id(UUID.randomUUID().toString())
.name("message")
.data(message)
.build());
} catch (IOException e) {
emitters.remove(emitter);
}
});
return ResponseEntity.ok("Message sent to " + emitters.size() + " clients");
}
}
3.2 完整的Service層
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
public class SseService {
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
public SseEmitter createConnection() {
SseEmitter emitter = new SseEmitter(60000L);
emitters.add(emitter);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(throwable -> emitters.remove(emitter));
return emitter;
}
public void sendToAll(String eventName, String data) {
List<SseEmitter> failedEmitters = new ArrayList<>();
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
.id(UUID.randomUUID().toString())
.name(eventName)
.data(data)
.build());
} catch (IOException e) {
failedEmitters.add(emitter);
}
});
emitters.removeAll(failedEmitters);
}
public int getActiveConnections() {
return emitters.size();
}
}
四、前端實現
4.1 HTML頁面
<!DOCTYPE html>
<html>
<head>
<title>Spring Boot SSE Demo</title>
<style>
body { font-family: Arial; margin: 20px; }
#messages { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: auto; }
.message { margin: 5px 0; padding: 5px; background: #f0f0f0; }
</style>
</head>
<body>
<h1>SSE 實時推送演示</h1>
<button onclick="connectSSE()">連接</button>
<button onclick="disconnectSSE()">斷開連接</button>
<input type="text" id="messageInput" placeholder="輸入消息">
<button onclick="sendMessage()">發送</button>
<div id="messages"></div>
<script>
let eventSource;
function connectSSE() {
eventSource = new EventSource('/connect');
eventSource.onopen = function() {
console.log('SSE connection established');
addMessage('系統', '連接已建立');
};
eventSource.addEventListener('message', function(e) {
addMessage('服務器', e.data);
});
eventSource.onerror = function(e) {
if (e.target.readyState === EventSource.CLOSED) {
console.log('Connection closed');
addMessage('系統', '連接已關閉');
}
};
}
function disconnectSSE() {
if (eventSource) {
eventSource.close();
addMessage('系統', '連接已斷開');
}
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value;
if (message.trim() === '') return;
fetch('/broadcast?message=' + encodeURIComponent(message), {
method: 'POST'
}).then(response => response.json())
.then(data => console.log(data));
input.value = '';
}
function addMessage(sender, text) {
const div = document.getElementById('messages');
const msg = document.createElement('div');
msg.className = 'message';
msg.innerHTML = '<strong>' + sender + ':</strong> ' + text;
div.appendChild(msg);
div.scrollTop = div.scrollHeight;
}
</script>
</body>
</html>
五、測試應用
5.1 啓動應用
mvn spring-boot:run
5.2 打開瀏覽器
訪問 http://localhost:8080 進行測試
5.3 使用curl測試推送
# 在一個終端連接SSE
curl http://localhost:8080/connect
# 在另一個終端發送消息
curl -X POST "http://localhost:8080/broadcast?message=Hello%20SSE"
六、高級特性
6.1 添加心跳檢測
@Scheduled(fixedRate = 30000)
public void sendHeartbeat() {
sseService.sendToAll("heartbeat", System.currentTimeMillis() + "");
}
6.2 錯誤處理和重連
function connectSSEWithRetry(maxAttempts = 5) {
let attempts = 0;
function connect() {
eventSource = new EventSource('/connect');
eventSource.onerror = function(e) {
if (e.target.readyState === EventSource.CLOSED) {
attempts++;
if (attempts < maxAttempts) {
console.log('Reconnecting... Attempt ' + attempts);
setTimeout(connect, 3000 * attempts);
}
}
};
}
connect();
}
七、常見問題
7.1 連接超時問題
- 增加SseEmitter的超時時間
- 實現心跳機制保活連接
- 檢查代理/防火牆是否限制連接
7.2 消息丟失
- 實現消息隊列持久化
- 使用事件ID追蹤已發送消息
- 客户端需要時可請求重放最近N條消息
7.3 性能優化
- 使用線程池異步處理髮送
- 實現消息分組,只發送相關消息
- 對大數據進行分片傳輸
八、總結
SSE是一種簡單而有效的服務器推送解決方案,特別適合:n- 實時通知系統
- 數據流更新
- 進度提示
- 日誌實時輸出
相比WebSocket實現更簡單,但功能受限,開發者需根據實際需求選擇合適的方案。