聊天、实时通知、协同编辑、多人游戏、行情看板——都需要服务器在事情一发生的瞬间就把数据推给客户端。普通 HTTP 做不到:它是请求/响应、永远由客户端发起。WebSockets 用一条持久、全双工的连接解决这个,任一方随时都能发送。理解可选项——轮询、长轮询、SSE、WebSockets——以及如何扩展有状态连接,是任何实时设计的核心,比如我们的 聊天应用

⚡ 速览要点
  • HTTP 是客户端发起的请求/响应——服务器无法推送,所以实时需要别的办法。
  • WebSockets 提供持久、全双工连接——经过一次 HTTP 升级握手后,双方在一条长生命 TCP 连接上自由收发。
  • 握手以带 Upgrade 头的 HTTP 开始 → 101 Switching Protocols → 连接变成 ws:///wss://
  • 了解替代方案——长轮询(伪推送)和 SSE(仅服务器→客户端、更简单)——按是否需要双向来选。
  • 扩展是难点——连接是有状态、长生命的;你需要海量打开的套接字、粘性路由,以及一个发布/订阅背板来跨服务器扇出。
  • 心跳 + 重连保持连接健康并从断线恢复。
tldr

WebSockets 把 HTTP 的单向请求/响应升级成一条持久、全双工的通道,让服务器能实时推送给客户端。只需服务器→客户端单向流时,SSE 更简单;真正双向就用 WebSockets;长轮询是兜底。真正的工程难点是规模:连接有状态、长生命,所以你需要粘性路由、容纳海量空闲套接字的能力,以及最重要的——一个发布/订阅背板(如 Redis)把消息扇出到连在其他服务器上的客户端。再加心跳和重连保证韧性。

问题:HTTP 是单向的

HTTP 的模型简单且可扩展:客户端发请求,服务器回响应,完事。但这意味着服务器无法主动发起——它只能应答。对任何需要服务器告诉客户端"来新消息了"或"价格变了"的场景,这是根本性的不匹配。朴素的变通是轮询(polling)(客户端每隔几秒问"有新东西吗?"),这很浪费——大多是空响应——而且有延迟。Web 上实时的历史就是对这个问题一连串越来越好的回答。

演进:轮询 → 长轮询 → SSE → WebSockets

WebSockets 是什么

WebSocket 是一条单一、长生命的 TCP 连接,支持全双工通信——客户端和服务器随时都能独立发送消息,每条消息开销很低(没有每消息的 HTTP 头)。它以一个普通 HTTP 请求开始,所以能穿过现有 Web 基础设施,然后升级:一旦建立,它就是一条裸的双向消息管道(ws://,或 TLS 上的 wss://)。

升级握手
# 客户端 → 服务器(看着像 HTTP)
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

# 服务器 → 客户端
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

# → 连接现在是全双工 WebSocket;任一方随时发送

WebSockets vs SSE vs 长轮询

方面长轮询SSEWebSockets
方向服务器→客户端(伪造)仅服务器→客户端全双工
连接反复请求一条长生命 HTTP一条持久套接字
开销高(重连抖动)每消息最低
复杂度低(浏览器内建)较高
用于老系统兜底feed、通知、行情聊天、协同、游戏

决策规则:如果客户端只需接收更新,SSE 更简单且跑在普通 HTTP 上。如果客户端也频繁发送(聊天打字、游戏里移动),用 WebSockets。长轮询是两者都不可用时的通用兜底。

扩展 WebSockets:真正的挑战

谁都能开一条 WebSocket;难的是运营几百万条。不像任何服务器都能处理的无状态 HTTP 请求,一条 WebSocket 是钉在某一台特定服务器上的有状态、长生命连接。这带来几个问题:

扇出问题与背板

假设 Alice 和 Bob 在同一个聊天房间,但他们的 WebSocket 连在不同的服务器上。Alice 发消息时,持有她连接的服务器没有直接办法触达 Bob 在另一台服务器上的连接。解法是一个发布/订阅背板(pub/sub backplane):服务器把进来的消息发布到一条共享总线(常用 Redis pub/sub 或 Kafka),每台订阅了该房间的服务器都收到消息,再推给它各自连接的客户端。

通过发布/订阅背板跨服务器扇出
Alice ──ws──▶ WS-服务器-1 ──发布 "room42"──▶ ┌───────────────┐
                                             │ Redis pub/sub │
Bob   ──ws──▶ WS-服务器-2 ◀──订阅 "room42"───┤   (背板)      │
                  │                          └───────────────┘
                  └──推送──▶ Bob   (服务器-2 投递给它自己的客户端)

  每台 WS 服务器订阅其客户端所在的房间;
  背板把活在不同服务器上的连接桥接起来

这个背板是 WebSocket 扩展的定义性部件——没有它,横向扩展就崩了,因为消息无法跨越服务器边界。

心跳与重连

长生命连接会悄无声息地死掉——笔记本睡眠、网络抖动、代理把空闲连接超时——常常没有干净关闭。两个机制保持健康。心跳(定时的 WebSocket ping/pong 帧)检测死连接,让服务器回收资源、让客户端知道该重连。客户端的重连逻辑在断线后重建套接字,最好用指数退避(避免服务器重启时的惊群),并能续传——重放自上次收到的 ID 以来错过的消息,这样断口期间不丢东西。

用途

常见坑

总结

WebSockets 把 HTTP 的单向请求/响应变成一条持久、全双工的通道,实现真正的实时。只推服务器→客户端时选 SSE,客户端也要发送时选 WebSockets,长轮询作兜底。难的不是开一条套接字——而是扩展有状态连接:粘性路由、容纳百万空闲套接字,以及最重要的、把消息跨服务器扇出的发布/订阅背板,再加心跳和重连保证韧性。

🎯 面试速答

实时为什么不直接用 HTTP?HTTP 由客户端发起请求/响应——服务器无法推送;轮询浪费且有延迟。
WebSocket vs SSE?SSE 单向(服务器→客户端)、跑在普通 HTTP 上、更简单;WebSockets 全双工,适合客户端也频繁发送时。
握手怎么工作?一个带 Upgrade: websocket 的 HTTP 请求 → 101 Switching Protocols → 连接变成持久的双向套接字。
怎么扩展 WebSockets?粘性路由到持有连接的服务器、容纳大量空闲套接字,以及一个发布/订阅背板(Redis/Kafka)把消息扇出到别的服务器上的客户端。
怎么保持连接健康?ping/pong 心跳检测死套接字,客户端带退避和消息续传的重连。

← 上一篇
gRPC 与 Protobuf