一、一句话本质

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 场景下:

  1. 每条消息末尾有 \n\n,触发 TCP flush
  2. 现代 HTTP/1.1 默认 Connection: keep-alive,连接不关闭
  3. 服务端主动调用 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 上加一层薄薄的消息格式。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐