一、什么是SSE
SSE(Server-Sent Events) 是一种基于HTTP的服务器推送技术,允许服务器向客户端单向发送事件流。它是HTML5规范的一部分,专为服务器到客户端的单向实时通信而设计。
1.1 SSE的核心特性
- 单向通信:仅支持服务器向客户端推送数据
- 基于HTTP:使用标准HTTP协议,无需额外的连接建立
- 自动重连:浏览器会自动处理断线重连
- 文本格式:传输的数据必须是文本格式
- 事件模型:支持事件类型和ID,便于客户端处理
二、SSE工作原理
2.1 连接建立流程
服务器 客户端 服务器 客户端 保持连接开放 持续传输... 发送HTTP请求Accept: text/event-stream 返回200 OKContent-Type: text/event-stream data: 消息1\n\n data: 消息2\n\n data: 消息3\n\n 连接断开 自动重连请求Last-Event-ID: xxx 继续发送后续消息
2.2 SSE数据格式
SSE的传输格式非常简单,每条消息由一个或多个字段组成,以换行符分隔:
字段名: 值\n
常用字段:
data: 消息内容(必需)event: 事件类型(可选)id: 事件ID(可选)retry: 重连间隔时间(可选,单位毫秒)
示例:
id: 1
event: message
data: {"content": "Hello SSE"}
id: 2
event: notification
data: {"title": "新消息", "count": 5}
data: 简单文本消息
注意: 每条消息必须以两个换行符 \n\n 结束。
2.3 消息类型处理
是
否
是
否
SSE消息到达
有event字段?
触发对应事件监听器
触发默认message事件
客户端处理特定事件
有id字段?
保存Last-Event-ID
不更新ID
断线重连时使用此ID
三、SSE与WebSocket对比
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议(ws://或wss://) |
| 连接建立 | 简单(标准HTTP请求) | 复杂(握手过程) |
| 自动重连 | 内置支持 | 需手动实现 |
| 数据格式 | 仅文本 | 支持文本和二进制 |
| 浏览器支持 | 广泛支持 | 广泛支持 |
| 服务器资源 | 相对较低 | 相对较高 |
| 适用场景 | 实时通知、数据推送 | 聊天、游戏、双向交互 |
3.1 选择建议
选择SSE的场景:
- 服务器单向推送数据
- 需要自动重连机制
- 推送的是文本数据
- 实时通知、数据更新
选择WebSocket的场景:
- 需要双向通信
- 需要传输二进制数据
- 低延迟要求极高的实时交互
- 聊天、协作编辑、在线游戏
四、SSE与传统轮询对比
SSE
一次请求
持续推送
客户端
服务器
传统轮询
定时请求
返回数据
客户端
服务器
轮询的问题:
- 频繁请求消耗服务器资源
- 数据延迟(取决于轮询间隔)
- 大量无效请求(无新数据时)
SSE的优势:
- 一次连接,持续推送
- 实时性高
- 资源消耗低
五、Spring Boot集成SSE
5.1 基础实现
创建SSE控制器:
java
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
@RequestMapping("/sse")
public class SseController {
private final ExecutorService executor = Executors.newCachedThreadPool();
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents() {
SseEmitter emitter = new SseEmitter(60_000L); // 超时时间60秒
executor.execute(() -> {
try {
for (int i = 0; i < 10; i++) {
// 模拟业务处理
Thread.sleep(1000);
// 发送消息
emitter.send(SseEmitter.event()
.id(String.valueOf(i))
.name("message")
.data("消息内容: " + i)
.reconnectTime(5000L)); // 重连间隔5秒
System.out.println("发送消息: " + i);
}
// 发送完成事件
emitter.complete();
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
5.2 完整的SSE服务实现
SSE服务类:
java
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Service
public class SseService {
// 存储所有活跃的SSE连接
private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();
// 存储用户ID到连接ID的映射
private final ConcurrentHashMap<String, String> userConnections = new ConcurrentHashMap<>();
// 存储订阅关系(用户ID -> 订阅的频道)
private final ConcurrentHashMap<String, CopyOnWriteArraySet<String>> subscriptions = new ConcurrentHashMap<>();
/**
* 创建SSE连接
*/
public SseEmitter createConnection(String userId) {
String connectionId = generateConnectionId();
// 设置超时时间为30分钟
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
// 连接关闭时的处理
emitter.onCompletion(() -> {
System.out.println("连接完成: " + connectionId);
removeConnection(connectionId);
});
emitter.onTimeout(() -> {
System.out.println("连接超时: " + connectionId);
removeConnection(connectionId);
});
emitter.onError((ex) -> {
System.err.println("连接错误: " + connectionId);
ex.printStackTrace();
removeConnection(connectionId);
});
// 存储连接
emitters.put(connectionId, emitter);
userConnections.put(userId, connectionId);
// 发送连接成功消息
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("连接建立成功")
.id(connectionId));
} catch (IOException e) {
removeConnection(connectionId);
throw new RuntimeException("建立连接失败", e);
}
return emitter;
}
/**
* 向指定用户发送消息
*/
public boolean sendToUser(String userId, String eventName, Object data) {
String connectionId = userConnections.get(userId);
if (connectionId == null) {
return false;
}
return sendToConnection(connectionId, eventName, data);
}
/**
* 向指定连接发送消息
*/
public boolean sendToConnection(String connectionId, String eventName, Object data) {
SseEmitter emitter = emitters.get(connectionId);
if (emitter == null) {
return false;
}
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data)
.id(String.valueOf(System.currentTimeMillis())));
return true;
} catch (IOException e) {
System.err.println("发送消息失败: " + connectionId);
removeConnection(connectionId);
return false;
}
}
/**
* 向所有连接广播消息
*/
public void broadcast(String eventName, Object data) {
emitters.forEach((connectionId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data)
.id(String.valueOf(System.currentTimeMillis())));
} catch (IOException e) {
System.err.println("广播消息失败: " + connectionId);
removeConnection(connectionId);
}
});
}
/**
* 移除连接
*/
private void removeConnection(String connectionId) {
SseEmitter emitter = emitters.remove(connectionId);
if (emitter != null) {
emitter.complete();
}
// 移除用户映射
userConnections.entrySet().removeIf(entry -> entry.getValue().equals(connectionId));
System.out.println("连接已移除: " + connectionId + ", 当前连接数: " + emitters.size());
}
/**
* 获取当前连接数
*/
public int getConnectionCount() {
return emitters.size();
}
/**
* 生成连接ID
*/
private String generateConnectionId() {
return java.util.UUID.randomUUID().toString();
}
}
控制器使用服务:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/api/sse")
public class SseController {
@Autowired
private SseService sseService;
/**
* 建立SSE连接
*/
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId) {
System.out.println("用户连接: " + userId);
return sseService.createConnection(userId);
}
/**
* 向指定用户发送消息(测试用)
*/
@PostMapping("/send")
public String sendToUser(
@RequestParam String userId,
@RequestParam(defaultValue = "message") String eventName,
@RequestBody Object data) {
boolean success = sseService.sendToUser(userId, eventName, data);
return success ? "发送成功" : "发送失败(用户未连接)";
}
/**
* 广播消息(测试用)
*/
@PostMapping("/broadcast")
public String broadcast(
@RequestParam(defaultValue = "message") String eventName,
@RequestBody Object data) {
sseService.broadcast(eventName, data);
return "广播成功";
}
/**
* 获取在线连接数
*/
@GetMapping("/count")
public int getConnectionCount() {
return sseService.getConnectionCount();
}
}
5.3 客户端代码示例
JavaScript客户端:
javascript
// 建立SSE连接
const userId = 'user123';
const eventSource = new EventSource(`http://localhost:8080/api/sse/connect?userId=${userId}`);
// 监听连接成功事件
eventSource.addEventListener('connected', (event) => {
console.log('连接成功:', event.data);
console.log('连接ID:', event.lastEventId);
});
// 监听自定义消息事件
eventSource.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
const data = JSON.parse(event.data);
// 处理消息...
});
// 监听通知事件
eventSource.addEventListener('notification', (event) => {
console.log('收到通知:', event.data);
const notification = JSON.parse(event.data);
// 显示通知...
});
// 监听错误
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// 浏览器会自动尝试重连
};
// 关闭连接
// eventSource.close();
带重试和心跳的客户端:
javascript
class SseClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 5000,
maxReconnectAttempts: 10,
heartbeatInterval: 30000,
...options
};
this.reconnectAttempts = 0;
this.eventSource = null;
this.heartbeatTimer = null;
this.isConnected = false;
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onopen = () => {
console.log('SSE连接已建立');
this.isConnected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
};
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
this.isConnected = false;
this.stopHeartbeat();
if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`尝试重连 (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`);
setTimeout(() => this.connect(), this.options.reconnectInterval);
} else {
console.error('达到最大重连次数,停止重连');
}
};
return this.eventSource;
}
addEventListener(event, callback) {
if (this.eventSource) {
this.eventSource.addEventListener(event, callback);
}
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
console.log('心跳检测');
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
close() {
this.stopHeartbeat();
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.isConnected = false;
}
}
// 使用示例
const sseClient = new SseClient('http://localhost:8080/api/sse/connect?userId=user123', {
reconnectInterval: 3000,
maxReconnectAttempts: 5,
heartbeatInterval: 60000
});
sseClient.connect();
sseClient.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
sseClient.addEventListener('notification', (event) => {
console.log('收到通知:', event.data);
});
// 关闭连接
// sseClient.close();
六、实际应用场景
6.1 实时消息通知
建立SSE连接
实时推送
订单状态变更
新评论
系统通知
显示通知
用户
前端页面
通知服务
订单服务
评论服务
系统服务
实现示例:
java
@Service
public class NotificationService {
@Autowired
private SseService sseService;
/**
* 发送订单状态变更通知
*/
public void sendOrderNotification(String userId, Order order) {
Notification notification = Notification.builder()
.type("order")
.title("订单状态更新")
.message("您的订单 " + order.getOrderNo() + " 状态已变更为: " + order.getStatus())
.data(order)
.timestamp(LocalDateTime.now())
.build();
sseService.sendToUser(userId, "notification", notification);
}
/**
* 发送评论通知
*/
public void sendCommentNotification(String userId, Comment comment) {
Notification notification = Notification.builder()
.type("comment")
.title("新评论")
.message("用户 " + comment.getAuthor() + " 评论了你的文章")
.data(comment)
.timestamp(LocalDateTime.now())
.build();
sseService.sendToUser(userId, "notification", notification);
}
}
6.2 实时数据大屏
java
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {
@Autowired
private SseService sseService;
@Autowired
private DataService dataService;
/**
* 实时数据推送
*/
@GetMapping(value = "/realtime", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter realtimeData() {
String userId = "dashboard_" + System.currentTimeMillis();
SseEmitter emitter = sseService.createConnection(userId);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
// 获取实时数据
DashboardData data = dataService.getRealtimeData();
// 推送数据
sseService.sendToUser(userId, "dashboard", data);
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 1, TimeUnit.SECONDS); // 每秒推送一次
emitter.onCompletion(() -> scheduler.shutdown());
emitter.onTimeout(() -> scheduler.shutdown());
return emitter;
}
}
6.3 实时日志监控
java
@Service
public class LogMonitorService {
@Autowired
private SseService sseService;
private final CopyOnWriteArraySet<String> subscribers = new CopyOnWriteArraySet<>();
/**
* 订阅日志
*/
public SseEmitter subscribeLogs(String userId) {
subscribers.add(userId);
return sseService.createConnection(userId);
}
/**
* 推送日志
*/
public void pushLog(LogEntry logEntry) {
if (!subscribers.isEmpty()) {
subscribers.forEach(userId -> {
sseService.sendToUser(userId, "log", logEntry);
});
}
}
/**
* 取消订阅
*/
public void unsubscribe(String userId) {
subscribers.remove(userId);
}
}
6.4 进度条推送
java
@RestController
@RequestMapping("/api/task")
public class TaskController {
@Autowired
private SseService sseService;
/**
* 执行长任务并推送进度
*/
@GetMapping(value = "/execute", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter executeTask(@RequestParam String userId, @RequestParam String taskId) {
SseEmitter emitter = sseService.createConnection(userId);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
// 推送任务开始
sseService.sendToUser(userId, "progress", Progress.builder()
.taskId(taskId)
.stage("初始化")
.percentage(0)
.message("任务开始")
.build());
// 模拟任务执行
for (int i = 0; i <= 100; i += 10) {
Thread.sleep(1000);
sseService.sendToUser(userId, "progress", Progress.builder()
.taskId(taskId)
.stage("处理中")
.percentage(i)
.message("正在处理... " + i + "%")
.build());
}
// 任务完成
sseService.sendToUser(userId, "progress", Progress.builder()
.taskId(taskId)
.stage("完成")
.percentage(100)
.message("任务完成")
.build());
sseService.sendToUser(userId, "complete", TaskResult.builder()
.taskId(taskId)
.success(true)
.message("任务执行成功")
.build());
emitter.complete();
} catch (Exception e) {
sseService.sendToUser(userId, "error", TaskResult.builder()
.taskId(taskId)
.success(false)
.message("任务执行失败: " + e.getMessage())
.build());
emitter.completeWithError(e);
} finally {
executor.shutdown();
}
});
return emitter;
}
}
七、生产环境最佳实践
7.1 连接管理
java
@Service
public class SseConnectionManager {
private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@PostConstruct
public void init() {
// 定期清理过期连接
scheduler.scheduleAtFixedRate(this::cleanExpiredConnections, 5, 5, TimeUnit.MINUTES);
}
private void cleanExpiredConnections() {
emitters.forEach((id, emitter) -> {
try {
// 发送心跳检测
emitter.send(SseEmitter.event().name("heartbeat").data("ping"));
} catch (IOException e) {
// 发送失败,移除连接
removeConnection(id);
}
});
}
@PreDestroy
public void destroy() {
scheduler.shutdown();
emitters.forEach((id, emitter) -> emitter.complete());
emitters.clear();
}
}
7.2 消息队列集成
java
@Service
public class SseMessageQueueService {
@Autowired
private SseService sseService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息到队列
*/
public void sendMessageToQueue(String userId, String eventName, Object data) {
SseMessage message = SseMessage.builder()
.userId(userId)
.eventName(eventName)
.data(data)
.timestamp(LocalDateTime.now())
.build();
rabbitTemplate.convertAndSend("sse.exchange", "sse.routing", message);
}
/**
* 从队列消费消息并推送
*/
@RabbitListener(queues = "sse.queue")
public void handleSseMessage(SseMessage message) {
sseService.sendToUser(message.getUserId(), message.getEventName(), message.getData());
}
}
7.3 分布式环境支持
java
@Service
public class DistributedSseService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SseService sseService;
/**
* 发布消息到Redis
*/
public void publishMessage(String channel, String eventName, Object data) {
SseMessage message = SseMessage.builder()
.eventName(eventName)
.data(data)
.timestamp(LocalDateTime.now())
.build();
redisTemplate.convertAndSend(channel, message);
}
/**
* 订阅Redis消息
*/
@PostConstruct
public void subscribeChannels() {
redisTemplate.getConnectionFactory()
.getConnection()
.subscribe((message, pattern) -> {
try {
SseMessage sseMessage = JsonUtils.fromJson(
new String(message.getBody()),
SseMessage.class
);
// 广播给当前服务器的所有连接
sseService.broadcast(sseMessage.getEventName(), sseMessage.getData());
} catch (Exception e) {
e.printStackTrace();
}
}, "sse.channel.*");
}
}
八、性能优化建议
8.1 连接数限制
java
@Configuration
public class SseConfig {
@Bean
public SseService sseService() {
return new SseService() {
private static final int MAX_CONNECTIONS = 10000;
private final AtomicInteger connectionCount = new AtomicInteger(0);
@Override
public SseEmitter createConnection(String userId) {
if (connectionCount.get() >= MAX_CONNECTIONS) {
throw new RuntimeException("连接数已达上限");
}
connectionCount.incrementAndGet();
return super.createConnection(userId);
}
@Override
public void removeConnection(String connectionId) {
super.removeConnection(connectionId);
connectionCount.decrementAndGet();
}
};
}
}
8.2 消息压缩
java
@RestController
@RequestMapping("/api/sse")
public class CompressedSseController {
@GetMapping(value = "/compressed", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamCompressedEvents(@RequestParam String userId) {
SseEmitter emitter = new SseEmitter(60_000L);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 100; i++) {
// 原始数据
Map<String, Object> data = new HashMap<>();
data.put("id", i);
data.put("timestamp", System.currentTimeMillis());
data.put("content", "这是一条测试消息");
// 序列化为JSON
String json = JsonUtils.toJson(data);
// 使用gzip压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
gzip.write(json.getBytes(StandardCharsets.UTF_8));
}
// 发送压缩后的数据
emitter.send(SseEmitter.event()
.name("message")
.data(Base64.getEncoder().encodeToString(bos.toByteArray()))
.comment("gzip"));
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
} finally {
executor.shutdown();
}
});
return emitter;
}
}
九、常见问题与解决方案
9.1 连接超时问题
问题: SSE连接在一段时间后自动断开
解决方案:
java
// 1. 设置合理的超时时间
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟
// 2. 定期发送心跳保持连接
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event().name("heartbeat").data("ping"));
} catch (IOException e) {
emitter.completeWithError(e);
}
}, 0, 30, TimeUnit.SECONDS); // 每30秒发送一次心跳
9.2 消息丢失问题
问题: 客户端断线重连后丢失部分消息
解决方案:
java
@Service
public class ReliableSseService {
// 存储消息历史
private final ConcurrentHashMap<String, LinkedList<SseMessage>> messageHistory = new ConcurrentHashMap<>();
private static final int MAX_HISTORY_SIZE = 100;
/**
* 发送消息并保存历史
*/
public void sendWithHistory(String userId, String eventName, Object data) {
SseMessage message = SseMessage.builder()
.id(generateMessageId())
.eventName(eventName)
.data(data)
.timestamp(LocalDateTime.now())
.build();
// 保存到历史
messageHistory.computeIfAbsent(userId, k -> new LinkedList<>())
.addLast(message);
// 限制历史大小
if (messageHistory.get(userId).size() > MAX_HISTORY_SIZE) {
messageHistory.get(userId).removeFirst();
}
// 发送消息
sseService.sendToUser(userId, eventName, data);
}
/**
* 重连时发送历史消息
*/
public void sendHistoryOnReconnect(String userId, String lastEventId) {
LinkedList<SseMessage> history = messageHistory.get(userId);
if (history != null) {
history.stream()
.filter(msg -> msg.getId().compareTo(lastEventId) > 0)
.forEach(msg -> sseService.sendToUser(userId, msg.getEventName(), msg.getData()));
}
}
}
9.3 Nginx代理配置
问题: 通过Nginx代理后SSE连接不稳定
解决方案:
nginx
location /sse {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Accel-Buffering no; # 禁用缓冲
proxy_cache off; # 禁用缓存
proxy_read_timeout 3600s; # 增加超时时间
proxy_send_timeout 3600s;
chunked_transfer_encoding on;
}
十、总结
SSE作为一种轻量级的服务器推送技术,在以下场景中具有明显优势:
✅ 适用场景:
- 实时消息通知
- 数据大屏实时更新
- 进度条推送
- 日志监控
- 股票/价格实时更新
- 新闻推送
❌ 不适用场景:
- 需要双向通信的场景(使用WebSocket)
- 需要传输二进制数据的场景(使用WebSocket)
- 需要低延迟高频交互的场景(使用WebSocket)
核心优势:
- 实现简单,基于标准HTTP
- 自动重连机制
- 服务器资源消耗低
- 浏览器原生支持
注意事项:
- 注意连接超时和心跳保持
- 生产环境需要考虑分布式支持
- 合理控制连接数和消息频率
- 做好错误处理和重连机制
SSE是实时通信领域的重要工具,掌握其原理和实现方式,能够帮助我们构建更加实时和高效的Web应用。