
1. 面试官到底想考你什么面到 WebSocket 这个题很多人第一反应是“我会用啊new WebSocket(url)然后onmessage收消息不就完了”。但真正在面试里被追问三层之后能答利索的人其实不多。我这些年带过不少候选人也帮朋友做过模拟面试发现一个规律面试官问 WebSocket从来不是想听你背 API而是想通过这一个点判断你对“实时通信”这件事的理解深度。它是一道典型的“一个点撬动一整片知识网”的题。往浅了问是握手、帧格式、心跳往深了问是 TCP 粘包、HTTP 升级、代理穿透、断线重连、消息可靠性和幂等再往工程上问就是集群广播、连接数上限、内存占用、压测方案。所以这篇文章我不打算写成一份 API 手册而是按面试的真实追问路径把每一层该说什么、为什么这么说、哪些坑必须提前知道全部摊开讲清楚。适合谁看准备中高级后端或前端面试的同学、正在做 IM 或实时推送业务的开发者、以及被“为什么不用轮询”“为什么需要心跳”这类问题问懵过的人。我会尽量用大白话把底层原理讲透同时给出可以直接抄的代码和参数让你不光能答还能落地。先说一个我自己的判断WebSocket 的面试价值80% 集中在“连接生命周期管理”和“可靠性设计”这两块。协议本身 RFC 6455 写得清清楚楚背下来不难难的是你知不知道线上跑起来会遇到什么。下面我按四个大块展开基本覆盖了面试官能问到的所有方向。2. 协议本质与握手细节拆解2.1 为什么是 101 而不是 200一次完整的升级握手WebSocket 最容易被问的第一个问题就是“它和 HTTP 什么关系”标准答案是WebSocket 借用了 HTTP 的握手过程来完成协议升级升级完成后就彻底脱离 HTTP跑在独立的 TCP 连接上。这句话里有两个得分点一个是“借用握手”一个是“脱离 HTTP”。具体握手长这样。客户端先发一个普通的 HTTP 请求但带上几个特殊头GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务端如果同意升级返回101 Switching ProtocolsHTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo这里面试官最爱追问的是Sec-WebSocket-Accept怎么算的。答案是把客户端传来的Sec-WebSocket-Key拼上一个固定的 GUID 字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11做一次 SHA-1 哈希再 Base64 编码。这个 GUID 是协议写死的作用是防止普通的 HTTP 缓存或代理误把升级请求当成普通请求处理。为什么是 101 不是 200因为 200 表示“请求成功响应体给你”而 101 表示“我同意切换协议接下来的字节流不再按 HTTP 解析”。这是 HTTP/1.1 里专门为协议升级预留的状态码。你如果能顺口说出“101 是 Upgrade 机制的标准响应码除了 WebSocketHTTP/2 的 h2c 升级也用它”面试官基本会认为你读过协议。注意很多人以为握手成功后连接就“变成 WebSocket 了”其实更准确的说法是——同一条 TCP 连接上双方约定后续用 WebSocket 帧格式通信。TCP 连接自始至终没变变的只是应用层的解析规则。2.2 帧格式为什么 WebSocket 能省流量面试里第二个高频追问是“WebSocket 相比轮询到底省在哪”很多人只会说“它是长连接不用反复建连”。这个答案对但不够。真正的省流量在于帧头极小。WebSocket 的数据帧头部最小只有 2 字节。我把它拆开讲第 1 字节FIN 位1 bit RSV 保留位3 bit 操作码 opcode4 bit第 2 字节MASK 位1 bit payload 长度7 bit如果 payload 长度是 126后面跟 2 字节扩展长度是 127跟 8 字节扩展长度如果 MASK 位为 1后面跟 4 字节掩码 keyopcode 决定了这一帧是什么类型0x1是文本0x2是二进制0x8是关闭0x9是 ping0xA是 pong。ping/pong 就是心跳的协议级实现这个后面会重点讲。对比一下一个 HTTP 请求光请求头随便就几百字节而 WebSocket 发一条 “hi”帧头 2 字节 掩码 4 字节 数据 2 字节总共 8 字节。高频小消息场景下这个差距是数量级的。所以面试时你可以这样总结WebSocket 省的不是建连开销而是每一条消息的协议开销。还有一个细节值得说客户端发往服务端的数据帧必须加掩码MASK1服务端发往客户端的不加。为什么协议设计者的初衷是防止恶意脚本通过 WebSocket 发起对代理缓存的污染攻击。这个点能答出来属于加分项。2.3 和 HTTP/2、SSE 的取舍面试官想看你有没有选型思维现在面试官越来越喜欢问“有了 HTTP/2 和 SSE为什么还要用 WebSocket”这题考的是选型能力不是让你踩一捧一。我的回答框架是这样的方案通信方向协议开销适用场景短轮询单向拉高兼容性要求极高、实时性要求低长轮询单向拉中兼容老环境、消息频率低SSE服务端单向推低行情、日志、通知等单向流HTTP/2多路复用低请求响应模型为主WebSocket全双工极低IM、协作、游戏、实时控制关键结论如果只需要服务端推、客户端不怎么发SSE 更简单还能自动重连如果需要双向高频交互WebSocket 才是正解。HTTP/2 虽然有多路复用但它的语义还是请求-响应服务端主动推的能力Server Push已经被主流浏览器废弃了所以替代不了 WebSocket。我踩过的一个坑早期项目用 SSE 做聊天结果客户端发消息还得另开一个 POST 接口两条链路状态不同步用户看到“消息发出去了但对方没收到”的诡异现象。后来换成 WebSocket 一条链路问题自然消失。这个经历我在面试里讲过面试官明显更感兴趣——因为它是真实工程判断不是背书。3. 连接生命周期与心跳机制3.1 从建立到关闭一条连接的完整旅程面试官很爱画一条时间线让你讲连接的生命周期。我建议按这五个阶段说握手 → 鉴权 → 就绪 → 心跳维持 → 关闭。握手刚才讲过了。鉴权是工程上绕不开的一环WebSocket 握手是 HTTP 请求所以可以带 Cookie、Header、URL 参数。但浏览器原生的WebSocket构造函数不支持自定义 Header这是个大坑。常见做法有三种用 URL query 带 token比如wss://host/ws?tokenxxx用 Cookie同域下自动携带但有 CSRF 风险先 HTTP 登录拿一次性 ticket再用 ticket 建连我一般推荐第三种因为 token 放 URL 里容易被日志记录放 Cookie 又有跨站风险。一次性 ticket 用完即焚安全性最好。就绪之后进入心跳维持阶段这是重点下一节展开。关闭阶段要注意关闭是双向的。主动关闭方发一个 opcode 为0x8的 Close 帧带上关闭码对方回一个 Close 帧然后 TCP 四次挥手。关闭码里1000是正常关闭1001是对方离开1006是异常断开这个码不会出现在帧里是本地推断的。实操心得很多线上“连接假死”问题根源就是没处理好关闭握手。比如服务端重启时直接 kill 进程客户端收到的是 1006如果不做重连就会一直挂着。所以客户端必须监听onclose并区分关闭码决定是否重连。3.2 心跳到底解决什么问题别只会说“保活”“为什么要心跳”——这是 WebSocket 面试的必考题也是最容易答得平庸的一题。大部分人答“保持连接活跃”这个答案只能拿 60 分。心跳真正解决的是三类问题第一类中间设备的空闲连接回收。运营商网关、负载均衡、NAT 设备都会维护连接状态表长时间没有数据流动的连接会被静默丢弃。这个超时时间通常是 60 秒到几分钟不等。心跳的作用就是定期“喂”一点数据让中间设备认为这条连接还活着。第二类检测半开连接。这是最隐蔽的问题。当网络异常比如拔网线、地铁进隧道时TCP 连接的两端可能都不知道对方已经不可达。TCP 本身有 KeepAlive但默认间隔是 2 小时完全不够用。应用层心跳能在秒级发现连接已死。第三类快速感知服务端故障。服务端挂了客户端如果只靠 TCP 感知可能要等很久。心跳超时能让你在几秒内触发重连。那心跳怎么做两种方案协议级用 WebSocket 自带的 ping/pong 帧。服务端发 ping客户端自动回 pong浏览器会自动回不用写代码。应用级自己定义{type:ping}这样的消息双方约定好。浏览器端有个限制JS 没法主动发 ping 帧只能被动回 pong。所以如果要在浏览器里做心跳通常用应用级方案由客户端定时发ping服务端回pong。参数怎么定我的经验值const HEARTBEAT_INTERVAL 30000; // 30秒发一次 const HEARTBEAT_TIMEOUT 10000; // 10秒没收到pong就判定断开为什么是 30 秒因为大多数中间设备的空闲超时在 60 秒以上30 秒发一次能留出安全余量。为什么超时是 10 秒因为要给网络抖动留空间太短会误判太长重连不及时。这个参数不是拍脑袋是根据中间设备超时时间反推出来的面试时把推导过程说出来比直接报数字强十倍。3.3 断线重连指数退避不是随便写的断线重连是工程能力的试金石。我见过太多项目写个setInterval(reconnect, 1000)就完事结果服务端一挂几千个客户端每秒重连一次直接把服务端打垮——这就是所谓的“惊群效应”。正确做法是指数退避 随机抖动function getReconnectDelay(attempt) { const base 1000; const max 30000; const delay Math.min(base * Math.pow(2, attempt), max); // 加随机抖动避免同时重连 const jitter Math.random() * 1000; return delay jitter; }第一次 1 秒第二次 2 秒第三次 4 秒……封顶 30 秒。为什么要加抖动因为如果一万个客户端同时断线它们会同时重连形成脉冲。加了随机抖动后重连请求被摊平到一段时间内服务端压力就平滑了。还有一个细节重连成功后要重新鉴权和重新订阅。很多人重连只做了建连忘了恢复订阅关系导致用户重连后收不到消息。这个坑我在一个协作编辑项目里踩过排查了半天才发现是重连后没重新 join 房间。提示重连次数要有上限比如连续失败 10 次后进入“慢速重试”模式比如每 5 分钟试一次并给用户一个“网络异常点击重试”的提示。无限快速重连是灾难。4. 可靠性设计与消息投递4.1 消息会不会丢TCP 保证不了端到端可靠这是面试里区分度最高的一题“WebSocket 基于 TCPTCP 是可靠的那消息是不是就不会丢”答案是TCP 只保证传输层可靠不保证应用层可靠。中间任何一环出问题消息都可能丢连接断开瞬间正在发送的消息丢了服务端处理到一半崩溃消息没落库客户端收到消息但处理失败没有反馈所以要做应用层的可靠性设计核心是三件事ACK 确认、消息去重、离线补偿。ACK 机制每条消息带一个唯一 ID接收方处理成功后回一个 ACK。发送方维护一个“待确认队列”超时没收到 ACK 就重发。这跟 TCP 的重传思路一样但作用在应用层。消息去重因为会重发接收方可能收到重复消息所以要用消息 ID 做幂等。服务端一般用 Redis 存最近处理过的消息 ID或者数据库唯一索引兜底。离线补偿用户不在线时消息要存起来等用户上线后拉取。这里的关键是用递增的序列号seq来标记消息顺序客户端上报自己收到的最大 seq服务端返回之后的所有消息。-- 消息表设计示例 CREATE TABLE messages ( id BIGINT PRIMARY KEY, seq BIGINT NOT NULL, -- 会话内递增序列号 from_uid BIGINT, to_uid BIGINT, content TEXT, created_at TIMESTAMP, INDEX idx_to_seq (to_uid, seq) );面试时如果你能说出“用 seq 而不是时间戳来保证顺序”面试官会眼前一亮。因为时间戳在分布式环境下可能重复或乱序而 seq 是单调递增的天然有序。4.2 消息顺序为什么时间戳靠不住接着上面的点展开。面试官可能追问“怎么保证消息顺序”先说结论单条连接内WebSocket 天然有序因为 TCP 保证字节流有序。但跨连接、跨服务端实例就不一定了。问题场景用户 A 的消息经过负载均衡可能落到服务端实例 1用户 B 的消息落到实例 2。如果两个实例各自生成时间戳时钟不同步就会乱序。即使时钟同步毫秒级并发下也可能相同。解决方案是用逻辑序列号。每个会话维护一个递增计数器消息落库时分配 seq。客户端按 seq 排序展示。这个计数器可以用 Redis 的INCR实现也可以用数据库自增。实操心得seq 的分配一定要在消息持久化的同一个事务里否则会出现“seq 分配了但消息没存进去”的空洞。我见过用 Redis INCR 分配 seq、然后异步写库的方案结果 Redis 挂了重启后 seq 回退消息全乱。稳妥做法是数据库自增或带持久化的序列服务。4.3 集群广播单机连接数上不去怎么办单机 WebSocket 连接数是有上限的。理论上受文件描述符、内存、CPU 限制实践中一台 4 核 8G 的机器优化得当能撑几万到十几万连接。但业务量一大必须上集群。集群带来的核心问题是用户 A 连在实例 1用户 B 连在实例 2A 给 B 发消息实例 1 怎么把消息送到实例 2常见方案有三种Redis Pub/Sub每个实例订阅一个频道消息广播到所有实例各实例检查目标用户是否在自己这里。简单但 Redis 挂了就全挂且消息不持久。消息队列如 Kafka消息发到 MQ各实例消费。可靠但延迟略高。服务注册 定向转发维护“用户 → 实例”的路由表直接转发到目标实例。效率高但路由表维护复杂。我一般推荐中小规模用 Redis Pub/Sub大规模用 MQ。面试时把三种方案的取舍讲清楚比只报一个方案强。这里还有个隐藏考点连接数上不去先别急着上集群先看单机优化。比如调大文件描述符限制ulimit -n 100000调优 TCP 参数比如net.ipv4.tcp_tw_reuse用 epoll 多路复用Netty、Go 的 netpoll 都帮你做了减少每条连接的内存占用比如及时释放缓冲区5. 高频追问与排查实录5.1 面试高频问题速查表我把这些年被问过、也被别人问过的 WebSocket 问题整理成一张表按难度分级方便你自查。难度问题回答要点基础WebSocket 和 HTTP 区别全双工 vs 请求响应101 升级帧格式基础握手过程Upgrade 头、Sec-WebSocket-Key、Accept 计算进阶为什么需要心跳中间设备回收、半开连接、故障感知进阶断线重连怎么做指数退避 抖动 重新鉴权订阅进阶消息可靠性ACK、去重、seq 顺序、离线补偿高级集群方案Redis Pub/Sub、MQ、路由表取舍高级单机连接数优化fd 限制、TCP 调优、内存控制高级如何压测连接数、消息吞吐、延迟分位数5.2 线上问题排查我踩过的三个真实坑坑一连接数上不去报 “too many open files”。这是最经典的。Linux 默认单进程文件描述符限制是 1024每条 WebSocket 连接占一个 fd超过就报错。解决ulimit -n调大同时改/etc/security/limits.conf持久化。注意系统级限制和进程级限制都要改只改一个没用。坑二消息偶发丢失日志里查不到。排查发现是服务端用了异步发送消息进队列后连接断了队列里的消息就丢了。解决发送前检查连接状态关键消息走 ACK 确认未确认的进重发队列。这个坑的教训是——别假设连接一直活着。坑三重连风暴打垮服务端。一次机房网络抖动几万客户端同时断线然后同时重连服务端 CPU 瞬间打满。解决加指数退避和随机抖动同时服务端做限流超过阈值的重连请求排队处理。这个坑让我彻底理解了“抖动”这两个字的价值。5.3 压测方案别用真实用户去试WebSocket 压测和 HTTP 压测不一样因为它是长连接。工具选择上简单场景自己写脚本用 Go 或 Python 起几万个连接定时发消息。专业工具可以用支持 WebSocket 的压测工具配置连接数、消息频率、消息大小。压测要关注的指标连接建立成功率消息端到端延迟P50、P95、P99单机最大连接数内存占用随连接数的增长曲线断线重连恢复时间我的经验是压测一定要压到崩溃点知道系统在什么量级下会出问题比知道它能撑多少更重要。因为线上流量是波动的你得留出安全余量。提示压测时注意客户端的资源消耗。如果你用一台机器模拟十万连接可能客户端先撑不住导致测试结果失真。分布式压测是常态。6. 我个人的一些实战体会聊了这么多最后说几句掏心窝的话。WebSocket 这个技术入门门槛极低但工程化门槛极高。我见过太多项目“能跑就行”结果上线后连接假死、消息丢失、重连风暴轮番上演。我的建议是在写第一行 WebSocket 代码之前先把连接生命周期、心跳、重连、ACK 这四件事的设计想清楚。这四件事没想清楚代码写得再漂亮也是空中楼阁。另外别迷信“协议自带的能力”TCP 的 KeepAlive 默认 2 小时WebSocket 的 ping/pong 浏览器又不让你主动发很多保活工作必须应用层自己扛。还有一个容易被忽视的点监控。WebSocket 的连接数、消息量、延迟、重连率这些指标必须上报。没有监控出了问题你连从哪查都不知道。我现在的习惯是任何实时通信项目第一天就把监控埋点做好后面省下的排查时间远超这点投入。面试的时候如果你能把上面这些工程细节讲出来而不是停留在 API 层面面试官基本会认定你是“干过活的”。因为这些东西文档里不会写只有真正踩过坑的人才知道。