SSE协议

SSE(Server-Sent Events,服务端推送事件)基于 HTTP 协议,为了解决服务器无法主动推送信息的问题。通过向客户端声明接下来要发送的是流信息(streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

特点:

  • 单向通信:服务器向客户端推送数据。

  • 实现简单:基于 HTTP 协议,大部分浏览器都支持,只需设置正确的 HTTP 头和响应格式,轻量级。

  • 自动重连:浏览器会自动处理重连机制。不允许缓存

  • 数据格式:SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。

协议实现

SSE 协议很简单,本质上是一个客户端发起的 HTTP Get 请求,服务器在接到该请求后,返回 200 OK 状态,同时附带以下 Headers

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

服务器数据格式: 每个事件以\n\n结尾,data字段包含有效负载,可选id(事件ID)、event(自定义事件类型)等字段。通过定时Flush推送数据,避免缓冲区阻塞

func StreamHandler(c *gin.Context) {
    c.Header("Content-Type", "text/event-stream")
    for {
        token := generateNextToken()
        fmt.Fprintf(c.Writer, "data: %s\n\n", token)
        c.Writer.Flush() // 关键:即时刷新缓冲区
        if isEnd(token) { break }
    }
}
// data: {"token": "Hello", "end": false}\n\n
// data: {"token": "World", "end": true}\n\n
  • SSE 是一个一直打开的 TCP 连接,所以 Connection 为 Keep-Alive

客户端使用JavaScript的EventSource监听数据流:

// 客户端:EventSource监听
const source = new EventSource('/stream');
source.addEventListener('message', (e) => {
    document.getElementById('output').innerHTML += e.data;
});
业务场景

以「文件下载」功能进行说明,一般情况下,大文件的下载,服务端压力比较大、处理时间也比较长,为了有更好的交互体验,我们可以使用异步处理,服务端处理完了之后主动通知 客户端,效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个小弹窗就是服务端处理完了之后,通过 SSE连接主动推送到客户端。

我们来看看处理流程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1)SSE 连接:

先建立 SSE 连接,确保服务端有主动推送消息的能力。

2)异步下载:

长耗时下载任务我们通过异步的方式处理,避免用户在下载页面长时间等待。

3)广播并推送:

下载完成后,我们需要将完成事件推送给客户端。需要注意的是,由于服务是集群部署、SSE 连接在节点本地 Map 维护,这就有可能导致当前客户端的 SSE连接所在节点 与 事件推送节点 是两个独立的节点。

因此,我们这里借助于 Redis 的发布/订阅能力,将消息广播出去,能匹配连接的节点负责将消息推送至客户端、其他节点直接丢弃即可。效果图如下:

能否做到精准投递?

答案也是可以的,我们可以这样来做:

借助 Redis 做中心存储,存储Map<用户,节点IP> 这样的映射关系。
在推送消息之前,先通过映射关系找到该用户的SSE连接所在节点
然后在通过 RPC调用 直接将消息投递到对应的服务节点,最后由该节点进行事件推送。
一般情况下,我们可以用「广播」这种简单粗暴的方式应对大部分场景,毕竟「精准投递」需要中心化的维护节点关系、应对节点变更等,处理起来稍显麻烦。具体视业务场景来做选择即可。

为什么大模型使用SSE而不是websocket

1.实现成本

  • 时间

WebSocket成本 = TCP握手(1次RTT) + TLS1.2握手(2次RTT) + Upgrade: websocket 请求(1次RTT) ,共计4次RTT。(如果是TLS1.3,可以做到0~1次RTT)

SSE成本 = HTTP长连接(1次RTT) + TLS ,会减少一次协议升级的RTT

tcp知识点:为什么三次握手是一个RTT

标准定义的 RTT 是:发送一个请求 + 等待一个响应的时间。

Client                                        Server
   | -------------- SYN --------------------> |   ← 第一次握手
   | <----------- SYN + ACK ---------------- |   ← 第二次握手
   | -------------- ACK --------------------> |   ← 第三次握手

对于客户端来说,ACK 不需要等响应(第三次握手不等待服务端返回)。所以完整的RTT过程是 从 SYN 到收到 SYN+ACK

  • 内存

WebSocket需维护全双工长连接,每个连接占用独立线程或内存资源,而SSE通过HTTP长连接实现推送,服务器资源消耗更低,适合高并发场景

  • 兼容性

SSE基于HTTP协议,无需额外协议升级(如WebSocket的ws:///wss://),可直接复用现有HTTP端口(80/443),减少防火墙或代理配置问题。现代浏览器原生支持EventSource API,兼容性优于WebSocket的部分限制

  • 格式

SSE支持纯文本或JSON流式传输,与大模型输出的结构化数据(如Token序列、置信度)天然契合。WebSocket虽支持二进制数据,但需额外编码解析

2.单向数据流需求

大模型的核心场景是流式文本生成,用户输入请求后,服务器需持续推送生成的文本片段。这种场景下,通信方向是单向的(服务器→客户端),而WebSocket的全双工通信能力(双向交互)反而冗余。

当用户输入问题后,模型逐词生成回答,客户端无需中途向服务器发送额外数据。

3.流式信息的不均衡性

大模型的输出Token量通常远大于输入(如输入10字问题,生成500字回答)。SSE的单向推送机制能高效处理这种**“稀疏请求,密集响应”**模式,避免WebSocket双向通道的资源浪费

4.断线重连天然支持

SSE内置自动重连机制(默认3秒重试),支持通过Last-Event-ID头部恢复断连后的数据流,适合大模型长文本生成场景的网络波动容错。而WebSocket需手动实现心跳检测与重连逻辑

SSE的核心优势在于将协议复杂度降至最低,精准匹配大模型的单向流式需求,同时通过HTTP生态的成熟性保障稳定性和兼容性。而WebSocket更适用于实时双向交互场景(如在线协作、聊天室)。技术选型时需遵循“用对的,而非用贵的”原则

什么时候不适用SSE

虽然SSE性能卓越,但在以下场景请慎用:

场景 问题 推荐方案
双向实时通信 SSE不支持客户端推送 WebSocket
二进制流传输 SSE仅支持文本 WebSocket+ArrayBuffer
超低延迟要求(<10ms) HTTP协议栈开销 QUIC协议
移动端弱网环境 长连接保活困难 MQTT+长轮询

典型案例:某在线教育平台的白板协作功能,初期采用SSE导致画笔延迟明显,切换WebSocket后延迟从200ms降至50ms。

ref :

Server-Sent Events 教程 - 阮一峰的网络日志

服务端实时推送技术之SSE(Server-Send Events)-CSDN博客

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐