← 全部材料 实时通信
REALTIME · 10 个高频考点

实时推送,先分清方向

实时通信题的关键不是背 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 })); 实时性:实时 双向
面试怎么答四个方案的差别在两个维度:连接是否持续、数据是否双向。短轮询是定时发请求,实现最简单但大量无效请求;长轮询是把请求挂住等有数据再返回,实时性好但服务端要维持大量挂起连接;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 头部开销
面试怎么答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 位可变) 所以应用层拿到的一定是完整消息
面试怎么答WebSocket 在 TCP 之上做了分帧,每帧头里有长度字段,接收方按长度切分,所以应用层拿到的直接是完整消息,不用像裸 TCP 那样处理粘包拆包。但协议允许一条消息拆成多帧,靠 FIN 标志判断结束,这些浏览器 API 都帮你处理了。应用层还需要在 payload 里自定义 type、请求 id、时间戳这些字段,用来区分消息类型和做请求响应配对。
4

心跳与重连:不写就会"假在线"★

连接看起来开着,其实早断了
TCP 连接可能静默失效:网络切换、NAT 超时、代理掐断,双方都不会收到 FIN。表现就是"连接状态是 OPEN,但收不到任何消息"。唯一可靠的解法是应用层心跳 + 超时判定 + 指数退避重连。
面试怎么答TCP 连接可能静默失效,两端都收不到断开信号,表现是 readyState 还是 OPEN 但消息全丢,所以不能只依赖 onclose。必须在应用层做心跳:定期发 ping,超时没收到 pong 就主动 close 触发重连。重连要用指数退避加随机抖动,避免服务端重启后所有客户端同时重连形成风暴。重连成功后还要做状态恢复,带上最后一条消息 id 让服务端补发断线期间的消息。
5

消息顺序、幂等与去重

重连之后可能收到重复消息
单条连接上 WebSocket 保证顺序(底层是 TCP),但重连、多端登录、服务端集群广播都会打破顺序或引入重复。所以客户端要能处理三件事:按序号排序、按 id 去重、按版本号丢弃过期。
面试怎么答单条 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 写的网关)
面试怎么答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 请求头 服务端据此续传,不用自己实现
面试怎么答SSE 就是一个不结束的 HTTP 响应,服务端以 text/event-stream 格式分段写数据。它自带断线自动重连,重连时会带 Last-Event-ID 请求头,服务端可以据此续传,不用自己实现。因为它走标准 HTTP,能过代理、能用 Cookie 认证,实现成本比 WebSocket 低很多。限制是只能单向推送,以及 HTTP/1.1 下同域最多 6 个连接,所以配 HTTP/2 最好。流式输出、进度推送、通知这类场景应该优先考虑 SSE。
8

长轮询:还在用的老方案

什么时候它反而是最优解
长轮询是把请求挂住,有数据或超时才响应。它在三种情况下反而比 WebSocket 好:环境不允许长连接(老代理、某些网关)、只需要极低频推送、以及不想改服务端架构。
面试怎么答长轮询是把请求挂住,服务端有数据或超时才响应,客户端收到后立刻发起下一次。它适合三种情况:环境不允许长连接、推送频率极低、不想改服务端架构。实现上要注意两点,一是服务端必须设超时兜底否则连接会一直占着,二是请求要带上次收到的消息 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,跨域无障碍 缺点:连接建立到认证之间有个短暂未认证窗口,要限制能力
面试怎么答WebSocket 握手是 HTTP,所以可以在升级阶段认证,但浏览器原生 API 不能自定义请求头,加不了 Authorization。三种做法:同域用 Cookie 最省事;URL 带 token 最方便但会进各种日志,所以应该用短时效一次性 ticket;推荐的是连接后首帧认证,服务端给几秒窗口。另外要注意 WebSocket 不受同源策略约束,任何站点都能连,所以服务端必须校验 Origin 头,否则等于对外开放。
10

怎么选:一张判断表

面试里最容易讲清的部分
选型不看"哪个先进",看四个条件:要不要双向、实时性要求、连接规模、以及环境限制。绝大多数业务场景的正确答案是 SSE 或 WebSocket,短轮询只在实时性要求低时合理。
按场景选【只要服务端推,客户端偶尔上行】 进度条、流式回答、日志、通知、行情 → SSE(简单、能过代理、自动重连) 【双向高频】 聊天、协同编辑、多人游戏、远程控制 → WebSocket 【实时性要求低】 后台列表刷新、邮件红点 → 短轮询 30-60 秒,成本最低 【环境不允许长连接】 某些企业网关、老旧 Ingress → 长轮询兜底 【需要兼容极老浏览器】 → 长轮询 + 短轮询降级
面试怎么答选型看四个条件:是否需要双向、实时性要求、连接规模、环境限制。只需要服务端推的场景优先 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 更简单,还能复用现有的鉴权和网关。