1、简述
1.1 SSE是什么?
SSE (Server-Sent Events,服务器推送事件) 是一种基于HTTP标准、让服务器能主动向客户端推送实时数据的技术。它在需要服务器单向持续更新的场景中非常实用。
1.2 SSE 特点
| 特点维度 | 具体说明 | 优势 |
|---|---|---|
| 单向通信 | 仅支持服务器向客户端推送数据。 | 模型简单,适合通知、广播等场景。 |
| 基于HTTP | 使用普通HTTP/HTTPS协议,通过长连接实现。 | 无需特殊协议或端口,易于部署、调试,且能利用现有HTTP设施(如身份验证、CORS) |
| 内置重连 | 连接断开后,客户端会自动尝试重新连接。 | 大幅提升连接健壮性,无需开发者手动实现重连逻辑。 |
| 事件ID与断点续传 | 服务器可为每条消息附带ID。重连时,客户端会通过Last-Event-ID头部自动发送最后收到的ID。 | 服务器可据此恢复上下文或发送遗漏消息,实现数据连续性。 |
| 轻量数据格式 | 传输格式为文本(常为JSON),规范简单。 | 易于生成和解析。 |
| 简单API | 浏览器端使用标准的 EventSource API。 | 几行代码即可实现连接、监听和事件处理,学习成本低。 |
2、SpringBoot 整合SSE
2.1 引入依赖
SSE无需额外的依赖,Spring Boot自带对SSE的支持。
bash
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.2 SSE 客户端管理器(核心)
该类主要包含:
- 维护所有 SSE 连接
- 支持分组推送、单点推送、广播
- 支持客户端主动 / 被动断开
- 支持查询所有订阅信息(运维 / 管理)
具体实现如下(大家可根据自己的需求添加日志):
java
@Slf4j
public class SseClientManager {
/**
* 数据结构:
* groupId -> (clientId -> SseEmitter)
*/
private final Map<String, Map<String, SseEmitter>> groupClients = new ConcurrentHashMap<>();
/* ===== 订阅 ===== */
/**
* 客户端订阅 SSE
*/
public SseEmitter subscribe(String groupId, String clientId) {
// 创建 SSE 连接(0 表示永不超时)
SseEmitter emitter = new SseEmitter(0L);
// 获取或创建分组
groupClients
.computeIfAbsent(groupId, k -> new ConcurrentHashMap<>())
.put(clientId, emitter);
log.info("用户 groupId:{}, clientId:{} 连接成功。", groupId, clientId);
// 连接完成回调(浏览器关闭 / 网络断开)
emitter.onCompletion(() -> removeClient(groupId, clientId));
// 超时回调
emitter.onTimeout(() -> removeClient(groupId, clientId));
// 异常回调
emitter.onError(e -> removeClient(groupId, clientId));
return emitter;
}
/**
* 主动断开客户端(前端调用)
*/
public Boolean disconnect(String groupId, String clientId) {
Map<String, SseEmitter> clients = groupClients.get(groupId);
if (clients == null) {
return false;
}
// 分组为空则移除
if (clients.isEmpty()) {
groupClients.remove(groupId);
}
SseEmitter emitter = clients.remove(clientId);
if (emitter != null) {
emitter.complete();
log.info("用户 groupId:{}, clientId:{} 断开成功。", groupId, clientId);
return true;
}
return false;
}
/**
* 移除客户端(内部使用)
*/
private void removeClient(String groupId, String clientId) {
disconnect(groupId, clientId);
}
/* ===== 推送 ===== */
/**
* 单客户端推送
*/
public Boolean sendToClient(String groupId, String clientId, String eventName, String data) {
Map<String, SseEmitter> clients = groupClients.get(groupId);
if (clients == null) {
return false;
}
SseEmitter emitter = clients.get(clientId);
if (emitter == null) {
return false;
}
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data));
log.info("groupId:{}, clientId:{}, 信息:{}, 单独推送成功。", groupId, clientId, data);
return true;
} catch (IOException e) {
// 推送失败,移除客户端
removeClient(groupId, clientId);
}
return false;
}
/**
* 分组广播
*/
public Boolean broadcastToGroup(String groupId, String eventName, String data) {
Map<String, SseEmitter> clients = groupClients.get(groupId);
if (clients == null) {
return false;
}
clients.forEach((clientId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data));
log.info("groupId:{}, clientId:{}, 事件名称:{}, 信息:{}, 分组推送成功。", groupId, clientId, eventName, data);
} catch (IOException e) {
removeClient(groupId, clientId);
}
});
return true;
}
/**
* 全局广播
*/
public Boolean broadcastAll(String eventName, String data) {
for (Map.Entry<String, Map<String, SseEmitter>> entry : groupClients.entrySet()) {
String groupId = entry.getKey();
broadcastToGroup(groupId, eventName, data);
log.info("groupId:{}, 事件名称:{}, 信息:{}, 分组推送成功。", groupId, eventName, data);
}
return true;
}
/* ===== 查询 ===== */
/**
* 获取所有分组 ID
*/
public Set<String> getAllGroups() {
return Collections.unmodifiableSet(groupClients.keySet());
}
/**
* 获取某个分组下的客户端 ID
*/
public Set<String> getClientsByGroup(String groupId) {
Map<String, SseEmitter> clients = groupClients.get(groupId);
if (clients == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(clients.keySet());
}
/**
* 获取所有订阅信息
* 返回结构:
* groupId -> [clientId1, clientId2]
*/
public Map<String, Set<String>> getAllSubscriptions() {
return groupClients.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Collections.unmodifiableSet(e.getValue().keySet())
));
}
/**
* 当前在线客户端总数
*/
public int getTotalClientCount() {
return groupClients.values()
.stream()
.mapToInt(Map::size)
.sum();
}
2.3 对外服务类
SSE 对外服务层,用于业务系统调用,避免直接操作底层 Manager。
具体实现如下:
java
@Service
public class SseService {
private final SseClientManager clientManager = new SseClientManager();
/* ===== 订阅 ===== */
/**
* 客户端订阅
*/
public SseEmitter subscribe(String groupId, String clientId) {
return clientManager.subscribe(groupId, clientId);
}
/**
* 主动断开客户端
*/
public Boolean disconnect(String groupId, String clientId) {
return clientManager.disconnect(groupId, clientId);
}
/* ===== 推送 ===== */
/**
* 单客户端推送
*/
public Boolean sendToClient(String groupId, String clientId, String eventName, String data) {
return clientManager.sendToClient(groupId, clientId, eventName, data);
}
/**
* 分组广播
*/
public Boolean broadcastToGroup(String groupId, String eventName, String data) {
return clientManager.broadcastToGroup(groupId, eventName, data);
}
/**
* 全局广播
*/
public Boolean broadcastAll(String eventName, String data) {
return clientManager.broadcastAll(eventName, data);
}
/* ===== 查询接口 ===== */
/**
* 获取所有分组
*/
public Set<String> getAllGroups() {
return clientManager.getAllGroups();
}
/**
* 获取特定组的信息
*/
public Set<String> getClientsByGroup(String groupId) {
return clientManager.getClientsByGroup(groupId);
}
/**
* 获取所有的连接信息
*/
public Map<String, Set<String>> getAllSubscriptions() {
return clientManager.getAllSubscriptions();
}
/**
* 获取所有连接总数
*/
public int getOnlineCount() {
return clientManager.getTotalClientCount();
}
}
2.4 Controller 对外接口
bash
@Api(tags = "sse API")
@RestController
@RequestMapping("/sse")
public class SseController {
private final SseService sseService;
public SseController(SseService sseService) {
this.sseService = sseService;
}
/* ===== 订阅 ===== */
@ApiOperation("客户端订阅")
@GetMapping("/subscribe")
public SseEmitter subscribe(@Parameter(name = "groupId", description = "分组ID", required = true, example = "group001") String groupId,
@Parameter(name = "clientId", description = "客户端ID", required = true, example = "client001") String clientId) {
return sseService.subscribe(groupId, clientId);
}
@ApiOperation("主动断开客户端")
@DeleteMapping("/disconnect")
public AjaxResult disconnect(@Parameter(name = "groupId", description = "分组ID", required = true, example = "group001") String groupId,
@Parameter(name = "clientId", description = "客户端ID", required = true, example = "client001") String clientId) {
return success(sseService.disconnect(groupId, clientId));
}
/* ===== 推送 ===== */
@ApiOperation("单客户端推送")
@PostMapping("/send/one")
public AjaxResult send(@Parameter(name = "groupId", description = "分组ID", required = true, example = "group001") String groupId,
@Parameter(name = "clientId", description = "客户端ID", required = true, example = "client001") String clientId,
@Parameter(name = "data", description = "信息", required = true, example = "这是发送的消息。") String data) {
return success(sseService.sendToClient(groupId, clientId, "message", data));
}
@ApiOperation("分组广播")
@PostMapping("/broadcast/group")
public AjaxResult broadcastGroup(@Parameter(name = "groupId", description = "分组ID", required = true, example = "group001") String groupId,
@Parameter(name = "data", description = "信息", required = true, example = "这是发送的消息。") String data) {
return success(sseService.broadcastToGroup(groupId, "message", data));
}
@ApiOperation("全局广播")
@PostMapping("/broadcast/all")
public AjaxResult broadcastAll(@Parameter(name = "data", description = "信息", required = true, example = "这是发送的消息。") String data) {
return success(sseService.broadcastAll("message", data));
}
/* ===== 查询 ===== */
@ApiOperation("获取所有分组")
@GetMapping("/groups/get_all")
public AjaxResult getAllGroups() {
return success(sseService.getAllGroups());
}
@ApiOperation("获取特定组的信息")
@GetMapping("/groups/get_one")
public AjaxResult getClients(@Parameter(name = "groupId", description = "分组ID", required = true, example = "group001") String groupId) {
return success(sseService.getClientsByGroup(groupId));
}
@ApiOperation("获取所有的连接信息")
@GetMapping("/subscriptions")
public AjaxResult getAllSubscriptions() {
return success(sseService.getAllSubscriptions());
}
@ApiOperation("获取所有连接总数")
@GetMapping("/onlineCount")
public AjaxResult onlineCount() {
return success(sseService.getOnlineCount());
}
}
3、测试
我这里使用的是 ApiPost 软件进行测试,记住新建 Event Stream Request 进行订阅。

新建普通接口进行信息推送:

成功的结果如图:
