一句话定义:SSE(Server-Sent Events)是浏览器内置的“服务器单向推送”通道;连接意外断开后,浏览器的 EventSource 对象会自动重连,并在请求头里带上 Last-Event-ID,告诉服务端“我最后收到的事件编号是这条”,服务端据此从下一条继续发送。
打个比方:这就像听连载评书。每回结尾报一个回目号,你听到“第 37 回”时网断了,重新连上就跟平台说“我听到 37 回了”,平台从 38 回开始放,不用从头再听一遍。回目号就是事件里的 id,那句“我听到 37 回”就是 Last-Event-ID。
具体怎么发生:SSE 走的是普通 HTTP,响应头是 Content-Type: text/event-stream,正文是一段段纯文本,每段用空行结尾:
```
id: 1024
event: token
data: {"text":"你好"}
```
id 是事件编号,event 是事件类型名(不写就是默认的 message),data 是载荷,另有 retry: 可以告诉浏览器重连前等多少毫秒。浏览器每完整收到一段(空行到达才算完整),就把里面的 id 记下来。连接一断,它等一小会儿再发一次 GET,头里多一句 Last-Event-ID: 1024。
这里要分清两件事:自动重连是浏览器送的,但“从哪接着发”得服务端自己实现。服务端要有一份按序号能回放的事件缓冲:内存环形队列、Redis 的 stream、或者落库都行;收到 Last-Event-ID 就查 1025 之后的内容往回灌。缓冲总是有限的,如果客户端要的编号已经被淘汰,只能退回“重发全量快照”,或者明确告诉前端“这条流接不上了,请重新开始”。
两个容易踩的坑:一是半截事件会被丢弃,编号只在完整事件到达时才更新,所以重连时最后一条可能重复,前端处理要幂等;二是事件里不写 id 就沿用上一次的编号,从头到尾没写过 id,浏览器压根不会带这个头。
和邻居的区别:
| SSE | WebSocket | fetch 流式读取 | |
|---|---|---|---|
| 方向 | 服务器→客户端 | 双向 | 服务器→客户端 |
| 断线重连 | 浏览器自动,带 Last-Event-ID | 自己写 | 自己写 |
| 续传语义 | 协议内置 | 自定义序号 | 自定义序号 |
| 自定义请求头 | 不支持(靠 cookie 或 query 传凭证) | 握手时可带 | 支持 |
实际意义:大模型应用的流式输出大多用 SSE,就是图这套“断了能续”。但注意,续传的前提是服务端把已经生成的 token 存下来了,比如边生成边往会话缓冲里追加并编号;如果没存,重连只能重来。另外在 HTTP/1.1 下,同一域名下并发的 SSE 连接数有上限(浏览器通常在 6 个左右),开一堆标签页会把连接占满,这也是老项目迁到 HTTP/2 的常见理由。对普通用户来说,这层机制就表现为:回答到一半网络卡了一下,回来字接着往下蹦,而不是让你重新问一遍。
