实时推送,先分清方向
实时通信题的关键不是背 API,而是说清每种方案的代价:轮询浪费请求、长连接要自己做心跳、SSE 只能单向、WebSocket 不受同源约束。这份按代价组织。
01先问方向只要服务端推,用 SSE;客户端也要频繁上行,才轮到 WebSocket。
02认准失效模式连接静默断开、消息重复、多机广播,这三个是真实场景的必答题。
03说出量级几万连接单机加 Redis 够用,几十万要网关,给出阶段比报技术名有用。
1
四代方案:轮询 / 长轮询 / SSE / WebSocket★
先分清"谁发起"和"单向还是双向"
四个方案的区别只用两个维度就能分开:连接是不是一直开着、数据是单向还是双向。短轮询 = 客户端反复问;长轮询 = 服务端把请求挂住等到有数据;SSE = 服务端单向推;WebSocket = 双向长连接。
◐ 可交互:20 秒内服务端推 3 条消息,四种方案各发多少请求
短轮询每 2 秒一次
请求 0平均延迟 —
长轮询请求挂住
请求 0平均延迟 —
SSE一条长连接
连接 0平均延迟 —
WebSocket一条双向连接
连接 0平均延迟 —
一次请求 / 连接收到一条消息
点「开始」跑 20 秒。三条消息分别在第 3、11、17 秒到达。
t = 0s
短轮询客户端每 N 秒问一次,没数据也返回空,大量无效请求
→
长轮询请求挂住直到有数据或超时,有数据立刻返回,客户端再发起下一次
→
SSE一个 HTTP 响应持续不结束,服务端往里写 event,单向推送
→
WebSocket一次握手升级成双向长连接,双方随时发,帧开销小
同一件事(服务端有新消息就通知)的四种写法【短轮询】3 秒一次
setInterval(async () => {
const r = await fetch('/msg');
if (r.items.length) render(r.items);
}, 3000);
实时性:平均延迟 1.5 秒 无效请求:每分钟 20 次
【长轮询】请求挂住
async function poll() {
const r = await fetch('/msg?wait=30'); // 服务端最多挂 30 秒
render(r.items);
poll(); // 立刻再挂
}
实时性:接近实时 无效请求:每次都有意义
【SSE】服务端持续写
const es = new EventSource('/stream');
es.onmessage = (e) => render(JSON.parse(e.data));
实时性:实时 单向
【WebSocket】双向
const ws = new WebSocket('wss://api.example.com/ws');
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.send(JSON.stringify({ type: 'sub', room: 1 }));
实时性:实时 双向
- 短轮询什么时候够用:数据变化不频繁、实时性要求不高(比如后台订单列表 30 秒刷新一次)。成本最低,不用改服务端架构。
- 长轮询的真实代价:每个在线用户都挂着一个连接,服务端要维持大量挂起的请求。用 Node 这类异步 IO 还好,用同步阻塞的模型会直接把线程占满。
- SSE 被严重低估:很多场景只需要服务端推(进度、日志、通知、股票行情),用 SSE 比 WebSocket 简单得多——它就是一个不结束的 HTTP 响应,走标准 HTTP,能过代理、能自动重连、能用 HTTP 认证。
- WebSocket 的唯一优势是双向:如果客户端也要频繁往服务端发,用两个 SSE 一个 HTTP 会很别扭,这时才值得上 WebSocket。
面试怎么答四个方案的差别在两个维度:连接是否持续、数据是否双向。短轮询是定时发请求,实现最简单但大量无效请求;长轮询是把请求挂住等有数据再返回,实时性好但服务端要维持大量挂起连接;SSE 是一个不结束的 HTTP 响应,服务端单向推,能过代理还能自动重连;WebSocket 一次握手升级成双向长连接,只有确实需要客户端频繁上行时才值得用。
2
WebSocket 握手:为什么能升级成双向★
本质是一次 HTTP 请求加两行头
WebSocket 不是凭空的新协议,它借用 HTTP 完成握手:客户端发一个带
Upgrade: websocket 的 GET 请求,服务端回 101 Switching Protocols,之后这条 TCP 连接就不再走 HTTP 了,双方按帧互相发。握手报文客户端 →
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端 →
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
之后:同一条 TCP 连接上跑 WebSocket 帧,没有 HTTP 头部开销
- Sec-WebSocket-Key 的作用不是加密:它是一个随机 base64,服务端拼上固定 GUID 做 SHA-1 再 base64 回来。目的是确认"对面真的懂 WebSocket",防止中间的缓存代理误把它当普通 HTTP 处理。
- 为什么能过防火墙:握手是标准 HTTP/HTTPS,看起来和普通请求没区别。所以 WebSocket 通常能穿企业代理,而任意端口的裸 TCP 常常被拦。
- Nginx 必须显式转发 Upgrade 头:不加这两行,握手永远失败,客户端表现为"连不上"而你从日志里看不出原因。这是部署 WebSocket 最经典的坑。
- wss 就是 WebSocket over TLS:端口 443,和 https 一样。生产必须用 wss,否则页面是 https 时浏览器会直接阻止 ws 连接(混合内容)。
面试怎么答WebSocket 握手是借 HTTP 完成的:客户端发一个普通 GET,带上 Upgrade: websocket 和 Sec-WebSocket-Key,服务端返回 101 Switching Protocols 和一个由 key 算出来的 Accept 值,之后这条 TCP 连接就脱离 HTTP,按帧通信。Sec-WebSocket-Key 的作用是防止中间代理误处理,不是加密。因为握手看起来就是普通 HTTPS 请求,所以容易穿过企业防火墙,这也是它比裸 TCP 好部署的原因。
3
帧结构和消息边界
为什么 WebSocket 不用处理粘包
WebSocket 有自己的分帧:每帧带长度字段,接收方按长度切分,所以应用层不用自己找分隔符。但一条逻辑消息可能被拆成多帧(分片),也一帧里只放了一部分。所以"粘包拆包"的问题从 TCP 层挪到了帧层,好在协议帮你做了大半。
如果自己实现协议(比如 TCP 自定义协议)TCP 是字节流,没有消息边界。
方案一:长度前缀(最常用)
[4 字节长度][payload][4 字节长度][payload]
读的时候先读 4 字节拿到长度,再读那么多字节
方案二:分隔符
payload\npayload\n
问题:内容里出现分隔符就断了,要转义
方案三:定长
每条消息固定 N 字节
问题:不灵活
WebSocket 用的是方案一的变体:
帧头里有 payload length 字段(7 / 16 / 64 位可变)
所以应用层拿到的一定是完整消息
- 为什么会有分片:发送方可以在不知道总长度时就开始发(比如流式上传),或者把一个超大消息拆开发以避免阻塞其他消息。接收方靠 FIN 标志位判断"这是不是最后一帧"。
- 应用层还要不要自定义协议:要。
type字段区分消息类型、请求 id 做请求响应配对、时间戳做排序,这些都在 payload 里自己定。 - 推荐用 JSON 还是二进制:消息量小、调试多,用 JSON。高频大数据(行情、游戏、协同编辑)用二进制(Protobuf / MessagePack / 自定义),体积能差好几倍。
面试怎么答WebSocket 在 TCP 之上做了分帧,每帧头里有长度字段,接收方按长度切分,所以应用层拿到的直接是完整消息,不用像裸 TCP 那样处理粘包拆包。但协议允许一条消息拆成多帧,靠 FIN 标志判断结束,这些浏览器 API 都帮你处理了。应用层还需要在 payload 里自定义 type、请求 id、时间戳这些字段,用来区分消息类型和做请求响应配对。
4
心跳与重连:不写就会"假在线"★
连接看起来开着,其实早断了
TCP 连接可能静默失效:网络切换、NAT 超时、代理掐断,双方都不会收到 FIN。表现就是"连接状态是 OPEN,但收不到任何消息"。唯一可靠的解法是应用层心跳 + 超时判定 + 指数退避重连。
- 为什么不能只靠
onclose:链路断在中间(比如路由器 NAT 表过期)时,两端都不知道,readyState一直是 OPEN,永远不触发 onclose。系统会一直以为在线,推消息全丢。 - 心跳间隔怎么定:要比中间的 NAT / 代理超时短。常见是 25-30 秒,因为很多 NAT 表项超时是 60 秒。太长会被断开,太短浪费流量。
- 为什么要加随机抖动:服务端重启后所有客户端同时重连,会形成"重连风暴"。加
(0.5 + random())把重连时间打散。 - 重连后要做状态恢复:订阅关系、未确认的消息、断线期间的消息都要补。通常做法是重连成功后发一个
sync请求,带上次收到的最后一条消息 id,服务端把之后的消息补发。 - 页面切到后台再回来:移动端和部分浏览器会冻结后台页面的定时器,心跳可能停掉。监听
visibilitychange,回来时立刻探活一次。
面试怎么答TCP 连接可能静默失效,两端都收不到断开信号,表现是 readyState 还是 OPEN 但消息全丢,所以不能只依赖 onclose。必须在应用层做心跳:定期发 ping,超时没收到 pong 就主动 close 触发重连。重连要用指数退避加随机抖动,避免服务端重启后所有客户端同时重连形成风暴。重连成功后还要做状态恢复,带上最后一条消息 id 让服务端补发断线期间的消息。
5
消息顺序、幂等与去重
重连之后可能收到重复消息
单条连接上 WebSocket 保证顺序(底层是 TCP),但重连、多端登录、服务端集群广播都会打破顺序或引入重复。所以客户端要能处理三件事:按序号排序、按 id 去重、按版本号丢弃过期。
- 为什么会有重复:断线重连后客户端补拉历史,可能和实时推送重叠;服务端做"至少一次投递"以保证不丢,重复是必然的。所以客户端必须幂等。
- 至少一次 vs 最多一次:要保证不丢就只能接受重复(至少一次),要保证不重就接受可能丢(最多一次)。绝大多数业务选不丢,靠客户端去重。
- 多端登录的顺序问题:同一账号在手机和电脑同时在线,两条连接各自有序但相互之间无序。这类场景要靠服务端给全局递增版本号,客户端按版本合并。
- 乐观更新必须能回滚:本地先显示"已发送",服务端返回失败时要撤回并提示。不做回滚的话用户会以为发出去了,实际没发出去——这是聊天类应用最常见的体验 bug。
面试怎么答单条 WebSocket 连接底层是 TCP,所以消息有序,但重连补偿、多端登录、服务端集群广播都会带来重复和乱序。所以每条消息要带唯一 id 和自增序号,客户端用 Set 去重、用 seq 丢弃过期消息。服务端一般选至少一次投递保证不丢,重复交给客户端处理。乐观更新一定要能回滚,请求带临时 id 关联服务端响应,失败时撤回。
6
服务端怎么扩容:从单机到多机★
连接在哪台机器,消息怎么跨机器传
WebSocket 是有状态的连接,不像 HTTP 可以随便打散。单机时一个 Map 就能管所有连接;多机时必须在机器之间广播——通常用 Redis Pub/Sub 或消息队列,让每台机器只推自己持有的连接。
三个阶段的架构【单机】连接池在内存里
const clients = new Map(); // userId -> ws
发消息:clients.get(id)?.send(msg)
限制:一台机器撑不住,也没法滚动发布
【多机 + 广播】Redis Pub/Sub
每台机器订阅同一个频道
业务侧往频道 publish,所有机器都收到
每台机器只给"自己持有的连接"推
优点:简单、够用(几万台连接没问题)
缺点:消息扇出到所有机器,机器越多浪费越大
【大规模】加一层路由表
Redis 里存 userId -> gatewayId
发消息先查表,只发给目标 gateway
或者用专门的消息网关 / 长连接服务(如 Go 写的网关)
- 为什么要 sticky session:如果握手请求经过负载均衡,重连时落到另一台机器,那台机器没有这条连接的历史状态(订阅关系、未确认的消息)。要么用一致性哈希保证同一用户落到同一台,要么把状态外置到 Redis。
- 滚动发布是最麻烦的场景:发版时机器逐台重启,上面的连接会断。要做的准备是:提前通知客户端"即将重连"、把连接优雅迁移、或者用双连接切换(新连接建立后再关旧连接)。
- Nginx 的配置要点:
proxy_read_timeout默认 60 秒,比心跳间隔长就没事,但如果不配proxy_http_version 1.1和 Upgrade 头,握手直接失败。
面试怎么答WebSocket 是有状态连接,不能像 HTTP 那样随便打散到任意机器。单机时用 Map 管连接就够了;多机时要做广播,通常用 Redis Pub/Sub,业务侧 publish 消息,所有机器订阅到之后各自推给本地持有的连接。规模再大就加一层用户到网关的路由表,只发给目标机器。另外要注意负载均衡要保证同一用户落到同一台(或者把状态外置),否则重连后状态会丢。
7
SSE:被低估的单向推送
一个不结束的 HTTP 响应
SSE 就是一个永远不结束的 HTTP 响应,服务端分段往里写
data: 行。它自带断线自动重连和事件 id 续传,走标准 HTTP 所以能过代理、能用 Cookie 认证。代价是只能服务端推,且 HTTP/1.1 下有 6 个同域连接上限。服务端响应长什么样HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
: 注释行,可用来做心跳(客户端忽略)
id: 1
event: progress
data: {"pct": 30}
id: 2
event: progress
data: {"pct": 60}
retry: 3000 ← 告诉浏览器重连间隔
要点:
每个事件以空行结束
id 会被浏览器记住,重连时带 Last-Event-ID 请求头
服务端据此续传,不用自己实现
- Last-Event-ID 是免费的能力:浏览器重连时会自动带上这个头,服务端从这条之后继续写,天然支持断点续传。WebSocket 要自己做。
- HTTP/1.1 的 6 连接上限:同一个域名最多 6 个 TCP 连接,SSE 占一个且一直不释放。开多个标签页会互相挤。HTTP/2 下多路复用就没这个问题,所以 SSE 配 HTTP/2 是最佳组合。
- 为什么 AI 对话都用 SSE:流式输出只需要服务端往下推 token,客户端不用一直上行。用 SSE 比 WebSocket 简单得多,还能直接复用现有的鉴权和网关。
- 关掉的时候要服务端感知:客户端断开时服务端要停止写和清理资源,监听
req.on('close')。不处理的话会泄漏。
面试怎么答SSE 就是一个不结束的 HTTP 响应,服务端以 text/event-stream 格式分段写数据。它自带断线自动重连,重连时会带 Last-Event-ID 请求头,服务端可以据此续传,不用自己实现。因为它走标准 HTTP,能过代理、能用 Cookie 认证,实现成本比 WebSocket 低很多。限制是只能单向推送,以及 HTTP/1.1 下同域最多 6 个连接,所以配 HTTP/2 最好。流式输出、进度推送、通知这类场景应该优先考虑 SSE。
8
长轮询:还在用的老方案
什么时候它反而是最优解
长轮询是把请求挂住,有数据或超时才响应。它在三种情况下反而比 WebSocket 好:环境不允许长连接(老代理、某些网关)、只需要极低频推送、以及不想改服务端架构。
- 服务端怎么挂住:Node 里就是先不
res.end(),把 res 存到一个待发队列,有数据时取出来写。要设超时兜底,否则连接会一直占着。 - 为什么要有
since参数:断线期间的消息要靠它补。每次请求带上"我最后收到的 id",服务端返回这之后的数据。这是长轮询做消息不丢的标准做法。 - 连接数就是并发数:每个在线用户都挂着一个未完成的请求,服务端要维持同样数量的挂起连接。这和 WebSocket 的常驻连接数量其实一样,差别在长轮询每次有数据都要重建请求(TCP 可能复用,但 HTTP 开销还在)。
- 还值得用的场景:需要穿过只支持 HTTP 短请求的网关、或者 K8s Ingress 对长连接支持不好时。工程上"能跑通"经常比"技术先进"重要。
面试怎么答长轮询是把请求挂住,服务端有数据或超时才响应,客户端收到后立刻发起下一次。它适合三种情况:环境不允许长连接、推送频率极低、不想改服务端架构。实现上要注意两点,一是服务端必须设超时兜底否则连接会一直占着,二是请求要带上次收到的消息 id,服务端返回这之后的数据,这样断线期间的消息才不会丢。
9
鉴权与安全★
握手时怎么带 token
WebSocket 握手是 HTTP 请求,所以可以在握手时做认证,但浏览器原生 WebSocket API 不能自定义请求头——不能加
Authorization。所以常见做法有三种:Cookie(同域最省事)、URL 参数(会进日志)、以及连接后第一帧认证。三种鉴权方式对比【Cookie】同域时最省事
new WebSocket('wss://api.example.com/ws')
浏览器自动带 Cookie,服务端在 upgrade 阶段校验
优点:不用传 token,httpOnly 更安全
缺点:跨域要配 SameSite / CORS,且容易受 CSRF 影响
【URL 参数】
new WebSocket('wss://api.example.com/ws?token=xxx')
优点:简单,跨域也行
缺点:token 会进 Nginx access log、浏览器历史、代理日志
只能用短时效的一次性 ticket
【首帧认证】推荐
连接建立后客户端立刻发 {type:'auth', token}
服务端给 5 秒窗口,没认证就断开
优点:token 不进 URL,跨域无障碍
缺点:连接建立到认证之间有个短暂未认证窗口,要限制能力
- 用一次性 ticket 替代 token:先走 HTTPS 接口换一个 30 秒有效、只能用一次的 ticket,再拿它连 WebSocket。这样即使 URL 进了日志也没用。这是相对稳妥的折中。
- 一定要 wss:https 页面里连 ws:// 会被浏览器直接拦掉(混合内容)。而且明文 WebSocket 内容全裸。
- 跨域:WebSocket 不受同源策略约束,任何网站都能连你的 ws 服务。所以服务端必须校验
Origin头,否则等于开放了任意站点连接。这一点很多人不知道。 - 权限要在每次消息里校验:连上时认证只代表"这一刻有效"。token 过期、被封号、退出登录,都要能主动踢掉连接。
面试怎么答WebSocket 握手是 HTTP,所以可以在升级阶段认证,但浏览器原生 API 不能自定义请求头,加不了 Authorization。三种做法:同域用 Cookie 最省事;URL 带 token 最方便但会进各种日志,所以应该用短时效一次性 ticket;推荐的是连接后首帧认证,服务端给几秒窗口。另外要注意 WebSocket 不受同源策略约束,任何站点都能连,所以服务端必须校验 Origin 头,否则等于对外开放。
10
怎么选:一张判断表
面试里最容易讲清的部分
选型不看"哪个先进",看四个条件:要不要双向、实时性要求、连接规模、以及环境限制。绝大多数业务场景的正确答案是 SSE 或 WebSocket,短轮询只在实时性要求低时合理。
按场景选【只要服务端推,客户端偶尔上行】
进度条、流式回答、日志、通知、行情
→ SSE(简单、能过代理、自动重连)
【双向高频】
聊天、协同编辑、多人游戏、远程控制
→ WebSocket
【实时性要求低】
后台列表刷新、邮件红点
→ 短轮询 30-60 秒,成本最低
【环境不允许长连接】
某些企业网关、老旧 Ingress
→ 长轮询兜底
【需要兼容极老浏览器】
→ 长轮询 + 短轮询降级
- 面试里怎么答出深度:不要只说"WebSocket 实时",要说"先看是不是需要双向。如果只是服务端推,SSE 更合适,因为它走标准 HTTP,能复用鉴权和网关,还自带重连和 Last-Event-ID 续传,而 WebSocket 不受同源策略约束,得自己校验 Origin、自己做心跳重连"。
- 能不能混用:能。常见做法是 SSE 收推送、普通 HTTP 发请求。这样上行自然带上了重试、超时、拦截器这些现成能力,比全塞进 WebSocket 简单。
- 规模评估:几万连接单机加 Redis 广播够用;几十万要专门的网关和连接管理。面试时给出量级和对应的架构阶段,比只报一个技术名有说服力。
面试怎么答选型看四个条件:是否需要双向、实时性要求、连接规模、环境限制。只需要服务端推的场景优先 SSE,因为它走标准 HTTP,能复用鉴权网关,自带重连和 Last-Event-ID 续传;确实需要双向高频才上 WebSocket,代价是不受同源策略约束要自己校验 Origin,还要自己做心跳和重连。实时性要求低的场景短轮询成本最低。也常见混用,推送用 SSE、写请求走普通 HTTP。
11
速答清单
考前十分钟只看这一页
01四个实时方案的区别?
短轮询定时问、长轮询把请求挂住等数据、SSE 服务端单向推、WebSocket 双向长连接。区别就两个字:方向和是否持续。
02WebSocket 握手怎么完成?
借用 HTTP:客户端发带 Upgrade: websocket 和 Sec-WebSocket-Key 的 GET,服务端回 101 和算出来的 Accept,之后同一条 TCP 连接改跑 WebSocket 帧。
03Sec-WebSocket-Key 是加密用的吗?
不是。它是随机 base64,服务端拼固定 GUID 做 SHA-1 再返回,用来确认对方真的支持 WebSocket,防止中间代理误处理。
04部署 WebSocket 最容易踩什么坑?
Nginx 没转发 Upgrade 和 Connection 头,握手直接失败,客户端只表现为连不上,日志里看不出来。
05WebSocket 要处理粘包吗?
不用。协议自带分帧,帧头有长度字段,浏览器 API 给你的已经是完整消息。但应用层还得在 payload 里自定义 type 和请求 id。
06为什么必须做心跳?
TCP 可能静默失效,两端都收不到断开信号,readyState 还是 OPEN 但消息全丢。所以要应用层 ping/pong 加超时判定。
07重连为什么要指数退避加抖动?
服务端重启时所有客户端同时重连会形成风暴。退避降低频率,随机抖动把时间打散。
08断线期间的消息怎么补?
重连后带上最后收到的消息 id 或序号,服务端补发这之后的。SSE 可以靠浏览器自动带的 Last-Event-ID 头。
09消息重复怎么处理?
服务端通常选至少一次投递保证不丢,客户端用消息 id 做 Set 去重,用自增序号丢弃过期消息。
10多机部署 WebSocket 怎么广播?
用 Redis Pub/Sub 或消息队列,业务侧 publish,每台机器订阅后只推给本地持有的连接。规模再大就加用户到网关的路由表。
11SSE 相比 WebSocket 的优势?
走标准 HTTP,能过代理、能复用 Cookie 鉴权和网关;自带断线重连和 Last-Event-ID 续传。限制是只能单向推。
12SSE 有什么限制?
单向推;HTTP/1.1 下同域最多 6 个连接且会一直占着,所以要配 HTTP/2。
13长轮询服务端要注意什么?
必须设超时兜底否则连接一直占着;请求要带上次收到的 id 才能补断线期间的数据。
14浏览器 WebSocket 能自定义请求头吗?
不能,加不了 Authorization。所以用 Cookie、URL 传一次性 ticket、或者连接后首帧认证。
15WebSocket 受同源策略约束吗?
不受。任何站点都能连你的 ws 服务,所以服务端必须校验 Origin 头,否则等于对外开放。
16什么时候用 SSE 而不是 WebSocket?
只需要服务端推:流式回答、进度、通知、行情。这时 SSE 更简单,还能复用现有的鉴权和网关。