1. 完整回答
SSE 和 WebSocket 都可以让服务端实时把消息推送到浏览器,减少传统轮询带来的延迟和无效请求。它们主要的区别是通信方向和底层协议。
SSE 基于 HTTP 流式响应。浏览器通常通过 EventSource 发起 GET 请求,服务端返回 text/event-stream,并保持响应持续打开,有新数据就按约定格式写入。浏览器会自动解析事件,连接异常中断后也会尝试重连;如果服务端提供事件 ID,还可以结合 Last-Event-ID 实现断点补发。
WebSocket 则提供全双工通信。常见的 HTTP/1.1 场景下,客户端先发起 HTTP 握手,服务端返回 101 Switching Protocols 后,双方就能通过 WebSocket 帧持续收发文本或二进制消息。它还支持 Ping/Pong 控制帧,但断线重连和业务消息补发通常要自己实现。
实际选型时,我会看客户端是否需要持续主动发送消息。像 AI 回答逐字输出、任务进度和通知推送,SSE 通常更简单;像即时聊天、在线协作和实时互动,WebSocket 更合适。两者都需要处理连接超时、代理配置和消息可靠性。
2. 核心原理与关键概念
2.1 核心原理
SSE 的数据流程:
浏览器发起 HTTP GET → 服务端以 Content-Type: text/event-stream 返回并保持响应打开 → 按事件格式持续写入数据 → 浏览器遇到空行完成一条事件的解析和分发 → 意外断线后 EventSource 尝试重连。
SSE 事件可以包含 data、event、id 和 retry 字段。data 表示内容,event 指定事件类型,id 用于记录最后处理的事件标识,retry 用于设置重连等待时间。一条事件通常以空行结束,服务端要及时刷新输出缓冲区,浏览器才能尽快收到。
WebSocket 的数据流程:
客户端发起 HTTP/1.1 升级握手 → 服务端验证并返回 101 → 连接切换为 WebSocket 通信 → 双方通过文本帧、二进制帧交换消息 → 通过 Ping/Pong 检测连接、Close 帧正常关闭。
WebSocket 在 TCP 连接上进行消息分帧,浏览器发往服务端的数据帧需要掩码处理。在单条正常连接内,TCP 提供可靠、有序传输;跨断线的消息恢复仍由业务处理。
2.2 关键概念
| 概念 | 含义与作用 |
|---|---|
EventSource |
浏览器原生 SSE 接口,接收服务端事件并负责正常情况下的自动重连。 |
text/event-stream |
SSE 响应类型,使用 UTF-8 文本和空行分隔事件。 |
Last-Event-ID |
SSE 重连时携带上一次事件 ID,供服务端判断从哪里恢复。 |
101 Switching Protocols |
HTTP/1.1 WebSocket 握手成功后的协议切换响应。 |
| WebSocket 帧 | 承载文本、二进制数据和 Ping、Pong、Close 等控制信息。 |
| 心跳与业务确认 | 心跳检查连接是否仍可用;业务确认用于判断一条消息是否真正处理成功。 |
3. 高频面试追问
3.1 SSE 和 WebSocket 怎么选?
主要看通信方向、数据类型和连接管理成本。如果客户端只需要持续接收服务端更新,比如 AI 流式回答、日志、进度条,我一般选 SSE。它沿用 HTTP,浏览器原生支持事件解析和重连,整体实现比较轻。需要客户端和服务端持续双向交流,比如聊天室、多人协作、实时游戏,我会选 WebSocket。WebSocket 还原生支持二进制消息,适合交互频繁的场景。性能不能只按协议名称判断,还要结合并发连接数、消息频率和部署环境评估。
3.2 SSE 和普通 HTTP 轮询有什么区别?
核心区别是数据到达的方式。普通轮询是客户端隔一段时间发请求,可能反复得到“没有更新”;长轮询会把请求挂起,等有消息或超时后返回,再重新发起请求。SSE 则保持一次 HTTP 响应持续输出多个事件,服务端产生数据就推送,减少反复请求的开销。SSE 适合持续单向更新,长轮询一般用在不方便使用流式响应的兼容场景。
3.3 SSE 断线后怎么重连?能保证消息不丢吗?
EventSource 能自动重连,但消息补发需要服务端配合。服务端给事件设置递增 ID,浏览器收到后保存最后的事件 ID;连接意外断开时,重连请求会通过 Last-Event-ID 带回这个值。服务端再根据持久化的事件记录补发后续消息。客户端最好按事件 ID 去重,因为连接切换和重试时可能出现重复投递。对于已经结束或不允许重试的流,还要明确关闭连接或按协议返回停止重连的响应。
继续追问:只设置 id 就能做到不丢消息吗?
不能。id 只提供恢复位置,服务端还得保留可重放的数据,并规定历史数据过期或恢复失败时如何处理。需要严格可靠性的业务,还会增加消息确认、幂等处理和数据校验。
3.4 WebSocket 为什么要先进行 HTTP 握手?握手过程是什么?
主要是利用现有 HTTP 基础设施协商建立 WebSocket 连接。以常见的 HTTP/1.1 为例,客户端发送 GET 请求,携带 Upgrade: websocket、Connection: Upgrade 和 Sec-WebSocket-Key 等字段。服务端验证后返回状态码 101,并通过 Sec-WebSocket-Accept 证明接受升级。此后数据按照 WebSocket 帧格式传输,双方可以随时发消息。需要补充的是,WebSocket 也有基于 HTTP/2、HTTP/3 的扩展建立方式,不能把 101 当成所有环境下的唯一握手形式。
3.5 WebSocket 为什么需要心跳?断线后怎么处理?
心跳用于及时发现连接失效,重连用于恢复通信。TCP 连接可能因为移动网络切换、代理超时等原因出现半开状态,应用未必能立刻收到断线事件。WebSocket 协议有 Ping/Pong 控制帧,服务端可以定期发 Ping 并检查 Pong。浏览器原生 WebSocket API 不能直接发送协议级 Ping,因此前端如需主动检测,通常发送自定义业务心跳。发现超时后关闭旧连接,再采用带退避和随机抖动的方式重连,连接恢复后重新鉴权、订阅并补拉遗漏数据。
3.6 WebSocket 基于 TCP,为什么仍然可能丢业务消息?
TCP 保证的是当前连接中的字节可靠、有序传输,不保证断线前后业务消息一定被处理。例如服务端刚发出消息就宕机,客户端可能没收到;客户端收到了但尚未保存,也可能因刷新页面而丢失状态。如果业务要求可靠,我会给消息分配唯一 ID,持久化关键消息,增加应用层 ACK;重连后按序号补拉,消费端按 ID 幂等去重。对于同一会话还要明确消息排序规则,避免多节点并发发送造成业务顺序混乱。
3.7 SSE 明明在推送,前端为什么迟迟收不到?
优先检查中间层缓冲、应用刷新和超时设置。SSE 数据可能已经由后端写出,但被应用框架、压缩模块或 Nginx 代理缓冲,积累到一定大小才一起到达。我会确认响应类型为 text/event-stream、每条事件有结束空行、服务端及时 flush,并根据部署方式关闭对应接口的代理缓冲,例如使用 X-Accel-Buffering: no。长时间无数据时还可以发送 SSE 注释行保活,同时检查负载均衡器的空闲超时。若走 HTTP/1.1,也要留意浏览器同源连接数限制;HTTP/2 多路复用可以缓解这个问题。
继续追问:WebSocket 经过 Nginx 连不上怎么办?
先检查反向代理是否正确转发 Upgrade、Connection 等升级请求头,再检查握手状态码、路由、TLS 和代理读取超时。握手成功后仍被断开,就结合心跳和空闲超时继续排查。
3.8 SSE 或 WebSocket 连接数很多,怎么做横向扩容?
先把长连接管理和业务处理分开。接入层维护客户端连接及用户、连接 ID 的映射,业务服务产生消息后通过消息系统把事件送到持有目标连接的节点,再由该节点推送。在线广播可以使用 Redis Pub/Sub,但它本身不保留离线消息;需要断线补发时,应结合数据库、Redis Streams 或其他可持久化消息系统。还要控制单节点连接数、心跳频率和发送队列,对慢客户端限流或断开,避免内存持续增长。WebSocket 前端可以借助 bufferedAmount 观察待发送数据量。
3.9 SSE 和 WebSocket 的鉴权与跨域有什么区别?
SSE 走 HTTP 请求,跨域读取要遵守 CORS;WebSocket 需要在握手阶段主动验证来源和身份。浏览器原生 EventSource 只提供 URL 和 withCredentials 等有限配置,不能像 fetch 一样自由设置 Authorization 请求头。实际可以使用同源 Cookie、短期令牌或改用 fetch 流式读取。浏览器 WebSocket API 也不能自由设置任意握手头,而且 WebSocket 不依靠普通 CORS 响应机制阻止跨站连接。服务端应检查 Origin 白名单、验证会话或令牌,并对每条敏感业务操作做权限校验。线上传输优先用 HTTPS 和 WSS。
3.10 AI 对话为什么经常用 SSE?
主要因为 AI 文本对话的流式输出以服务端向客户端推送为主,SSE 已经能满足需求,实现和维护也更简单。
用户发送问题后,模型会逐段生成回答,服务端通过持续的 HTTP 响应把内容推送给前端,前端边接收边渲染,用户不用等整段回答生成完。
SSE 基于 HTTP,能够复用现有的鉴权、网关和日志体系,接入成本比较低。原生 EventSource 还支持自动重连。WebSocket 虽然支持双向全双工通信,但对于普通 AI 文本问答,这种能力通常用不上,反而增加了连接管理的复杂度。
所以我一般会选择 SSE。如果涉及实时语音对话、双向音频传输或高频交互,WebSocket 会更合适。
实际项目中,AI 对话通常还需要通过 POST 提交问题和上下文,可以使用 fetch 发起 POST 请求,再读取 SSE 格式的流式响应,不过事件解析、异常处理和重连需要自行实现。
继续追问:用户点击“停止生成”时怎么处理?
如果用 fetch,前端可以通过 AbortController 终止读取;如果用 EventSource,调用 close()。服务端还要感知断开并取消对应生成任务;对于后台独立运行的任务,通常另设取消接口,避免只停止页面显示却继续消耗计算资源。
4. 易混淆概念对比
4.1 SSE 与 WebSocket
| 对比项 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端 → 客户端 | 双向全双工 |
| 通信方式 | 持续的 HTTP 响应流 | 握手后使用 WebSocket 帧通信 |
| 数据形式 | UTF-8 文本事件 | 文本和二进制消息 |
| 浏览器断线恢复 | EventSource 自动尝试重连 |
通常由应用实现重连 |
| 典型场景 | AI 输出、进度、通知 | 聊天、协作、实时互动 |
4.2 SSE 与长轮询
| 对比项 | SSE | 长轮询 |
|---|---|---|
| 一次请求 | 可以连续推送多条事件 | 通常返回一批结果后结束 |
| 后续更新 | 沿同一响应流持续推送 | 客户端再发下一次请求 |
| 浏览器支持 | EventSource 提供标准事件接口 |
基于普通 HTTP 请求自行组织 |
4.3 EventSource 与 fetch 流式读取
| 对比项 | EventSource |
fetch + ReadableStream |
|---|---|---|
| HTTP 方法 | 原生接口使用 GET | 可使用 GET、POST 等 |
| 请求配置 | 配置能力有限 | 可设置请求头、请求体等 |
| 事件解析与重连 | 内置 SSE 事件解析和重连 | 需要自行处理流数据;SSE 格式也要自行解析 |
| 适用情况 | 简单的事件订阅 | 带请求体的 AI 流式调用、定制认证与取消 |
4.4 心跳、重连与消息补发
| 机制 | 解决的问题 | 做不到的事 |
|---|---|---|
| 心跳 | 发现失效连接,降低中间层空闲断开的概率 | 恢复已丢失的业务消息 |
| 重连 | 重新建立传输通道 | 自动找回断线期间的消息 |
| 消息 ID、持久化、补发、去重 | 恢复遗漏消息并避免重复处理 | 单靠协议自动保证端到端恰好一次处理 |
5. 总结
SSE 和 WebSocket 都用于实时消息推送。SSE 基于 HTTP 长时间保持响应流,服务端持续发送文本事件,浏览器的 EventSource 支持自动重连,还能配合事件 ID 做断点恢复,所以很适合 AI 流式输出、进度通知。WebSocket 握手成功后可以双向持续收发文本和二进制消息,适合聊天、在线协作等互动场景,也支持 Ping/Pong 检测连接。两者的长连接都可能断开,自动重连不等于消息不丢;如果业务要求可靠投递,还需要持久化、消息确认和补发机制。