WebSocket 和 SSE 有什么区别?从实时通信到 AI 流式输出

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,原因是两者刚好对上:

  1. 方向本来就是单向的。模型生成的时候,数据只需要从服务器流向浏览器,用户在这一刻并不需要同时往回发东西。SSE 唯一的短板(不能双向)在这个场景里根本不算短板。
  2. HTTP 基础设施全兼容。不用额外配 WebSocket 网关,公司里现成的反向代理、负载均衡、鉴权、日志都能直接用上。
  3. 自动重连省事 。网络抖一下断了,EventSource 会自己接回来。
  4. 实现简单 。后端只要设好 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。
相关推荐
parser3 小时前
Python 装饰器:从语法糖、闭包到 @wraps(上篇)
后端
高频因子挖掘机3 小时前
股票池一大就请求缓慢?量化系统批量获取行情的设计与优化
后端·github·api
郑州光合科技余经理3 小时前
本地生活平台搭建:跨业态用户标识怎么贯通
java·开发语言·前端·后端·uni-app·php·ai编程
Thneonl3 小时前
故意弄坏生产:混沌工程不是乱砸,是实验设计
后端·程序员
Thneonl3 小时前
别上来就 strace:60 秒十条命令看清一台病机
后端·程序员
马剑威(威哥爱编程)3 小时前
【AI全栈后端12-11】Spring Boot 把 AI 接口真正上线扛量:限流 / 降级 / 可观测
人工智能·spring boot·后端
tachibana23 小时前
Golang 中数组和切片的区别?
开发语言·后端·golang·数组·切片
我是人✓4 小时前
Spring IOC注解全解析和Spring整合测试单元
java·后端·spring
Sam_Deep_Thinking4 小时前
CountDownLatch实现原理
java·后端·面试·程序员