一文讲清楚: SSE、WebSocket 与 HTTP的关系

一文讲清楚: SSE、WebSocket 与 HTTP的关系

前言

最近在研究 AI 编程工具的时候,我发现一个很有意思的现象:客户端向 AI 服务端发送请求时,很多场景下并不是等服务端一次性返回完整结果,而是通过 SSE 的方式持续接收模型生成的内容。

也就是说,AI 不是"想完之后一次性把答案发回来",而是一边生成,一边把内容流式推送给前端。于是我顺着这个方向研究了一下 SSE 的请求和响应机制,并整理了这篇文章,主要讲清楚:什么是 SSE?它的底层原理是什么?为什么 AI 对话、代码生成这类场景特别适合用 SSE?


一、协议层次关系

bash 复制代码
┌─────────────────────────────────────────────────────┐
│                     应用层                           │
│                                                     │
│   HTTP/1.1          SSE             WebSocket        │
│  (请求-响应)    (HTTP 长响应)     (独立帧协议)        │
│                                                     │
│   普通接口        AI 流式输出      聊天室 / 游戏       │
│   GET /api/user   GET /api/stream  ws://xxx/chat     │
├─────────────────────────────────────────────────────┤
│                     传输层                           │
│                    TCP 连接                          │
│             (三次握手建立,四次挥手关闭)               │
├─────────────────────────────────────────────────────┤
│                     网络层                           │
│                   IP 协议                            │
│              (寻址、路由、分包)                       │
└─────────────────────────────────────────────────────┘

核心结论

  • SSE 不是新协议,它就是 HTTP 响应的一种特殊用法(不关闭响应,持续写数据)
  • WebSocket 是新协议 ,通过 HTTP 握手后升级为独立的帧协议(ws://
  • 三者底层都是 TCP 连接,没有任何魔法

二、普通 HTTP ------ 一问一答

工作模式

bash 复制代码
客户端                        服务端
  │                            │
  │─── GET /api/user/info ───>│  ← 发起请求
  │                            │  ← 服务端处理
  │<── 200 { "name":"张三" } ──│  ← 返回响应
  │                            │
  │      TCP 连接关闭           │  ← 结束

特点

  • 一次请求,一次响应,响应写完就关闭
  • 响应头 Content-Length: 1234 告诉客户端"一共这么多字节"
  • 客户端收完数据才能处理,不能"收一半就用"

代码示例(Spring Boot)

bash 复制代码
@GetMapping("/api/user/info")
public Map<String, Object> getUserInfo() {
    return Map.of("name", "张三", "age", 25);
}
bash 复制代码
// 前端
const res = await fetch('/api/user/info')
const user = await res.json()  // 必须等响应完整返回

三、SSE ------ 服务端单向推送

工作模式

bash 复制代码
客户端                              服务端
  │                                  │
  │── GET /api/stream ─────────────>│  ← 普通 HTTP GET
  │                                  │
  │<── 200 OK                        │
  │    Content-Type: text/event-stream
  │    Transfer-Encoding: chunked    │  ← 响应头,不关闭连接
  │                                  │
  │<── data:{"d":"你"}               │  ← 第 1 块数据(时刻 1s)
  │<── data:{"d":"好"}               │  ← 第 2 块数据(时刻 1.1s)
  │<── data:{"d":"世"}               │  ← 第 3 块数据(时刻 1.2s)
  │<── data:{"d":"界"}               │  ← 第 4 块数据(时刻 1.3s)
  │         ...                      │  ← 持续推送
  │<── event:done                    │  ← 结束事件
  │       data:                      │
  │                                  │
  │      TCP 连接关闭                 │  ← 服务端主动关闭

本质

一个不关闭的 HTTP 响应。 服务端返回响应头后,不调用 response.close(),有数据就往响应体里 write,客户端的 TCP socket 一直在 read,读到数据就触发回调。

所谓"推送",就是服务端往一个没有关闭的 HTTP 响应里继续写数据

两个关键响应头

响应头 作用
Content-Type: text/event-stream 告诉浏览器这是事件流,收到一条处理一条,不要等全部写完
Transfer-Encoding: chunked 分块传输,每次发一小块,不需要提前知道总长度

数据格式

SSE 规定了一个极简的纯文本格式:

bash 复制代码
event: 事件名\n        ← 可选,默认是 "message"
data: 数据内容\n       ← 必须
\n                     ← 空行表示一条消息结束

多行数据:

bash 复制代码
data: 第一行\n
data: 第二行\n
\n

带事件名:

bash 复制代码
event: build_complete\n
data: {"status": "success"}\n
\n

代码示例(Spring Boot + WebFlux)

bash 复制代码
@GetMapping(value = "/api/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
    return Flux.create(sink -> {
        // 模拟 AI 逐字输出
        tokenStream
            .onPartialResponse(token -> {
                sink.next(token);  // → 变成 data:xxx\n\n 写入 TCP
            })
            .onComplete(() -> {
                sink.complete();   // → 写入 event:done,关闭连接
            });
    });
}
bash 复制代码
// 前端
const es = new EventSource('/api/stream')

es.onmessage = (event) => {
    const data = JSON.parse(event.data)
    // 每收到一条就处理,不需要等全部完成
    appendToPage(data.d)
}

es.addEventListener('done', () => {
    es.close()  // 手动关闭
})

浏览器内置能力

EventSource 是浏览器原生 API,自带:

  • 自动重连:连接断开后自动重新连接(默认 3 秒)
  • 事件解析 :自动按 data:\n\n 格式解析,触发回调
  • Last-Event-ID:重连时自动带上上次收到的事件 ID,服务端可据此续传

四、WebSocket ------ 全双工双向通信

工作模式

bash 复制代码
客户端                              服务端
  │                                  │
  │── GET /chat (Upgrade: websocket)│  ← HTTP 握手请求
  │<── 101 Switching Protocols ─────│  ← 协议升级成功
  │                                  │
  │ ═══════ WebSocket 连接建立 ═══════│  ← 不再是 HTTP 了
  │                                  │
  │──── {"msg": "你好"} ──────────>│  ← 客户端发
  │<─── {"msg": "你好!有什么可以帮你?"}│  ← 服务端发
  │──── {"msg": "帮我写代码"} ─────>│  ← 客户端发
  │<─── {"msg": "好的,请稍等"} ────│  ← 服务端发
  │<─── {"msg": "代码写好了"} ──────│  ← 服务端主动发
  │                                  │
  │ ═══════ 任意一方关闭 ═════════════│

本质

WebSocket 是真正的新协议。握手阶段借用 HTTP,握手完成后切换为独立的二进制帧协议。

握手过程(唯一和 HTTP 有关的部分)

客户端请求

bash 复制代码
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket              ← 请求升级协议
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZQ==   ← 随机 key
Sec-WebSocket-Version: 13

服务端响应

bash 复制代码
HTTP/1.1 101 Switching Protocols    ← 101 表示协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← 验证 key

握手之后,这条 TCP 连接上传输的就不再是 HTTP 文本了,而是 WebSocket 的二进制帧格式。

帧格式(握手后的数据传输)

bash 复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |            (16/64)            |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (if MASK=1)   |                               |
+-------------------------------+-------------------------------+
|                     Payload Data                              |
+---------------------------------------------------------------+

这已经和 HTTP 完全无关了,是独立的二进制协议。

代码示例(Spring Boot)

bash 复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/ws/chat");
    }

    @Bean
    public WebSocketHandler chatHandler() {
        return new TextWebSocketHandler() {
            @Override
            protected void handleTextMessage(WebSocketSession session, TextMessage message) {
                // 收到客户端消息
                String payload = message.getPayload();
                // 服务端可以随时主动发
                session.sendMessage(new TextMessage("收到:" + payload));
            }
        };
    }
}
bash 复制代码
// 前端
const ws = new WebSocket('ws://localhost:8123/ws/chat')

ws.onopen = () => {
    ws.send('你好')  // 客户端 → 服务端
}

ws.onmessage = (event) => {
    console.log(event.data)  // 服务端 → 客户端
}

ws.send('再发一条')  // 随时可以发

五、三者核心对比

bash 复制代码
                普通 HTTP              SSE                  WebSocket

协议            HTTP/1.1              HTTP/1.1              ws://(新协议)
                                      (不变)                (HTTP 升级握手后切换)

通信方向        客户端 → 服务端        服务端 → 客户端        双向
                (请求-响应)            (单向推送)             (任意一方随时发)

连接生命周期    短(一问一答就关闭)    中长(服务端控制关闭)   长(任意一方关闭)

数据格式        任意                   纯文本                 文本或二进制
                (JSON/XML/HTML)       (data: xxx\n\n)       (帧格式)

浏览器 API      fetch / XMLHttpRequest EventSource           WebSocket

断线重连        无(每次新请求)        自动重连               需自己实现

服务端实现      @GetMapping            Flux<String>          WebSocketHandler
                返回对象               + text/event-stream    + @EnableWebSocket

适用场景        查数据、提交表单        AI 流式输出            聊天室、游戏
                                      实时通知               协同编辑
                                      构建状态推送            双向交互

六、从 TCP 连接的视角看三者

普通 HTTP

bash 复制代码
TCP 连接建立 → 发请求 → 收响应 → TCP 连接关闭
   (0.1s)                          (0.3s)

一条 TCP 连接只活了 0.3 秒。

SSE

bash 复制代码
TCP 连接建立 → 发请求 → 收响应头 → 收数据块 → 收数据块 → ... → TCP 连接关闭
   (0.1s)                         (1s)       (1.1s)          (45s)

一条 TCP 连接活了 45 秒,期间服务端不断往里写数据。

WebSocket

bash 复制代码
TCP 连接建立 → HTTP 握手 → 协议升级 → 双向帧传输 → ... → TCP 连接关闭
   (0.1s)      (0.2s)     (0.2s)    (持续数小时)        (用户关闭页面)

一条 TCP 连接可能活几个小时,双方随时互发数据。

三者用的都是同一条 TCP 连接,区别只在于上面跑的是什么格式的数据、谁能发、什么时候关。


七、为什么 AI 流式输出选 SSE 而不是 WebSocket

考量 SSE WebSocket
方向需求 AI 输出只需要服务端 → 客户端(单向够用) 双向能力浪费了
实现成本 极低,Flux + 一个注解 需要握手、帧处理、心跳
断线重连 浏览器自动 需自己写
HTTP 兼容 完全兼容,能过所有代理和 CDN 部分企业防火墙会拦截 ws://
用户输入 用户发消息走普通 POST 即可 ---

AI 流式输出的场景:用户发一条消息(POST),AI 逐字返回(SSE)。 这是一个"一问、多答"的模式,SSE 完美匹配,WebSocket 大材小用。


八、轮询 vs SSE 推送 ------ 两种"等结果"的方式

当你需要等待一个异步操作的结果时(比如等构建完成),有两种方式:

轮询(Polling)

bash 复制代码
前端                          后端
  │── GET /build/status ────>│
  │<──── "building" ─────────│
  │        (等 2 秒)          │
  │── GET /build/status ────>│
  │<──── "building" ─────────│
  │        (等 2 秒)          │
  │── GET /build/status ────>│
  │<──── "success" ──────────│  ← 终于完成了
  │   停止轮询,刷新预览       │

本质:客户端定时发 HTTP 请求问"好了没?"

优点:实现极简(一个 setInterval + 一个 GET 接口)

缺点:大部分请求是无效的(返回 "building")

SSE 推送

bash 复制代码
前端                              后端
  │── GET /build/events ────────>│
  │<── 200 text/event-stream ────│  ← 连接保持
  │         (安静等待)             │
  │                               │  ← 构建完成
  │<── event:build_complete ─────│  ← 服务端主动推
  │   关闭连接,刷新预览           │

本质:客户端建立长连接,服务端有结果时主动写入

优点:零延迟,零无效请求

缺点:需要管理长连接的生命周期(超时、断开、清理)

选择依据

bash 复制代码
事件频率低(构建完成通知)→ 轮询就够了,简单可靠
事件频率高(AI 逐字输出) → 必须 SSE,轮询做不了
需要双向通信(聊天室)   → WebSocket

九、一句话总结

HTTP 是"你问我答",SSE 是"你问一次我一直答",WebSocket 是"咱俩随便聊"。三者底层都是 TCP 连接,区别只在于谁能说话、说多久、什么时候挂。

相关推荐
代码丰1 小时前
java 21虚拟线程vs传统线程 原理分析以及具体测试例子去分析性能提升
后端
用户0534369380731 小时前
langchainrust:Rust 版 LangChain 框架(LLM+Agent+RAG)
后端
fox_lht1 小时前
第十章 通用集合
开发语言·后端·算法·rust
悟空聊架构2 小时前
GStack的26种专家角色,真正实现一人成军!
后端
counting money2 小时前
Spring框架基础(依赖注入-半注解形式)
java·后端·spring
Code_Artist2 小时前
一天之内我让 AI 用 Netty 造了一个最小可用的 MVC 框架:体验一下造轮子的快感😅!
后端·netty·ai编程
也许明天y2 小时前
LangChain4j + Spring Boot 多智能体协调架构原理深度解析
spring boot·后端·agent
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第20题:HashMap在计算index的时候,为什么要对数组长度做减1操作
java·开发语言·数据结构·后端·面试·哈希算法·hash-index
阿丰资源3 小时前
基于Spring Boot的新闻推荐系统(源码+数据库+文档)
数据库·spring boot·后端