SpringBoot 集成 SSE 实现服务端推送或可代替Websocket

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

最近做的业务项目总是遇到到长连接、服务端向前端推送消息的功能点。由于项目中已经集成了现成的WebSocket,也就是没有再重复造轮子。

但是如果单独搭建WebSocket可能会繁琐,能不能更加简单的SSE代替呢?我们一起来试试SSE这门技术。

02 简介

2.1 核心概念

SSE(Server-Sent Events)是一种基于 HTTP 协议的服务端推送技术。服务端通过持久的 HTTP 连接,单向向客户端推送数据,客户端通过 EventSource API 监听并处理这些事件。

主要特点:

  • 单向通信:仅服务端能发送消息
  • 消息仅支持文本数据
  • 协议是常规的HTTP 协议

为什么选择SSE

主要原因就是简单,通常我们遇到的场景都是单项推送:从服务器接收数据流。。这正是 EventSource 所擅长的。它还支持自动重新连接,此外,它是一个普通的旧的 HTTP,不是一个新协议。

官网地址:html.spec.whatwg.org/multipage/s...

2.2 SSE vs WebSocket 对比

维度 SSE WebSocket
通信方向 单向(服务端 → 客户端) 双向
底层协议 HTTP 独立的 WS 协议
断线重连 浏览器自动重连 需手动实现
数据格式 纯文本(text/event-stream) 任意二进制
复杂度 低,几行代码搞定 高,需要握手协议
代理/防火墙友好 好(本质就是 HTTP) 一般
连接数限制 浏览器对同域名 SSE 连接数有限制(通常 6 个) 无此限制
适用场景 通知推送、日志流、进度条、AI 流式输出 聊天室、实时游戏、协同编辑

选型建议 :如果你的场景只需要服务端向客户端单向推送,优先用 SSE,简单、稳定、省心。不要为了"听起来高级"就上 WebSocke

03 SpringBoot 集成 SSE

org.springframework.web.servlet.mvc.method.annotation.SseEmitterSpring MVC原生支持的方式,最常用。

是在Spring 4.2及以后就开始支持的。

3.1 核心控制类

java 复制代码
@Slf4j
@RestController
@RequestMapping("/sse")
public class SseController {

    private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    /**
     * @Description: 建立连接
     *
     * @Author: ws
     * @Date: 2026/9/17 11:38
     **/
    @RequestMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter connect(HttpServletResponse response) {
        response.setHeader("Access-Control-Allow-Origin", "*");
        log.info("建立连接。。。");
        SseEmitter sseEmitter = new SseEmitter(0L);
        try {
            sseEmitter.send(SseEmitter.event().data("连接成功"));

            emitters.add(sseEmitter);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sseEmitter;
    }

    /**
     * @Description: 推送测试
     *
     * @Author: ws
     * @Date: 2026/9/17 17:27
     **/
    @RequestMapping(value = "/push")
    public void push() throws Exception {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Thread.sleep(1000);
            emitters.forEach(emitter -> {
                try {
                    emitter.send(SseEmitter.event().data("推送数据" + index));
                } catch (Exception e) {
                    log.error("推送数据失败", e);
                }
            });
            log.info("推送数据" + i);
        }
    }
}

其中connect()用来建立连接,并将客户端保存到CopyOnWriteArrayList中,而push()用来模拟数据推送。

3.2 客户单订阅

javascript 复制代码
const eventSource = new EventSource('http://localhost:8080/sse/connect');

eventSource.addEventListener('message', (event) => {
    console.log('收到消息:', event.data);
});

eventSource.onerror = (event) => {
    console.log('连接出错,浏览器会自动重连...');
};

3.3 测试

分别通过谷歌浏览器和火狐浏览器接受服务端的消息。

推送消息:http://localhost:8080/sse/push

到这里简单的搭建就已经完成了,是不是很简单。

04 使用注意事项

上面完成简单的搭建功能,如果用在生产商可能还会有很多问题。

4.1 超时问题

SseEmitter 构造函数接收一个 Long 类型的超时时间(毫秒):

java 复制代码
new SseEmitter(0L)      // 永不超时(慎用)
new SseEmitter(60000L)  // 60秒超时
new SseEmitter()        // 使用默认超时(Spring 默认 30秒)

注意事项

  • 超时时间到了之后,Spring 会自动调用 complete(),连接会被关闭。
  • 如果你的推送场景是长时间保持的(如实时通知),要么设 0L 永不超时,要么自己实现心跳机制定期续命。
  • 推荐做法:设置一个合理的超时时间 + 客户端自动重连 + 服务端心跳保活。

永不超时固然好用,但是客户端堆积的问题如何解决?这是首要考虑的问题。

4.2 心跳保活机制

既然选择了永不超时,为什么还要使用保活机制呢?

这是因为长时间不发送数据,连接可能被中间代理、Nginx 或浏览器断开。这就需要我们定时不断给客户端发送心跳,用来保持连接。

下面是伪代码:

java 复制代码
@Scheduled(fixedRate = 30000) // 每30秒
public void sendHeartbeat() {
    emitters.forEach(emitter -> {
        try {
            emitter.send(SseEmitter.event().comment("heartbeat"));
        } catch (IOException e) {
            emitter.complete();
            emitters.remove(emitter);
        }
    });
}

4.3 连接数管理

浏览器对同一域名的 SSE 连接数有限制(HTTP/1.1 下通常为 6 个)。如果你的应用打开了多个标签页,连接数会很快耗尽。超过6个以上的标签页均无法建立连接。

解决方案

  • 使用 HTTP/2(连接复用,不受 6 个限制)
  • 使用共享 Worker(SharedWorker)让多个标签页共享一个 SSE 连接
  • 后端用频道/分组管理,一个连接推送多个主题的数据

05 易错点

5.1 忘记注册事件

案例就是没有使用注册事件:onCompletion onTimeout onError

当然案例使用的永不超时onTimeout就没有了意义。

会造成什么后果:客户端断开后,emitter 还留在列表里,每次推送都报 IOException,日志刷屏,内存泄漏。

推荐做法:

java 复制代码
SseEmitter emitter = new SseEmitter(timeout);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(e -> emitters.remove(emitter));
emitters.add(emitter);

5.2 emitter 管理

如果在异步线程中发送数据时 emitter 已完成,就会出现这样的异常:

IllegalStateException: ResponseBodyEmitter is already set complete

主要因为,在另一个线程中调用 emitter.send() 时,连接已经被超时或客户端断开关闭了。

推荐做法:

java 复制代码
try {
    emitter.send(SseEmitter.event().data("hello"));
} catch (IOException | IllegalStateException e) {
    emitter.complete();
    emitters.remove(emitter);
    log.warn("推送失败,已移除 emitter: {}", e.getMessage());
}

5.3 produces 类型

produces 类型没设置或设错,会导致客户端收到数据但 EventSource 不触发事件,或者浏览器直接拒绝连接。并不是所有的浏览器都会出现这样的问题。这个可以说是针对不同版本的浏览器的兼容性处理。

推荐做法:

java 复制代码
// ✅ 必须设置 produces
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() { ... }

如果不设置 produces = "text/event-stream",Spring 可能返回 application/json,浏览器 EventSource 会直接报错。

5.4 跨域问题

前后端分离项目,前端控制台报 CORS 错误。自己在做测试的时候也遇到了,所以案例使用了 response.setHeader("Access-Control-Allow-Origin", "*");来解决跨域。

解决跨域的方法有很对可以@CrossOrigin注解、全局配置 CORS或者其他方式,只要解决跨域问题即可。

06 小结

除了上面列举的问题,还有其他很多细节,如鉴权、集群等场景。既然小细节那么多,就没有直接造好的轮子,直接使用么?

找了一圈下来,没有找到。既然找不到,那就自己造一个。后面专门建一个项目将SSE作为一个独立的推送服务,再也不用去考虑那些零碎的小细节。

相关推荐
字节探索1 小时前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
不正经的码狗1 小时前
JAVA 、Eclipse 安装及配置 JAVA 项目(无需配置环境)
java·eclipse
挖掘狂人1 小时前
用 AI 写代码别只甩一句指令,这套工作流救过我的项目
程序员·ai编程·codebuddy
xiaoqiMikko1 小时前
你升的是 jackson-databind,中招的是 jackson-core:一个超长 JSON key 就能吃掉 200MB 堆
java·安全
不正经的码狗1 小时前
Eclipse汉化(快速,推荐)
java·eclipse
极客互动API1 小时前
极客互动-企业微信基于外部API接口实现AI客服自动接管外部联系人消息收发
java·微信·企业微信·ai编程·rpa
Sam_Deep_Thinking1 小时前
关于java final关键字的可见性
java·后端·面试·程序员
tryxr1 小时前
矩阵的几种基础变换
java·数据结构·算法·矩阵
写后端的胖头鱼1 小时前
如何区分IO/CPU 密集型,线程池核心参数怎么设置
java·线程池·多线程·io密集·cpu密集