SSE(Server-Sent Events)原理详解(二)
·
一、一句话本质
SSE 就是服务端通过一个保持打开的 HTTP 连接,持续向客户端推送文本数据。底层是 HTTP + Chunked Transfer,上层定义了一套简单的消息格式。
二、协议规范(W3C 标准)
SSE 不是某个框架的发明,是 W3C 标准(WHATWG Living Standard),浏览器原生支持。
2.1 请求
GET /events HTTP/1.1
Accept: text/event-stream ← 告诉服务端:我要 SSE
Cache-Control: no-cache
就是普通 HTTP GET,没有任何特殊握手。
2.2 响应
HTTP/1.1 200 OK
Content-Type: text/event-stream ← 关键:声明这是 SSE 流
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked ← 底层分块传输
<--- 这里不关闭,持续写入数据 --->
区别于普通 HTTP 响应: 没有 Content-Length,连接不会关闭,服务端可以随时往 TCP 流里写数据。
三、消息格式(Event Stream)
SSE 的消息格式极简:
event: eventType\n
id: messageId\n
retry: 3000\n
data: 你的数据内容\n
\n ← 空行 = 消息结束标记
3.1 四个字段
| 字段 | 作用 | 必须? |
|---|---|---|
data: |
数据内容(核心) | ✅ 必须 |
event: |
消息类型,客户端按类型分发处理 | 可选,默认 message |
id: |
消息 ID,断线重连时自动通过 Last-Event-ID 头回传 |
可选 |
retry: |
重连间隔(毫秒) | 可选,默认 3000ms |
3.2 完整示例
服务端实际写入的数据:
event: message
id: msg-001
data: {"role":"assistant","content":"你"}
event: message
id: msg-002
data: {"role":"assistant","content":"好"}
event: message
id: msg-003
data: {"role":"assistant","content":",世界"}
event: done
id: msg-004
data: [DONE]
注意最后有一个空行 \n\n,这是消息分隔符。
3.3 多行数据
如果数据本身有换行,用多个 data: 行:
data: 第一行\n
data: 第二行\n
data: 第三行\n
\n
客户端收到后会合并为:
第一行\n第二行\n第三行
四、客户端接收
4.1 浏览器 EventSource API
const eventSource = new EventSource('/chat/stream?message=你好');
// 监听默认 message 事件
eventSource.onmessage = (event) => {
console.log('收到数据:', event.data);
console.log('消息ID:', event.lastEventId);
};
// 监听自定义事件类型
eventSource.addEventListener('done', (event) => {
console.log('流结束');
eventSource.close();
});
// 监听错误(会自动重连)
eventSource.onerror = (error) => {
console.log('连接断开,浏览器会自动重连...');
};
4.2 Fetch API(更灵活,AI 场景常用)
const response = await fetch('/chat/stream?message=你好', {
headers: { 'Accept': 'text/event-stream' }
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// 解析 SSE 格式
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
console.log(JSON.parse(data));
}
}
}
五、底层传输原理
5.1 TCP 层面
TCP 连接建立后,保持 ESTABLISHED 状态
客户端 服务端
│ │
│◀── "event: message\n" ───────────│ 写入 → 立即发送
│◀── "data: 你好\n\n" ────────────│ 写入 → 立即发送
│ │
│ (TCP 连接保持打开) │
│ │
│◀── "event: message\n" ───────────│ 3秒后又写了新数据
│◀── "data: ,世界\n\n" ───────────│
│ │
5.2 为什么实时?
TCP 的 Nagle 算法 默认会合并小包。但 SSE 场景下:
- 每条消息末尾有
\n\n,触发 TCP flush - 现代 HTTP/1.1 默认
Connection: keep-alive,连接不关闭 - 服务端主动调用
flush(),数据立刻推送到客户端
延迟: 通常是毫秒级(取决于网络),不是轮询那种秒级延迟。
六、断线重连机制
这是 SSE 的一大优势,浏览器自动处理:
连接断开
│
▼
浏览器等待 retry 指定的时间(默认 3 秒)
│
▼
自动发起新请求,带上 Last-Event-ID 头:
GET /events HTTP/1.1
Accept: text/event-stream
Last-Event-ID: msg-002 ← 告诉服务端:我最后收到的是这条
│
▼
服务端可以从 msg-003 开始继续推送
七、心跳保活
长时间无数据时,连接可能被中间代理关闭。解决方案:
: 这是注释,用于保活\n\n
服务端定时(如每 30 秒)发送注释行,保持连接活跃:
// Java 实现心跳
ScheduledExecutorService heartbeat = Executors.newSingleThreadScheduledExecutor();
heartbeat.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event().comment("heartbeat"));
} catch (IOException e) {
heartbeat.shutdown();
}
}, 0, 30, TimeUnit.SECONDS);
八、完整数据流图
┌─────────────────────────────────────────────────────────────┐
│ 完整 SSE 数据流 │
└─────────────────────────────────────────────────────────────┘
客户端 服务端(Java)
│ │
│ 1. GET /chat/stream?msg=你好 │
│ Accept: text/event-stream │
│─────────────────────────────────────────────▶│
│ │ SseEmitter 创建
│ │ AI 开始生成...
│ 2. 响应头 │
│ Content-Type: text/event-stream │
│ Transfer-Encoding: chunked │
│◀─────────────────────────────────────────────│
│ │ 生成 token "你"
│ 3. 第一条事件 │
│ event: message\ndata: "你"\n\n │
│◀─────────────────────────────────────────────│
│ │ 生成 token "好"
│ 4. 第二条事件 │
│ event: message\ndata: "好"\n\n │
│◀─────────────────────────────────────────────│
│ │ 生成 token "世"
│ 5. 第三条事件 │
│ event: message\ndata: "世"\n\n │
│◀─────────────────────────────────────────────│
│ │ 生成 token "界"
│ 6. 第四条事件 │
│ event: message\ndata: "界"\n\n │
│◀─────────────────────────────────────────────│
│ │ AI 生成完毕
│ 7. 结束事件 │
│ event: done\ndata: [DONE]\n\n │
│◀─────────────────────────────────────────────│
│ │ emitter.complete()
│ 8. 连接正常关闭 │
│◀─────────────────────────────────────────────│
九、SSE 的局限性
| 局限 | 说明 |
|---|---|
| 单向通信 | 只能服务端→客户端,客户端→服务端需要另发 HTTP 请求 |
| 纯文本 | 只支持文本,二进制数据需要 Base64 编码 |
| 同源限制 | EventSource 默认受 CORS 限制(可用代理或 CORS 头解决) |
| HTTP/1.1 并发限制 | 浏览器对同一域名最多 6 个连接(HTTP/2 多路复用解决此问题) |
| 中间代理超时 | 某些代理会在空闲超时后关闭连接(用心跳解决) |
十、总结
SSE = HTTP + 保持连接不关闭 + 简单文本格式 + 浏览器自动重连
底层:TCP 长连接 + Chunked Transfer
协议:text/event-stream + data:\n\n 消息分隔
客户端:EventSource API(自动重连)或 Fetch + ReadableStream
优势:零额外协议开销、标准 HTTP、天然穿透防火墙和代理
SSE 的设计哲学:用最简单的方式实现服务端推送。不发明新协议,不搞复杂握手,就在 HTTP 上加一层薄薄的消息格式。
更多推荐





所有评论(0)