1. 项目概述
Server-Sent Events (SSE) 是一种基于HTTP的轻量级实时通信技术,它允许服务器主动向客户端推送数据。与WebSocket不同,SSE是单向通信(服务器→客户端),特别适合需要服务器推送但客户端无需频繁发送数据的场景。
我在实际项目中多次使用SSE实现诸如实时通知、数据监控、进度更新等功能。相比轮询方案,SSE能显著减少网络开销;而相比WebSocket,它实现更简单且天然支持断线重连。Spring Boot对SSE提供了开箱即用的支持,只需几行代码就能搭建起完整的推送服务。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理与架构设计
2.1 SSE协议工作原理
SSE建立在HTTP协议之上,通过以下机制实现实时推送:
- 长连接机制:客户端发起请求后,服务器保持连接开放(Keep-Alive)
- 事件流格式:服务器返回
Content-Type: text/event-stream的响应 - 消息格式:每条消息以
data:开头,以\n\n结束,例如:code复制event: message data: {"time":"2023-05-01","value":42} id: 12345
重要提示:SSE默认支持断线重连,客户端会自动尝试重新连接。这是通过消息中的
retry:字段控制的。
2.2 Spring Boot中的实现架构
在Spring Boot中,SSE的核心组件是SseEmitter,其工作流程如下:
- 客户端连接:浏览器通过EventSource API发起SSE连接请求
- 服务端处理:
- 创建SseEmitter实例
- 将emitter存入内存缓存(通常用ConcurrentHashMap)
- 返回emitter给Spring MVC框架
- 消息推送:业务逻辑通过缓存的emitter发送消息
- 连接管理:处理超时、错误和主动关闭事件
3. 完整实现步骤
3.1 环境准备
虽然Spring Web已经包含SSE支持,但我推荐添加以下依赖以获得更好的开发体验:
xml复制<!-- Lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.12</version>
</dependency>
3.2 核心服务层实现
java复制@Slf4j
@Service
public class SseService {
// 使用线程安全的Map存储连接
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
// 默认心跳间隔(毫秒)
private static final long HEARTBEAT_INTERVAL = 30_000L;
/**
* 创建SSE连接
* @param clientId 客户端唯一标识
* @return SseEmitter实例
*/
public SseEmitter createConnection(String clientId) {
// 设置1小时超时(0表示永不超时)
SseEmitter emitter = new SseEmitter(3600_000L);
// 注册回调函数
emitter.onCompletion(() -> {
log.info("[{}]连接正常完成", clientId);
EMITTER_MAP.remove(clientId);
});
emitter.onTimeout(() -> {
log.warn("[{}]连接超时", clientId);
EMITTER_MAP.remove(clientId);
});
emitter.onError(e -> {
log.error("[{}]连接异常: {}", clientId, e.getMessage());
EMITTER_MAP.remove(clientId);
});
// 发送初始心跳
try {
emitter.send(SseEmitter.event()
.id("init")
.name("INIT")
.data("CONNECTED")
.reconnectTime(5000));
} catch (IOEx
