sse
SSE (Server-Sent Events) 与 HTTP 连接原理
一、SSE 是什么
SSE(Server-Sent Events)是 HTML5 标准的一部分,允许服务器通过单个 HTTP 连接持续向客户端推送数据。数据方向是单向的(服务器 → 客户端)。
与 WebSocket 的核心区别:
- SSE 是纯 HTTP 协议,不需要协议升级
- WebSocket 是独立协议,需要
101 Switching Protocols握手升级 - SSE 内置断线自动重连机制,WebSocket 需手动实现
- SSE 是服务器→客户端单向,WebSocket 是双向
二、SSE 协议格式
数据流是纯文本,基于 text/event-stream MIME 类型:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
消息体格式:
data: {"text": "你好"}
data: {"text": "世界"}
data: [DONE]
data:— 消息数据行,可多行(相当于拼接)event:— 事件类型(浏览器用addEventListener监听)id:— 消息 ID,断线重连时带上Last-Event-IDretry:— 重连间隔(毫秒)- 两个
\n\n表示消息结束
三、SSE 在 AI 中的应用
大模型推理是逐 token 生成的。不使用流式传输时,用户必须等完整输出才能看到;SSE 让每个 token 生成后立即推送。
以 OpenAI 兼容 API 为例(stream: true):
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"中"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"国"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"的"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"首都"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"是"},"index":0}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"北京"},"index":0}]}
data: [DONE]
客户端实现
浏览器原生支持 EventSource API:
const sse = new EventSource('/api/chat/stream');
sse.addEventListener('message', (event) => {
if (event.data === '[DONE]') return;
const chunk = JSON.parse(event.data);
output.textContent += chunk.choices[0].delta.content;
});
sse.addEventListener('error', () => {
// 自动重连,无需手动处理
});
EventSource 的限制:
- 仅支持 GET 请求
- 无法设置自定义请求头(认证 token 需用 URL query 或 cookie)
- 生产环境中常 fallback 到
fetch+ReadableStream以支持 POST 和自定义头
服务端实现(NestJS 示例)
@Get('stream')
@Header('Content-Type', 'text/event-stream')
@Header('Cache-Control', 'no-cache')
@Header('Connection', 'keep-alive')
async stream(@Res() res: Response) {
for await (const token of model.generateStream(prompt)) {
res.write(`data: ${JSON.stringify({ text: token })}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
}
四、SSE 使用场景
| 场景 | 说明 |
|------|------|
| AI 流式输出 | LLM token 逐字推送(聊天、补全) |
| 实时通知 | 消息推送、告警通知 |
| 数据仪表盘 | 股票行情、监控指标定时推送 |
| 日志流 | 服务器日志实时展示 |
五、HTTP 连接模型
5.1 短连接 vs 长连接
| 特性 | 短连接 (HTTP/1.0) | 长连接 (HTTP/1.1 Keep-Alive) |
|------|-------------------|------------------------------|
| 请求流程 | 每次请求都 TCP 三次握手→传输→四次挥手 | 建立 TCP 后复用,请求串行发送 |
| 握手开销 | 每次都有 | 只在首次 |
| HTTP 默认 | HTTP/1.0 默认短连接 | HTTP/1.1 默认长连接 |
5.2 长连接的本质
长连接并不是"一个连接多个请求同时进行"。HTTP 协议依然是一问一答(串行):
TCP 连接建立后(三次握手)
→ GET /index.html
← 响应完整结束
(间隔几秒,连接保持)
→ GET /style.css
← 响应完整结束
(空闲超时或主动关闭)
← 四次挥手
Keep-Alive 的"长"指的是 TCP 通道保持开放,而不是 HTTP 请求可以推送或双工。
5.3 HTTP/1.1 的队头阻塞
因为同一连接上的请求必须串行,如果前一个响应迟迟不结束,后面的请求就被阻塞。浏览器因此对每个域名默认开6 条并行 TCP 连接来缓解。
5.4 HTTP/2 的多路复用
HTTP/2 解决了队头阻塞——所有请求共享一条 TCP 连接,可以在同一连接上并行交错发送多个请求和响应,不再需要 6 条连接并行。
5.5 SSE 的特殊性
SSE 绕开一问一答的方式:同一个 HTTP 响应的 body 被持续写入数据,服务端不发完就不关闭。和下载大文件的原理类似——连接一直开着,数据源源不断流过来。
普通 HTTP 长连接:
GET /a → 响应结束 → GET /b → 响应结束
SSE:
GET /stream → 响应体持续写入 → [data] → [data] → ... → [DONE] → 连接关闭
六、TCP 连接原理
6.1 四元组
每个 TCP 连接由四元组唯一标识:
(源IP, 源端口, 目标IP, 目标端口)
当浏览器建立多条连接访问同一个网站:
- 目标端口全部相同(HTTP 80 / HTTPS 443)
- 源端口各不相同——OS 为每条连接分配不同的临时端口(如 54321, 54322, ...)
多域名时目标 IP 也不同,四元组完全独立。
6.2 浏览器的连接池
Chrome → baidu.com → 连接池1(最多6条TCP连接)
→ google.com → 连接池2(完全独立)
→ api.baidu.com → 连接池3(子域名不同,池子也不同)
- 每个域名维护独立的连接池
- HTTP/1.1 下每个域名默认最多 6 条并发连接
- 不同域名的连接池互不干扰
6.3 HTTP/2 下的变化
HTTP/2 支持多路复用,所有请求走同一条 TCP 连接,不需要 6 条并行连接。
七、连接断开与失效处理
7.1 服务端 IP 变化
大型网站(百度等)通常有多个 IP,会发生变化:
- DNS 轮询:每次解析可能返回不同 IP
- CDN 节点切换:按地域、运营商动态分配
- 服务器扩容/维护:IP 池变动
已经建立的 TCP 连接不会因 DNS 变化而断开——连接基于目标 IP,不是域名。DNS 更新只影响下一次新连接。
7.2 连接断开场景
1. 优雅关闭(正常四次挥手)
服务端调用 close() 发送 FIN 包 → 客户端 OS 收到后 read() 返回 0 → 应用层感知连接关闭。
2. 机器宕机/断电
不发送任何包。重连检测流程:
客户端发送数据 → 收不到 ACK
→ 超时重试(默认 5-15 秒,每次间隔加倍)
→ 最终返回 "Connection timed out"
如果连接空闲、没有数据发送,客户端可能一直不知道连接已死,直到 TCP Keep-Alive 探测(默认 2 小时后)才发现。
3. 中间路由器重置(RST)
收到 RST 包 → 客户端下次读写立刻报 "Connection reset by peer"。
4. 负载均衡器超时回收
即使连接空闲,LB 侧也可能有超时策略(如 60 秒空闲断开)。客户端不知情,下次发数据时才发现连接失效。
7.3 浏览器如何处理
当浏览器发现连接断开:
1. 自动重建新连接(三次握手)
2. 在新连接上重新发送之前的 HTTP 请求
3. 如果请求是幂等的(GET),用户无感知
4. 对于 SSE,EventSource 自动重连并携带 Last-Event-ID,服务端可以续推
八、SSE vs WebSocket 对比
| 特性 | SSE | WebSocket |
|------|-----|-----------|
| 协议 | HTTP | ws:// 独立协议 |
| 方向 | 服务器→客户端单向 | 双向 |
| 协议升级 | 不需要 | 需 101 握手 |
| 自动重连 | 内置 | 需手动实现 |
| 请求头控制 | EventSource 不支持(可用 fetch 替代) | 支持 |
| 二进制数据 | 需 base64 编码 | 原生支持 |
| 浏览器支持 | 广泛(IE 除外) | 广泛 |
| 服务端复杂度 | 低 | 较高 |
| 适用场景 | 推送/流式输出 | 实时互动(游戏/聊天/协作) |
九、常见误解澄清
Q: SSE 是连续多次 HTTP 请求吗?
A: 不是。SSE 只有一个 HTTP 请求和一个 HTTP 响应,响应体是流式的,持续写入数据直到连接关闭。
Q: HTTP/1.1 长连接可以同时发多个请求吗?
A: 不可以。HTTP/1.1 长连接只是复用 TCP 通道,请求仍然串行(一问一答),同一时间只能处理一个请求。不过浏览器可以用 6 条并行连接来等效并行。
Q: HTTP/2 多路复用和 SSE 是什么关系?
A: 独立的两个概念。HTTP/2 多路复用是传输层多条请求共享一条 TCP;SSE 是应用层用一条 HTTP 响应持续推送数据。两者可以共存(SSE over HTTP/2)。
Q: 连接断开浏览器为什么不立刻知道?
A: TCP 是状态协议,但在无数据传输时没有心跳机制。连接断开没有包通知时,客户端只能通过下次发送数据超时或 TCP Keep-Alive 定时探测发现(可能长达数小时)。