一、技术演进:从轮询到 WebSocket
网页一旦需要'实时',麻烦就开始了:数据在不断变化,用户却只能等下一次刷新。
- 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;
- 再加长轮询,试图把'有了新数据再说'变成一种伪推送,却仍困在请求—响应的笼子里。
- 开发者于是继续前探:让连接不再频繁重建,尝试分块直输,把事件像水一样持续送达,于是有了更顺滑的 Streaming 与标准化的 SSE。
直到某一刻,我们不再满足于'更聪明的单向',而是迈向真正的'同时说话与倾听'——WebSocket 把通信从一次次请求,变成一条持久而通透的通道。此后,HTTP/2、HTTP/3 与 QUIC 又在底层为效率和时延开了绿灯,甚至提供了可选可靠与无序传输的更多可能。
接下来,我们就沿着这条主线,层层展开:它们各自解决了什么、在哪些场景最合拍、又如何在你的系统里形成清晰的选型边界。
1. 从整页刷新到长轮询
这一块是为了解决'整页刷新导致的高延迟与带宽浪费',逐级细化与优化。
a. 早期网页:整页刷新
- 背景与问题:每次更新都整页请求,体验割裂、带宽浪费、延迟高。
- 直接影响:促使前端与服务端思考'只取变化'。
b. 短轮询(Short Polling)
- 解决:改为'隔一段时间拉一次',显著减少整页重载带来的浪费。
- 局限:高频请求带来大量空响应与服务器开销。
- 承接改进:为减少空转,演进到长轮询;同时催生更流式的思路(Streaming/SSE)。
c. 长轮询(Comet/挂起请求)
- 解决:请求挂起,服务器有新数据才返回,接近'伪推送',显著降低空转。
- 局限:本质仍是请求 - 响应;连接频繁重建;难做真正双向。
- 承接改进:
- 单向推送更稳:SSE 标准化单向事件流。
- 若要真双向与二进制:交给 WebSocket。
d. HTTP Streaming(分块传输/持续输出)
- 解决:保持连接,分块持续输出,适合连续文本/事件流,重连更少、延迟更低。
- 局限:多为单向,受代理/中间件影响,兼容性不一。
- 承接改进:单向事件由 SSE 标准化;双向场景仍需 WebSocket。
e. SSE(Server-Sent Events)
- 解决:以标准事件流语义提供单向推送,浏览器原生支持,资源占用低。
- 适配范围:通知、进度、日志/监控等文本或事件流。
- 位置关系:在'只需单向推送'的场景中,SSE 是这一链条的稳定落点,而非过渡技术。
2. 范式跃迁:WebSocket
这不是前面链条的'又一改良',而是从请求 - 响应转向全双工持久连接的范式变化。
WebSocket(全双工、持久、低开销)
- 解决:真正的双向实时通信,降低握手与头部开销,支持文本与二进制,端到端延迟低。
- 典型场景:聊天、协同编辑、在线游戏、行情推送、IoT。
- 与上一链条的关系:
- 在'需要双向实时'的主战场,实质上取代了短轮询/长轮询等过渡方案。
- 与 SSE 并存:若只有单向通知/事件流,SSE 更简单更省资源;若需要双向或二进制,WebSocket 更合适。
- 运维关注:连接状态管理、容量与反压、企业代理/负载均衡兼容。
3. 底座升级与新选项: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,以便后续重用。
示意代码(精简):
const ws = 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 的处理结果 (核心验证逻辑):- 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接; - 对拼接后的字符串进行
SHA-1哈希计算; - 将哈希结果转为 Base64 编码,即为 Sec-WebSocket-Accept 的值;
- 客户端会验证该值是否正确,若不正确则拒绝建立连接 (防止伪造响应)。
- 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串
3) 进入通信阶段(双向数据 + 基础发送)
握手通过,onopen 会触发。此时做两件事:
- 发送初始化数据(如身份、订阅主题)
- 启动心跳(下一步会讲)
通信注意:
onmessage既可能是字符串,也可能是二进制(Blob/ArrayBuffer)bufferedAmount可用来做背压控制(积压太大时暂停继续 send)
4) 启动心跳(让连接'活着'且可感知)
持久连接会遭遇 Wi‑Fi 抖动、防火墙清理等问题。心跳=周期性发 ping,超时未收到 pong 就判死链。
推荐参数(可按业务调优):
HEARTBEAT_INTERVAL ≈ 30sHEARTBEAT_TIMEOUT ≈ 10s
实操要点:
- 启动前先清理旧定时器,避免重复
- 收到 pong 立即清除超时定时器
- onclose/onerror 必须停止心跳
5) 异常→重连(恢复连接但不过载)
一旦 onerror、onclose(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. 如何保证消息不丢、不重、按序?
- 不丢: 消息先落到能持久化、带副本确认的总线里(像'写盘且多副本到位才算成功'),写失败就退避重试;消费侧是'先送到用户手里或进可靠下行队列,再更新位点',断线后拿着
resumeToken+offset从保留的历史里把漏掉的补回来。 - 不重: 每条消息都有一个不会撞车的'指纹'(messageId 或 topic-partition-offset);网关用一小块内存做近端去重,只有第一次真正写入才前进位点,重复的一概忽略;客户端也按同一指纹做幂等处理,避免业务状态被二次改动。
- 按序: 把需要有序的对象(userId/roomId)哈希到同一分区,借用分区内天然顺序;同一个键在网关里串行发送、同队列内重试,不跨分区不并行穿插,这样即使重试和补发也不会把顺序打乱。
4. 心跳如何设计?超时如何判定?
这里的心跳,目标是 '保活、探测、可平滑重连'。
- 不失联: 用应用层
ping/pong,客户端主发、服务端回;- 间隔 20–60s,加±10% 抖动
- 未知网络时取 20–30s,确保小于最短 NAT 空闲回收。
- 怎么判死: 别一跳不回就拍板。记录
lastSeen,允许 2–3 次心跳未达或now-lastSeen超过 2–3 个周期再判断;进入'Suspect'时降级写入,仍有业务流量即立刻恢复。 - 断了咋办: 重连走指数退避并带抖动,携带
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/队列长度等指标与事件,便于快速定位长尾与故障。

