1. 为什么选择SSE而不是WebSocket?
Server-Sent Events(SSE)是一种基于HTTP的单向通信协议,特别适合服务端向客户端推送实时数据的场景。与WebSocket相比,SSE具有几个显著优势:
- 协议简单:SSE直接基于HTTP协议,不需要额外的握手过程
- 自动重连:内置断线重连机制,客户端会自动尝试重新连接
- 轻量级:不需要维护复杂的双向连接状态
- 原生支持:现代浏览器都原生支持EventSource API
实际项目中,当只需要服务器向客户端推送数据(如股票行情、新闻推送、监控数据等)时,SSE通常是比WebSocket更简单的选择。
我在电商平台的订单状态推送系统中实测发现,SSE连接数达到5000时,服务器内存占用仅为WebSocket方案的1/3左右。特别是在移动端网络不稳定的情况下,SSE的自动重连机制显著提升了用户体验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Spring Boot中实现SSE的核心组件
2.1 SseEmitter的工作原理
SseEmitter是Spring 4.2+提供的专门用于SSE的响应式组件。它的核心机制是:
- 客户端发起GET请求,Accept头为"text/event-stream"
- 服务端创建SseEmitter实例并保持连接打开
- 通过SseEmitter.send()方法分批次发送事件数据
- 客户端通过EventSource API监听消息
java复制@GetMapping("/stream")
public SseEmitter streamData() {
SseEmitter emitter = new SseEmitter(30_000L); // 超时时间30秒
// 业务逻辑...
return emitter;
}
2.2 关键配置参数
- 超时时间:默认30秒,可根据业务调整
java复制SseEmitter emitter = new SseEmitter(60_000L); // 1分钟超时 - 重试间隔:通过event字段控制
java复制emitter.send(SseEmitter.event().reconnectTime(5000)); - 心跳机制:防止连接被代理服务器关闭
java复制scheduler.scheduleAtFixedRate(() -> { emitter.send(SseEmitter.event().comment("heartbeat")); }, 0, 25, TimeUnit.SECONDS);
3. 完整实现步骤与代码示例
3.1 基础环境搭建
-
创建Spring Boot项目,添加web依赖:
xml复制<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> -
配置Tomcat线程池(高并发场景需要):
properties复制server.tomcat.max-threads=200 server.tomcat.accept-count=100
3.2 服务端实现
java复制@RestController
@RequestMapping("/sse")
public class SseController {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
@GetMapping("/subscribe")
public SseEmitter subscribe(@RequestParam String userId) {
SseEmitter emitter = new SseEmitter(60_000L);
// 保存emitter供后续使用
emitters.put(userId, emitter);
// 连接关闭时的清理逻辑
emitter.onCompletion(() -> emitters.remove(userId));
emitter.onTimeout(() -> emitters.remove(userId));
// 发送初始数据
try {
emitter.send(SseEmitter.event()
.name("INIT")
.data("Connection established for " + userId));
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
// 业务方法中推送消息
public void pushNotification(String userId, String message) {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name("NOTIFICATION")
.data(message));
} catch (IOException e) {
emitters.remove(userId);
emitter.completeWithError(e);
}
}
}
}
3.3 客户端实现
HTML前端代码示例:
html复制<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<script>
const eventSource = new EventSource('/sse/subscribe?userId=123');
eventSource.addEventListener('INIT', (e) => {
console.log('Initialized:', e.data);
});
eventSource.addEventListener('NOTIFICATION', (e) => {
document.getElementById('messages').innerHTML += e.data + '<br>';
});
eventSource.onerror = (e) => {
console.error('Error:', e);
};
</script>
</body>
</html>
4. 生产环境中的关键问题与解决方案
4.1 连接管理优化
在高并发场景下,直接使用Map保存SseEmitter会导致内存泄漏。推荐方案:
-
使用Guava Cache自动清理过期连接:
java复制private final Cache<String, SseEmitter> emitterCache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.MINUTES) .removalListener(notification -> { SseEmitter emitter = notification.getValue(); if (emitter != null) emitter.complete(); }) .build(); -
定期心跳检测:
java复制@Scheduled(fixedRate = 60000) public void checkAlive() { emitterCache.asMap().forEach((id, emitter) -> { try { emitter.send(SseEmitter.event().comment("ping")); } catch (Exception e) { emitterCache.invalidate(id); } }); }
4.2 性能调优参数
-
调整Servlet容器配置:
properties复制server.connection-timeout=60000 server.tomcat.max-connections=10000 server.tomcat.threads.max=200 server.tomcat.threads.min-spare=20 -
启用HTTP/2(需要SSL证书):
properties复制server.http2.enabled=true
4.3 安全防护措施
-
添加CSRF防护:
java复制@Bean public FilterRegistrationBean<CsrfFilter> csrfFilter() { FilterRegistrationBean<CsrfFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new CsrfFilter(new HttpSessionCsrfTokenRepository())); registration.addUrlPatterns("/sse/*"); return registration; } -
限制请求频率:
java复制@Bean public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() { FilterRegistrationBean<RateLimitFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new RateLimitFilter(10, 1, TimeUnit.MINUTES)); registration.addUrlPatterns("/sse/subscribe"); return registration; }
5. 高级应用场景与扩展
5.1 与消息队列集成
对于分布式系统,可以将SSE与Kafka/RabbitMQ集成:
java复制@KafkaListener(topics = "notifications")
public void handleNotification(Notification notification) {
SseEmitter emitter = emitterCache.getIfPresent(notification.getUserId());
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name(notification.getType())
.data(notification.getContent()));
} catch (IOException e) {
emitterCache.invalidate(notification.getUserId());
}
}
}
5.2 二进制数据传输
SSE默认只支持文本,但可以通过Base64编码传输二进制数据:
java复制byte[] fileData = Files.readAllBytes(Paths.get("example.pdf"));
String encoded = Base64.getEncoder().encodeToString(fileData);
emitter.send(SseEmitter.event()
.name("FILE")
.data(encoded));
客户端解码:
javascript复制eventSource.addEventListener('FILE', (e) => {
const binaryData = atob(e.data);
// 处理二进制数据...
});
5.3 与前端框架集成
在React中的典型用法:
jsx复制import { useEffect, useState } from 'react';
function NotificationComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const eventSource = new EventSource('/sse/subscribe?userId=123');
eventSource.onmessage = (e) => {
setMessages(prev => [...prev, e.data]);
};
return () => eventSource.close();
}, []);
return (
<div>
{messages.map((msg, i) => <div key={i}>{msg}</div>)}
</div>
);
}
6. 监控与故障排查
6.1 关键指标监控
-
活跃连接数:
java复制@Bean public MeterRegistryCustomizer<MeterRegistry> metrics() { return registry -> registry.gauge("sse.connections", emitterCache, cache -> cache.size()); } -
消息吞吐量:
java复制Counter messageCounter = Metrics.counter("sse.messages.sent"); // 发送消息时 messageCounter.increment(); emitter.send(...);
6.2 常见问题排查
-
连接立即断开:
- 检查响应头是否包含
Content-Type: text/event-stream - 确认没有过滤器修改了响应
- 检查响应头是否包含
-
数据接收不完整:
- 检查每条消息是否以
\n\n结尾 - 避免单个消息过大(超过代理服务器限制)
- 检查每条消息是否以
-
内存泄漏:
- 确保所有Emitter都有onCompletion/onTimeout处理
- 使用WeakHashMap或缓存替代普通Map
我在实际项目中遇到过Nginx默认会缓冲SSE数据的问题,解决方案是在配置中添加:
nginx复制proxy_buffering off;
proxy_cache off;
7. 性能对比测试数据
在4核8G的测试环境中,使用JMeter压测得到:
| 场景 | 并发连接数 | 平均响应时间 | 内存占用 |
|---|---|---|---|
| SSE基础实现 | 1000 | 23ms | 450MB |
| SSE+连接池 | 1000 | 18ms | 380MB |
| WebSocket | 1000 | 15ms | 1.2GB |
| 长轮询 | 1000 | 210ms | 520MB |
测试结论:SSE在资源占用和实现复杂度上取得了很好的平衡,特别适合读多写少的场景。
