HTTP 的基本模型是"一问一答":客户端发一个请求,服务器回一个响应,然后这次交互就结束了。用户不点、不刷新,服务器就没有机会主动开口。
但有一类需求天生就是反过来的------数据在服务器那边不断产生,要主动推给客户端 :聊天消息、股票行情、系统通知、以及现在的 AI 一个字一个字往外吐。HTTP 这套"你不问我不说"的模型顶不住,于是就有了 WebSocket 和 SSE 两条路。
这两样东西经常被摆在一起比较,因为它们解决的是同一类问题,但走的是完全不同的方向。这篇文章先把 HTTP 为什么不够说清楚,再分别讲这两条路,最后落到 AI 流式输出为什么选 SSE。
HTTP 为什么不够用
请求-响应是本位
HTTP 每次交互都是客户端发起、服务器响应。哪怕开了 keep-alive 复用 TCP 连接,语义上还是一问一答,服务器不能凭空插一句。
要做"服务器主动推",我们能想到的最直觉的土办法是 轮询( polling ):客户端每隔两秒问一次"有没有新消息"。
text
客户端 → 服务器:有新消息吗? (没有)
客户端 → 服务器:有新消息吗? (没有)
客户端 → 服务器:有新消息吗? (还是没有)
客户端 → 服务器:有新消息吗? (有一条!)
问题很明显:消息什么时候来是随机的,但你的提问间隔是固定的。来得早,你就等在下一个轮询周期;来得晚,前几次问都是白问。间隔调小浪费请求,调大延迟又高,怎么都别扭。
我们再看改进版长轮询( long polling ):服务器收到请求后先不回应,一直挂着,等真有消息了才把响应返回,客户端收到后立刻再发下一个请求。这能压低延迟,但每次消息都要重新建一次请求,而且服务器要一直挂着大量连接。
这两条路都是在"请求-响应"这个框子里想办法,而问题恰恰是这个框子本身。真正的解法是把连接的性质改掉------SSE 和 WebSocket 就是两种改法。
SSE
它其实就是一个"不结束的 HTTP 响应"
SSE( Server-Sent Events ) 的做法非常朴素:客户端发起一个普通 HTTP 请求,服务器不回完,就这么一直挂着,隔一会儿往响应体里写一段数据。
响应头里声明类型:
text
GET /stream HTTP/1.1
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
然后响应体不是"一次性的一个 HTML",而是一段一段往下流的文本,每段格式固定,我们看一段例子:
text
event: message
id: 42
data: {"text": "你"}
data: {"text": "好"}
data: {"text": "呀"}
规则是:每个事件由若干行组成,行是 字段: 值,一个空行代表这个事件结束。常用的几个字段:
| 字段 | 作用 |
|---|---|
data |
要发送的内容(可以出现多行,客户端会拼接) |
event |
事件名,前端可以按名字分别监听 |
id |
事件编号,重连时用来续传 |
retry |
建议客户端重连的间隔(毫秒) |
浏览器端用内置的 EventSource 就能消费,不需要额外库:
javascript
const es = new EventSource('/stream');
es.onmessage = (e) => {
appendToPage(JSON.parse(e.data));
};
自带的两个好处
SSE 最省心的地方是断线重连是浏览器自动做的 。连接断了,EventSource 会自己按 retry 的间隔重连,而且重连时会带上 Last-Event-ID 请求头,把上次收到的最后一个 id 报给服务器,服务器可以据此从断点继续发。
另一个好处是它就是普通 HTTP。不用改协议、不用配特殊端口,普通的负载均衡、CDN、反向代理、鉴权中间件都能直接套上去,防火墙也不会拦。
限制
- 只能单向。数据只会从服务器流向客户端,客户端没法用这条连接往回说话。要发东西,得另开一个普通 HTTP 请求。
- 只能是文本。传输内容必须是 UTF-8 文本,二进制数据要先 base64 编码塞进去,会变大一截。
- HTTP/1.1 下有连接数上限。浏览器对同一个域名的并发连接默认限制在 6 个左右,开满之后第七条就排队了。所以一个页面开太多 SSE 会互相卡住。换到 HTTP/2 就没这个问题------多路复用下连接上限高得多。
WebSocket
先借 HTTP 握个手,再把协议换掉
WebSocket 也从一个 HTTP 请求开始,但这个请求是来"谈判"的:
text
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
服务器如果同意,会回一个 101 Switching Protocols。这一行返回之后,这条 TCP 连接上跑的就不再是 HTTP 了,换成了 WebSocket 自己的帧协议。之后的通信都是一个个帧,双向的。
地址用 ws:// 和 wss://,后者就是加了 TLS 的版本,和 HTTPS 的关系一模一样,可以回顾一下 HTTPS 和 TLS 那篇。
全双工
握手完成后,这条连接是全双工的:客户端和服务器可以随时往对方发数据,不用等谁先问。
javascript
const ws = new WebSocket('wss://example.com/chat');
ws.onmessage = (e) => { appendToPage(JSON.parse(e.data)); };
ws.send(JSON.stringify({ type: 'msg', text: 'hello' })); // 随时往回发
帧既支持文本也支持二进制,不用像 SSE 那样为二进制做编码。协议里还定义了 ping / pong 帧,用来探测连接是否还活着。
代价是重连要自己写。WebSocket 没有内置自动重连,断线之后得自己实现重连,还要处理重连期间的消息补偿、心跳保活这些事。SSE 那边浏览器帮你做掉的,这里都要自己来。
两者的区别
| SSE | WebSocket | |
|---|---|---|
| 方向 | 单向,服务器 → 客户端 | 全双工,双向 |
| 底层协议 | 就是 HTTP,响应一直不结束 | HTTP 握手后升级为独立帧协议 |
| 地址 | 普通 HTTP 地址 | ws:// / wss:// |
| 数据格式 | 只能是 UTF-8 文本 | 文本 + 二进制 |
| 自动重连 | 浏览器内置 | 要自己实现 |
| 消息编号/续传 | 内置(id / Last-Event-ID) |
要自己实现 |
| 代理 / 防火墙兼容 | 最好,就是普通 HTTP | 需要支持 Upgrade,个别老代理会拦 |
| 实现成本 | 低 | 高 |
| 典型场景 | 通知、进度条、AI 流式输出 | 聊天、协同编辑、游戏、行情 |
简单记:SSE 只允许服务器往客户端发,WebSocket 允许双方随时对发。方向上只需要单向的时候,SSE 通常更划算;一旦需要双向,或者要传二进制,就得上 WebSocket。
AI 流式输出为什么用 SSE
现在说标题的后半段。大模型生成回答是一个 token 一个 token 往外蹦的,产品上想做成"打字机"效果------生成一个就显示一个,而不是等一整段生成完再一次性返回。这就是 AI 流式输出的场景。
这个场景几乎清一色用 SSE,原因是两者刚好对上:
- 方向本来就是单向的。模型生成的时候,数据只需要从服务器流向浏览器,用户在这一刻并不需要同时往回发东西。SSE 唯一的短板(不能双向)在这个场景里根本不算短板。
- HTTP 基础设施全兼容。不用额外配 WebSocket 网关,公司里现成的反向代理、负载均衡、鉴权、日志都能直接用上。
- 自动重连省事 。网络抖一下断了,
EventSource会自己接回来。 - 实现简单 。后端只要设好
Content-Type: text/event-stream,然后一行行往外写data: ...就行,不需要维护连接状态机。
以 OpenAI 的接口为例,请求里带 "stream": true,返回的就是 SSE:
text
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: [DONE]
一段一段 data: {...} 流下来,最后用一条 data: [DONE] 表示结束。前端把每个 chunk 里的 delta.content 拼起来,就是屏幕上看到的一个个字往外冒的效果。
注意这里的 SSE 是"借来当传输用"的:消息内容是 JSON,格式完全是应用层自己定的,跟 SSE 标准本身关系不大。选它主要是因为"它就是普通 HTTP,还能流"。
什么时候会想换回 WebSocket
前面讲的都是"你问一句、AI 答一段"的简单对话,单向确实够用。但现在的 AI 产品往 agent 方向走之后,出现了双向的需求:
- 打断生成:用户看到一半想让模型停下,或者中途改个方向------这是客户端往服务器发的指令。
- 工具调用确认:agent 动手之前弹窗问用户"同意吗",用户点同意或拒绝,又是一次反向消息。
- 多设备同步:手机上批准了一个操作,笔记本上那个标签页也要立刻更新。
这些都需要在同一条连接上双向通信。用 SSE 的话,每个反向动作都得另开一个 HTTP 请求,前端要维护"SSE 流"和"一堆 HTTP 端点"之间的状态对应,写起来很快就开始打补丁。所以 2026 年前后能明显看到一些团队在 agent 场景里从 SSE 往 WebSocket 迁。
所以选型很直白:纯"问答式"流式输出用 SSE 就够了,一旦进入需要用户实时介入的 agent 交互,就该考虑 WebSocket 了 。还有个地方要注意,SSE 和 WebSocket 都建立在 HTTP 之上,属于应用层,和互联网四层模型对照着看,它们的位置在 Transport Layer 的 TCP 之上、Application Layer 这一层。
Summary
- HTTP 不够用的地方:它是一问一答,服务器无法主动推数据;轮询和长轮询都只是在这个框子里打补丁。
- SSE :一个"不结束的 HTTP 响应",服务器持续往下写
data:文本。单向、纯文本、浏览器自带重连,兼容性最好。 - WebSocket :先用 HTTP 握手,再升级成全双工的帧协议(
ws:///wss://)。双向、支持二进制,但重连要自己写。 - 区别的核心:方向(单向 / 双向),其它差异(数据格式、重连、代理兼容)基本都是从这个区别衍生出来的。
- AI 流式输出选 SSE:生成是单向的,SSE 的短板用不上,而 HTTP 兼容 + 自动重连正好是对的。等交互变成需要用户实时介入的 agent 时,才会考虑换 WebSocket。