⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/nihaizaishiyong-WebSocket-shixianshishixiaoxituisongma/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

又有人在群里问”实时消息推送用啥”

需求群里今天又有人发:「老板要做一个数据大屏实时刷新,用 WebSocket 是不是合适?」

我看了一眼问的同学——做的是 toB 后台的数据看板,单向推数据,前端不需要回话。给的回答是:「别上 WebSocket,SSE 就够了。」

群里很多人愣住了:「啥是 SSE?」

国内 Java 圈最近半年讨论 ChatGPT / DeepSeek 这种流式响应的越来越多,几乎所有大模型流式接口走的都是 SSE 不是 WebSocket——但生态里大家提到「实时推送」第一反应还是 WebSocket。

这篇就把这件事讲清楚:90% 的单向推送场景,SSE 是更轻、更稳、更便宜的选择——什么时候用、什么时候不该用、国内反代环境有什么坑、Demo 怎么跑。

三种方案的真实对比

服务端推数据给客户端,主流就三种方案:轮询、WebSocket、SSE。

维度 轮询 WebSocket SSE
协议 HTTP(多次请求) ws/wss(独立协议) HTTP(长连接)
通信方向 客户端 → 服务端(伪推送) 双向 服务端 → 客户端(单向)
浏览器并发限制 占用一个连接名额 不占(新协议) 占用一个连接名额
服务端实现复杂度 低 高(需要 ws 库) 中(标准 HTTP)
自动断线重连 不需要 需要自己写 原生支持
自定义事件类型 不需要 需要自己设计 原生支持
跨防火墙 / 反代 完美 需要专门配置 完美(就是 HTTP)
Token 鉴权 HTTP Header 直接传 握手时传,复杂 Cookie 透传 / 短票据 query(原生 EventSource 不支持自定义 Header)
移动端兼容 通用 主流支持 小程序不支持
IE 兼容 支持 部分支持 不支持

关键洞察:SSE 不是 WebSocket 的「弱化版」——它在协议层、断线重连、事件类型上反而比 WebSocket 更适合「单向推送」这个场景。

WebSocket 的优势是双向通信——这点 SSE 替代不了。但「真的需要双向」的场景比想象中少。

浏览器兼容性已经不是选型依据——Chrome / Firefox / Safari / Edge 近 5 年的版本对 WebSocket 和 SSE 都原生支持,真正还有差异的只剩两处:

  • IE:SSE 不支持,WebSocket 部分支持——但 IE 已于 2022 年 6 月被微软停止支持,2026 年还要兼容的几乎只剩政企内网项目;
  • 小程序:微信 / 支付宝小程序原生不支持 SSE,但支持 WebSocket(wx.connectSocket)——所以 to C 跑小程序的场景默认走 WebSocket。

主流 Web 场景下,兼容性都不是问题——真正的差距在协议设计哲学。

决策矩阵:什么场景用什么

按两个维度判断:

服务端单向推(无需客户端反馈) 客户端服务端双向(互动场景)
低频更新(秒级) SSE ✅✅ WebSocket ✅
高频互动(毫秒级) SSE ⚠️(够用但临界) WebSocket ✅✅
极端场景(万级 QPS 双向) 不适合 WebSocket / 自研 TCP ✅

直接结论:

  • 大屏实时数据 / 通知中心未读数 / 大模型流式回答 / 监控告警推送——SSE 甜区;
  • 聊天 / 在线协作 / 直播弹幕 / 多人游戏——WebSocket 甜区;
  • 轮询——只在「极个别浏览器既不支持 WebSocket 也不支持 SSE」时兜底,生产里几乎不该用。

国内业务里 SSE 真正甜的几个场景:

场景 为什么 SSE 比 WebSocket 好
大模型流式响应(ChatGPT 风格) 协议简单 + 断线重连 + token 用 HTTP Header 透传
后台数据大屏 部署门槛低(HTTP 即可),不用专门开 ws 端口
消息中心未读数推送 单向 + 无状态,SSE 拿来即用
监控告警 / 日志流 长连接 + 自定义事件类型

WebSocket 真正甜的几个场景:

场景 为什么 WebSocket 不可替代
IM 聊天 客户端要发消息、服务端要推消息,双向
多人协作(在线文档) 编辑事件双向广播
直播弹幕 / 投票 高频双向

说人话:「需要服务端单方面持续往下推 + 不需要客户端立即响应」默认走 SSE,反过来才用 WebSocket。

SSE 在国内反代环境的隐藏坑

看似 SSE 是 HTTP 长连接,应该哪都能跑——国内的部署环境不一样。

坑一:Nginx 默认开 buffer,SSE 会被攒包

Nginx 默认对 HTTP 响应做 buffer——SSE 流式数据会被攒到 buffer 满了或者超时才一次性吐给客户端,实时性彻底丢失。

修复:

location /sse/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # ← 关键:关 buffer
proxy_cache off; # ← 关 cache
proxy_read_timeout 24h; # ← 长连接超时延长
chunked_transfer_encoding on;
}

坑二:运营商劫持 / 移动网络下断流

国内移动 / 联通宽带在长连接 30-60 秒无数据时会主动断开——SSE 默认实现下,长时间无消息会被当成”死连接”。

修复:服务端定期发心跳(注释行就够了,不影响业务事件):

: heartbeat\n\n

冒号开头的是注释,浏览器原生忽略,但能保活 TCP 连接。

坑三:浏览器 6 个连接限制(同域名)

Chrome 同域名同时只允许 6 个连接,SSE 会占一个。如果用户同时打开多个页面,第七个页面的请求会被阻塞。

修复:后端服务用独立域名(比如 sse.example.com),或者上 HTTP/2(HTTP/2 不受 6 连接限制)。

坑四:CDN / WAF 不一定支持长连接

国内的 CDN(阿里云、腾讯云、又拍云)默认对长连接有超时限制,且 SSE 通常不在 CDN 加速名单里。WAF 也可能因为「长连接 + 持续数据」误判攻击。

修复:SSE 接口不要走 CDN,直连业务网关;WAF 加白名单。

坑五:小程序不支持 SSE

国内 to C 业务很多走小程序——微信 / 支付宝小程序的 wx.request 不支持 SSE,只能轮询或 WebSocket(小程序对 WebSocket 支持)。

修复:小程序场景默认 WebSocket,Web 场景 SSE。

实操 Demo:前端 + 后端 SSE 流

最小可跑的 Demo——复制粘贴就能用。

后端:Spring Boot 实现

@RestController
public class SseController {

private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);

@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
SseEmitter emitter = new SseEmitter(0L); // 不超时,走心跳保活

// 业务消息推送
ScheduledFuture<?> messageTask = scheduler.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(Map.of("time", LocalDateTime.now().toString())));
} catch (Exception e) {
emitter.completeWithError(e);
}
}, 0, 1, TimeUnit.SECONDS);

// 心跳保活(防止运营商断流)
ScheduledFuture<?> heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event().comment("heartbeat"));
} catch (Exception e) {
emitter.completeWithError(e);
}
}, 30, 30, TimeUnit.SECONDS);

// 客户端断开 / 超时 / 异常时,必须取消两个定时任务,否则会持续 send 泄漏线程池
Runnable cleanup = () -> {
messageTask.cancel(false);
heartbeatTask.cancel(false);
};
emitter.onCompletion(() -> { cleanup.run(); log.info("SSE 连接关闭"); });
emitter.onTimeout(() -> { cleanup.run(); log.info("SSE 连接超时"); });
emitter.onError(e -> { cleanup.run(); log.warn("SSE 连接异常", e); });

return emitter;
}
}

关键点:

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE 让 Spring 输出 text/event-stream Content-Type;
  • SseEmitter(0L) 表示永不超时——业务侧用心跳维护连接活跃;
  • 真生产里不要用单 ScheduledExecutorService 推所有连接,连接多了会卡——按用户分组或上 Reactor。

前端:原生 EventSource

<script>
if (!window.EventSource) {
throw new Error('当前浏览器不支持 SSE');
}

const source = new EventSource('/sse');

source.onopen = () => console.log('连接已建立');

source.addEventListener('message', e => {
const data = JSON.parse(e.data);
console.log('收到消息', data);
// 业务处理
});

source.onerror = err => {
console.error('连接异常', err);
// 默认会自动重连,无需手动处理
};
</script>

SSE 自带断线重连——网络抖动 / 服务端重启后,浏览器会自动重新发起 GET /sse。WebSocket 这件事要自己写心跳 + 重连逻辑。

Demo 跑起来的样子

跑起来后,浏览器控制台里每秒会打一条 收到消息 {time: ...}——断网把服务停了再启,几秒后自动重连,无需手动处理。这就是 SSE 的「便宜」:前后端各 30 行,断线重连白送。

上线前必踩的几件事

按踩到概率从高到低排:

坑一:连接管理与回收(最常见)

每个 SSE 连接占用一个 HTTP 长连接 + 一个服务端线程。1 万个并发用户就是 1 万个长连接——直接把 Tomcat 默认线程池(200)打挂。这是接 SSE 第一个上量后必踩的。

做法:

  • Spring Boot 默认 Tomcat 线程数改大或用 NIO(Tomcat 默认就是 NIO);
  • 推荐用 Spring WebFlux + Reactive 替代传统 Servlet,单机能撑万级长连接;
  • 客户端关闭浏览器要回收 emitter,注册 onCompletion / onTimeout 钩子。

坑二:浏览器 EventSource 不能自定义 Header(最常见)

new EventSource(url) 不支持自定义 Header——SSE 没法用 Bearer Token 鉴权(除非走 cookie)。鉴权场景几乎必踩。

做法:

  • token 放 URL Query 参数(/sse?token=xxx)——简单但日志会打到 token;
  • 用 cookie + CSRF 防御——更安全但跨域配置复杂;
  • 用第三方库(比如 event-source-polyfill)支持 Header。

token 刷新也要顺手处理:SSE 是单向流,token 过期后客户端只能 source.close() + 用新 token 重新创建 EventSource。

坑三:横向扩展时的会话保持(常见)

多实例部署时,SSE 连接会落到不同实例——A 实例发的消息,B 实例上的连接收不到。

做法:

  • 走 Redis Pub/Sub 或 RocketMQ 广播——所有实例订阅,收到消息再推给自己持有的连接;
  • 或者用「消息中心服务」,业务服务把消息丢给消息中心,消息中心负责跟客户端的长连接。

坑四:日志量爆炸(少见但量级敏感)

SSE 每秒推一次的话,1 万用户就是 1 万次 emit/秒——对应日志、指标、链路追踪都会爆炸。只有量上来才会遇到,但量上来时是炸 ELK 级别。

做法:业务日志用 INFO 级别打入采样后的子集;trace 不要给每个 emit 都打;监控按连接数 + 累计推送数采集,不要按每条事件采集。

一句话收口

SSE 不是 WebSocket 的「弱化版」,是专门为「服务端单向推送」设计的更合适工具——协议简单、复用 HTTP 生态、自带断线重连和事件类型、token 透传走 Header。

WebSocket 该用还得用——双向交互场景没替代品;但 90% 的场景其实是单向——大屏数据、通知未读、大模型流式、监控告警。这些场景你硬上 WebSocket,就是用一辆 SUV 拉一袋米。

工程取舍很简单——默认 SSE,需要双向才升 WebSocket——这跟「能用单表就别 join」是一样的工程纪律。

选工具的本质,是选「最合适的复杂度」——能用更简单的方案达成同样目标,复杂度就是 bug 的温床。

文章目录
  1. 1. 又有人在群里问”实时消息推送用啥”
  2. 2. 三种方案的真实对比
  3. 3. 决策矩阵:什么场景用什么
  4. 4. SSE 在国内反代环境的隐藏坑
    1. 4.0.1. 坑一:Nginx 默认开 buffer,SSE 会被攒包
    2. 4.0.2. 坑二:运营商劫持 / 移动网络下断流
    3. 4.0.3. 坑三:浏览器 6 个连接限制(同域名)
    4. 4.0.4. 坑四:CDN / WAF 不一定支持长连接
    5. 4.0.5. 坑五:小程序不支持 SSE
  • 5. 实操 Demo:前端 + 后端 SSE 流
    1. 5.0.1. 后端:Spring Boot 实现
    2. 5.0.2. 前端:原生 EventSource
    3. 5.0.3. Demo 跑起来的样子
  • 6. 上线前必踩的几件事
    1. 6.0.1. 坑一:连接管理与回收(最常见)
    2. 6.0.2. 坑二:浏览器 EventSource 不能自定义 Header(最常见)
    3. 6.0.3. 坑三:横向扩展时的会话保持(常见)
    4. 6.0.4. 坑四:日志量爆炸(少见但量级敏感)
  • 7. 一句话收口