Spring Boot 整合 SSE(Server-Sent Events)

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 进行订阅。

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

成功的结果如图:

相关推荐
xuejianxinokok21 小时前
如何在 Rust 中以惯用方式使用全局变量
后端·rust
爬山算法21 小时前
Hibernate(26)什么是Hibernate的透明持久化?
java·后端·hibernate
彭于晏Yan21 小时前
Springboot实现数据脱敏
java·spring boot·后端
luming-021 天前
java报错解决:sun.net.utils不存
java·经验分享·bug·.net·intellij-idea
北海有初拥1 天前
Python基础语法万字详解
java·开发语言·python
alonewolf_991 天前
Spring IOC容器扩展点全景:深入探索与实践演练
java·后端·spring
super_lzb1 天前
springboot打war包时将外部配置文件打入到war包内
java·spring boot·后端·maven
毛小茛1 天前
芋道管理系统学习——项目结构
java·学习
天远云服1 天前
Go语言高并发实战:集成天远手机号码归属地核验API打造高性能风控中台
大数据·开发语言·后端·golang