前端实时推送 & WebSocket 面试题(2026版)

一、历史背景 + 时间轴

网页一旦需要 "实时" ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新;

  • 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;
  • 再加长轮询,试图把"有了新数据再说"变成一种伪推送,却仍困在请求---响应的笼子里。
  • 开发者于是继续前探:让连接不再频繁重建,尝试分块直输,把事件像水一样持续送达,于是有了更顺滑的 Streaming 与标准化的 SSE。

直到某一刻,我们不再满足于"更聪明的单向",而是迈向真正的"同时说话与倾听"------WebSocket 把通信从一次次请求,变成一条持久而通透的通道。此后,

  • HTTP/2、HTTP/3 与 QUIC 又在底层为效率和时延开了绿灯,甚至提供了可选可靠与无序传输的更多可能。

接下来,我们就沿着这条主线,层层展开:它们各自解决了什么、在哪些场景最合拍、又如何在你的系统里形成清晰的选型边界。

01|从整页刷新出发:减少浪费的一条链路

这一块是为了解决"整页刷新导致的高延迟与带宽浪费",逐级细化与优化。

a. 早期网页:整页刷新

  • 背景与问题:每次更新都整页请求,体验割裂、带宽浪费、延迟高。
  • 直接影响:促使前端与服务端思考"只取变化"。

b. 短轮询(Short Polling)为解决整页刷新的低效

  • 解决:改为"隔一段时间拉一次",显著减少整页重载带来的浪费。
  • 局限:高频请求带来大量空响应与服务器开销。
  • 承接改进:为减少空转,演进到长轮询;同时催生更流式的思路(Streaming/SSE)。

c. 长轮询(Comet/挂起请求)为减少短轮询的空转

  • 解决:请求挂起,服务器有新数据才返回,接近"伪推送",显著降低空转。

  • 局限:本质仍是请求-响应;连接频繁重建;难做真正双向。

  • 承接改进:

    • 单向推送更稳:SSE 标准化单向事件流。
    • 若要真双向与二进制:交给 WebSocket(见独立块)。

d. HTTP Streaming(分块传输/持续输出)为进一步降低重连与延迟

  • 解决:保持连接,分块持续输出,适合连续文本/事件流,重连更少、延迟更低。
  • 局限:多为单向,受代理/中间件影响,兼容性不一。
  • 承接改进:单向事件由 SSE 标准化;双向场景仍需 WebSocket。

e. SSE(Server-Sent Events)单向推送的标准化终点

  • 解决:以标准事件流语义提供单向推送,浏览器原生支持,资源占用低。
  • 适配范围:通知、进度、日志/监控等文本或事件流。
  • 位置关系:在"只需单向推送"的场景中,SSE 是这一链条的稳定落点,而非过渡技术。

02|范式跃迁:WebSocket(独立大块)

这不是前面链条的"又一改良",而是从请求-响应转向全双工持久连接的范式变化。

WebSocket(全双工、持久、低开销)

  • 解决:真正的双向实时通信,降低握手与头部开销,支持文本与二进制,端到端延迟低。

  • 典型场景:聊天、协同编辑、在线游戏、行情推送、IoT。

  • 与上一链条的关系:

    • 在"需要双向实时"的主战场,实质上取代了短轮询/长轮询等过渡方案。
    • 与 SSE 并存:若只有单向通知/事件流,SSE 更简单更省资源;若需要双向或二进制,WebSocket 更合适。
  • 运维关注:连接状态管理、容量与反压、企业代理/负载均衡兼容。

03|底座升级与新选项:HTTP/2·HTTP/3·QUIC 家族

这部分不是替代前两块,而是提供更高效的承载与更灵活的传输语义。

WS over H2/H3

  • 价值:与同域请求复用连接、更好穿透与效率、更低握手成本。
  • 作用:让 WebSocket 的部署与网络效率更优。

WebTransport(基于 QUIC)

  • 价值:可选可靠与有序/无序、更低延迟,适合实时媒体、游戏、定制协议。
  • 关系:不是取代 WebSocket/SSE 的通吃方案,而是当你需要"更细粒度的可靠性与顺序控制"时的新工具。

二、速查表

实时推送的目标是"低延迟、双向或单向地把数据从服务端送到客户端"。主流技术选型包括:

三、WebSocket 核心定义(重要)

WebSocket 是 HTML5 推出的一种全双工(Full-Duplex)、持久化(Persistent)的网络通信协议, 基于TCP协议构建,允许客户端(浏览器)与服务器之间建立一条长期稳定的连接通道,实现「服务器主动向客户端推送数据」和「客户端实时向服务器发送数据」的双向通信,无需频繁建立/断开连接。

其核心特点可概括为:

  • 全双工:通信双方可同时发送/接收数据(区别于HTTP的「请求-响应」单向通信);
  • 持久连接:连接建立后长期保持,避免HTTP每次通信都需重新握手的开销;
  • 轻量协议:数据帧头部信息简洁(仅2-14字节),传输效率远高于HTTP;
  • 协议标识:客户端发起连接时使用ws://(非加密)或wss://(加密,基于TLS,类似HTTPS)作为协议前缀。

从零开始的完整流程

下面是一条你在前端真实会走的链路:创建连接 → HTTP 握手与协议切换 → 进入 WebSocket 双向通信 → 启动心跳检测 → 发现异常并重连 → 重连成功后的补偿 → 服务端跨域放行 → 正常/异常关闭。


1) 创建连接(入口)

你写下第一行代码时,要解决两件事:用对协议前缀、绑定好事件。

  • 如果网页是 HTTPS 必须用 wss://HTTP 页面用 ws://
  • 立刻绑定 onopen/onmessage/onerror/onclose,以便后续重用。

示意代码(精简):

  • new WebSocket(url)
  • 绑定事件:initWebSocketEvents(ws)

2) HTTP 握手与协议切换(从"请求"到"长连")

客户端(浏览器) 创建 WebSocket 实例时,会发起一个特殊的 HTTP - GET,核心目的是 「请求将协议从HTTP升级为WebSocket」 。服务端验证通过后返回 101,双方切换到 WebSocket 帧通信。

WebSocket 帧是双向通信中的最小传输结构,携带 " 数据类型 是否为消息的最后一段 负载长度 掩码/密钥 实际数据 " 。消息可以被拆成多帧连续发送,也可以一个帧就送完。

客户端发起「协议升级请求」(HTTP - GET 请求)

请求头中关键字段(面试高频考点):

  • GET /ws-endpoint HTTP/1.1:请求方法为 GET,路径为服务器的 WebSocket 端点(如/ws);
  • Host:example.com:服务器域名;
  • Upgrade:websocket:核心字段,告知服务器「要升级协议为 WebSocket」;
  • Connection:Upgrade:配合 Upgrade,表示「这是一个协议升级请求」;
  • Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jzQ==:客户端生成的随机字符串(Base64 编码,长度 16 字节),用于服务器验证(防止恶意连接);
  • Sec-WebSocket-Version:13:WebSocket 协议版本(当前主流为 13,需服务器支持);
  • Sec-WebSocket-Origin:https://example.com:客户端所在域名(用于服务器跨域验证)。

服务器响应「协议升级成功」(HTTP - 101状态码)

服务器收到请求后,若支持 WebSocket 协议且验证通过(如 Sec-WebSocket-Key 验证、跨域验证) ,会返回HTTP - 101(SwitchingProtocols)状态码,表示「同意协议升级」。

响应头中关键字段 (面试高频考点):

  • HTTP/1.1 101 Switching Protocols:101 状态码是协议切换的标志;

  • Upgrade: websocket:确认升级为WebSocket 协议;

  • Connection:Upgrade:确认连接用于协议升级;

  • Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=:服务器对Sec-WebSocket-Key 的处理结果(核心验证逻辑):

    1. 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
    2. 对拼接后的字符串进行 SHA-1 哈希计算;
    3. 将哈希结果转为 Base64 编码,即为 Sec-WebSocket-Accept 的值;
    4. 客户端会验证该值是否正确,若不正确则拒绝建立连接(防止伪造响应)。

3) 进入通信阶段(双向数据 + 基础发送)

握手通过,onopen 会触发。此时做两件事:

  • 发送初始化数据(如身份、订阅主题)
  • 启动心跳(下一步会讲)

通信注意:

  • onmessage 既可能是字符串,也可能是二进制(Blob/ArrayBuffer)
  • bufferedAmount 可用来做背压控制(积压太大时暂停继续 send)

4) 启动心跳(让连接"活着"且可感知)

持久连接会遭遇 Wi‑Fi 抖动、防火墙清理等问题。心跳=周期性发 ping,超时未收到 pong 就判死链

推荐参数(可按业务调优):

  • HEARTBEAT_INTERVAL ≈ 30s
  • HEARTBEAT_TIMEOUT ≈ 10s

实操要点:

  • 启动前先清理旧定时器,避免重复
  • 收到 pong 立即清除超时定时器
  • onclose/onerror 必须停止心跳

5) 异常→重连(恢复连接但不过载)

一旦 onerroronclose(code ≠ 1000) 或心跳判定超时,进入重连。

目标是:能恢复、不过载、可被用户停止。

策略三件套:

  • 指数退避:1s → 2s → 4s ... 最多 30s
  • 次数上限:如 10 次(达到即停)
  • 可控停止:提供"停止重连"或页面关闭时停

补偿机制:

  • 在断开前缓存"待发送"数据(例如未发出的聊天消息)
  • 重连成功后按序补发,确保业务连续性

6) 服务器放行跨域(握手能否过关的关键)

虽然 WebSocket 原生"支持跨域",但握手是 HTTP,服务端需要对 Sec-WebSocket-Origin 做白名单校验。

否则会 403 或直接关闭。

  • Node.js(ws)

    • 读取 req.headers['sec-websocket-origin']
    • 不在 allowedOrigins 列表:关闭 1008 "Cross-origin access denied"
  • Spring Boot

    • registry.addHandler(...).setAllowedOrigins("https://a.com", "https://b.com")
    • 需要时 .withSockJS()提供降级

7) 正常关闭与资源清理(善始善终)

  • 用户离开页面或主动退出:ws.close(1000, '用户主动退出')
  • onclose 中停止心跳与重连,清空定时器与队列,避免泄漏
  • 记录关闭原因码:1000 正常、1006 常见于异常/心跳超时

四、面试题

面试关注点通常围绕"协议对比、连接管理、消息语义、可靠性与扩展性、安全与运维成本"。

1、WebSocket 与 SSE 的差异与使用场景,HTTP轮询呢?

WebSocket(全双工,二进制/文本)

  • 适用:即时聊天、协作编辑、游戏状态同步、行情推送、需要客户端→服务端主动上行的实时交互。
  • 优点:低延迟、头开销小、全双工、支持二进制、可自定义子协议。
  • 注意:需处理心跳、重连、背压、鉴权与扇出扩展;代理/LB 要正确透传 Upgrade 和超时设置。

Server-Sent Events(SSE,单向 server→client)

  • 适用:通知流、日志流、监控事件、流式生成文本(如增量输出)、只需服务端下行的实时更新。
  • 优点:浏览器原生 EventSource、文本流、自动重连、支持 Last-Event-ID 断点续传;实现简单。
  • 注意:单向、仅文本(可 base64 二进制)、连接数限制与代理超时需要关注;移动端网络切换要做容错。

HTTP 轮询/长轮询(兼容兜底)

  • 适用:对实时性要求不高的小流量场景、受网络环境或企业防火墙限制无法使用 WS/SSE 时的兜底。
  • 优点:最易落地、与缓存/鉴权/监控体系天然兼容;对中间设备最友好。
  • 注意:延迟更高、资源利用低;高频轮询会带来成本与限流压力。

✅ 重点

  • WebSocket 通过 HTTP/1.1 Upgrade → 101 完成切换,此后是帧协议的全双工通道;Keep-Alive 仅是复用 TCP,不改变 HTTP 的请求-响应语义。
  • 选型规则:需要双向实时交互选 WebSocket;单向事件流选 SSE;受限或低实时性场景用轮询作兜底。

2、如何设计一个可水平扩展的实时推送系统?

在可水平扩展的实时推送系统中,WebSocket 连接会分布在多台网关节点上。

核心挑战是如何在连接与消息不在同一台机器时,仍能把消息快速路由到正确的连接。可行的范式是

  • 网关层负责连接
  • 管道层负责路由与发布订阅
  • 存储层负责状态与回放

🔌 网关层(负责连接)

  • 终止 TLS/WS,维持心跳与速率限制,保持无状态实例。
  • 建立本地索引:connectionId → 订阅集合,userId → connectionIds。
  • 将连接元数据上报共享存储:connectionId、userId、nodeId、订阅、最近心跳。
  • 仅订阅"与自己有关的分片":按 userId/topic 的哈希分片从管道层拉取,减少无关扇出。
  • 写通道背压与优先级:控制帧/关键消息优先,低优先级可丢尾或抽样。

🚇 管道层(负责路由与发布订阅)

  • 选型具备分片与回放能力的总线(Kafka/Pulsar/NATS/Redis Streams)。

  • 分片策略

    • 点推:按 userId/connectionId 一致性哈希到分区,保证单用户局部有序。
    • 主题推送:按 topic 分区,网关本地再做订阅过滤与扇出。
  • 路由方式

    • 生产者只需写对的分片;总线按分区把消息送到订阅该分片的网关。
    • 广播/超大房间采用"分层扇出":先到分片,再由各网关本地扇出,必要时加中间扇出代理。
  • 去重与幂等:messageId 或 (topic, partition, offset) 作为幂等键,网关/客户端各自维护短期去重集合。

🗃️ 存储层(负责状态与回放)

  • 会话与订阅状态:使用 Redis Cluster 或 KV 服务存 userId→connectionIds、connectionId→nodeId、订阅清单、心跳时间。
  • 游标与回放:在总线层保留 offset;客户端重连携带 resumeToken,网关据此恢复订阅并按 offset 增量补发。
  • 一致性与更新:订阅变更写事件流,相关网关消费后刷新本地索引;用版本号/逻辑时钟避免乱序覆盖。

3、如何保证消息不丢、不重、按序?

  1. 不丢: 消息先落到能持久化、带副本确认的总线里(像"写盘且多副本到位才算成功"),写失败就退避重试;消费侧是"先送到用户手里或进可靠下行队列,再更新位点",断线后拿着 resumeToken+offset 从保留的历史里把漏掉的补回来。
  2. 不重: 每条消息都有一个不会撞车的"指纹"(messageId 或 topic-partition-offset);网关用一小块内存做近端去重,只有第一次真正写入才前进位点,重复的一概忽略;客户端也按同一指纹做幂等处理,避免业务状态被二次改动。
  3. 按序: 把需要有序的对象(userId/roomId)哈希到同一分区,借用分区内天然顺序;同一个键在网关里串行发送、同队列内重试,不跨分区不并行穿插,这样即使重试和补发也不会把顺序打乱。

4、心跳如何设计?超时如何判定?

这里的心跳,目标是 "保活、探测、可平滑重连"。

  1. 不失联: 用应用层 ping/pong,客户端主发、服务端回;

    1. 间隔 20--60s,加±10%抖动
    2. 未知网络时取 20--30s,确保小于最短 NAT 空闲回收。
  1. 怎么判死: 别一跳不回就拍板。记录 lastSeen允许 2--3 次心跳未达或 now-lastSeen 超过 2--3 个周期再判断 ;进入"Suspect"时降级写入,仍有业务流量即立刻恢复。
  2. 断了咋办:重连走指数退避并带抖动,携带 resumeToken/offset 补发;移动端切网优先复用会话,失败再重建。监控 RTT/丢包与 Suspect 比例,自动调心跳与阈值。

5、如何在 Nginx/Envoy 反向代理后稳定运行 WebSocket?

核心思路:让代理"不瞎操心"、连接"常被看见"、后端"可续上"。

  • 代理设置:开启 WebSocket 升级;调大超时,禁用缓冲与压缩;保持 TCP keepalive,HTTP/2 用 CONNECT(H2/WebSocket)。
  • 心跳与保活:应用层 ping/pong 20--30s(±10%抖动),保证小于代理/NAT空闲回收;大连接数用轻量负载均衡(hash by userId/roomId)避免跨节点迁移。
  • 断线与重连:客户端指数退避+抖动,带会话 token/offset 续传;后端幂等去重,重放不重不丢。
  • 运维与观测:开代理层指标(升级成功率、idle 关闭数、5xx)、RTT/丢包与重连率,异常时自适应缩短心跳或放宽超时。

6、如何做鉴权与权限隔离?

握手前校验 JWT;Subprotocol 指定租户/版本;频道级 ACL;避免敏感数据从客户端请求非授权频道。

  • 握手前校验 JWT :在 HTTP Upgrade 前验证 iss/aud/exp/签名并解析 tenant_id/user_id/scopes,避免建立长连后再踢。
  • Subprotocol 指定租户/版本:用 Sec-WebSocket-Protocol 携带 tenant 和策略版本做白名单匹配,确保连接上下文一致。
  • 频道级 ACL:频道强制以租户前缀命名,每次 subscribe/publish 依据 RBAC+scope 前缀(到资源或前缀)做服务端授权。
  • 避免敏感数据越权:仅按服务器维护的"已授权订阅集合"下发数据,忽略客户端自报筛选请求并拒绝未授权频道。

7、如何评估性能与成本?

  • 每连接内存占用: 用基线压测量出 MB/1k 连接的实际占用,结合目标并发外推单机上限并监控 GC/碎片。
  • 每秒消息数(fanout×频率): 用发布频率×平均扇出得到总吞吐,核算带宽与发送队列容量,识别热点频道放大效应。
  • 尾延迟 P95/P99: 持续跟踪端到端延迟长尾并关联队列深度与CPU/GC事件,确保在SLA红线下仍稳定。
  • 压测考虑广播峰值与重连风暴: 分别模拟大扇出瞬时广播与大量短时间内握手重连,验证背压、限速和握手路径的韧性。

8、遇到"重连风暴"怎么处理?

  • 抖动退避(指数退避 + 随机抖动): 客户端按指数退避间隔重试并加入随机抖动,避免同相位同时重连造成尖峰。
  • 分批恢复: 将连接恢复按固定批次/时间片发放(如每 100ms 开放 N 个),把尖峰摊平到更长窗口。
  • 服务端限流与排队: 在握手与认证路径设置并发/速率上限与队列,超限直接返回可重试错误或延迟令牌。
  • 灰度放量: 按租户/区域/版本逐步提升允许重连比例,结合健康度与错误率自动调节放量速度。

9、前端如何封装一个健壮的 WebSocket 客户端?

  • 状态机( CONNECTING / OPEN / CLOSING / CLOSED ): 用有限状态机驱动所有事件与迁移,单航道控制避免并发重连与回调竞态。
  • 心跳/重连策略: 按固定心跳探活与半开检测,重连采用指数退避叠加随机抖动并设上限与冷却期。
  • 消息序列化: 统一 envelope(type/id/ts/payload),默认 JSON,性能敏感时切 Protobuf/MessagePack 并保持向后兼容。
  • 离线缓存与去重: 未连通时将待发入队、跨刷新用 IndexedDB,按 seq/uuid 去重并用 last-seq 做断点续传。
  • 可观测日志: 记录连接尝试/关闭码/重连次数/心跳RTT/队列长度等指标与事件,便于快速定位长尾与故障。
相关推荐
JefferyXZF2 小时前
新手建站零门槛!Vercel+Cloudflare+Namesilo域名购买部署全流程
前端
yinuo2 小时前
微信浏览器缓存机制大揭秘:为什么你总刷不出新页面?
前端
拉不动的猪2 小时前
try...catch 核心与生态协作全解析
前端·javascript·vue.js
Xeon_CC2 小时前
在react-app-rewired工程项目中,调试AntVG6库源码包。
前端·react.js·前端框架
o***Z4483 小时前
前端无障碍开发检查清单,WCAG合规
前端
J***Q2923 小时前
前端CSS架构模式,BEM与ITCSS
前端·css
G***T6914 小时前
React性能优化实战,避免不必要的重渲染
前端·javascript·react.js
q***d1734 小时前
前端微前端部署方案,Nginx与Webpack
前端·nginx·webpack
y***54884 小时前
前端构建工具扩展,Webpack插件开发
前端·webpack·node.js