簡介

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實現更簡單,但功能受限,開發者需根據實際需求選擇合適的方案。