聊天、实时通知、协同编辑、多人游戏、行情看板——都需要服务器在事情一发生的瞬间就把数据推给客户端。普通 HTTP 做不到:它是请求/响应、永远由客户端发起。WebSockets 用一条持久、全双工的连接解决这个,任一方随时都能发送。理解可选项——轮询、长轮询、SSE、WebSockets——以及如何扩展有状态连接,是任何实时设计的核心,比如我们的 聊天应用。
- HTTP 是客户端发起的请求/响应——服务器无法推送,所以实时需要别的办法。
- WebSockets 提供持久、全双工连接——经过一次 HTTP 升级握手后,双方在一条长生命 TCP 连接上自由收发。
- 握手以带
Upgrade头的 HTTP 开始 →101 Switching Protocols→ 连接变成ws:///wss://。 - 了解替代方案——长轮询(伪推送)和 SSE(仅服务器→客户端、更简单)——按是否需要双向来选。
- 扩展是难点——连接是有状态、长生命的;你需要海量打开的套接字、粘性路由,以及一个发布/订阅背板来跨服务器扇出。
- 心跳 + 重连保持连接健康并从断线恢复。
WebSockets 把 HTTP 的单向请求/响应升级成一条持久、全双工的通道,让服务器能实时推送给客户端。只需服务器→客户端单向流时,SSE 更简单;真正双向就用 WebSockets;长轮询是兜底。真正的工程难点是规模:连接有状态、长生命,所以你需要粘性路由、容纳海量空闲套接字的能力,以及最重要的——一个发布/订阅背板(如 Redis)把消息扇出到连在其他服务器上的客户端。再加心跳和重连保证韧性。
问题:HTTP 是单向的
HTTP 的模型简单且可扩展:客户端发请求,服务器回响应,完事。但这意味着服务器无法主动发起——它只能应答。对任何需要服务器告诉客户端"来新消息了"或"价格变了"的场景,这是根本性的不匹配。朴素的变通是轮询(polling)(客户端每隔几秒问"有新东西吗?"),这很浪费——大多是空响应——而且有延迟。Web 上实时的历史就是对这个问题一连串越来越好的回答。
演进:轮询 → 长轮询 → SSE → WebSockets
- 短轮询——客户端定时请求。简单,但浪费请求、延迟最高可达轮询间隔。
- 长轮询(long polling)——客户端发请求,服务器挂住它直到有数据(或超时),然后客户端立即重发。近实时、到处能用,但是个 hack,有连接抖动和开销。
- Server-Sent Events(SSE)——标准化的单向流:服务器在一条长生命 HTTP 响应上持续推事件流。简单、自动重连,但仅服务器→客户端。
- 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 长轮询
| 方面 | 长轮询 | SSE | WebSockets |
|---|---|---|---|
| 方向 | 服务器→客户端(伪造) | 仅服务器→客户端 | 全双工 |
| 连接 | 反复请求 | 一条长生命 HTTP | 一条持久套接字 |
| 开销 | 高(重连抖动) | 低 | 每消息最低 |
| 复杂度 | 低 | 低(浏览器内建) | 较高 |
| 用于 | 老系统兜底 | feed、通知、行情 | 聊天、协同、游戏 |
决策规则:如果客户端只需接收更新,SSE 更简单且跑在普通 HTTP 上。如果客户端也频繁发送(聊天打字、游戏里移动),用 WebSockets。长轮询是两者都不可用时的通用兜底。
扩展 WebSockets:真正的挑战
谁都能开一条 WebSocket;难的是运营几百万条。不像任何服务器都能处理的无状态 HTTP 请求,一条 WebSocket 是钉在某一台特定服务器上的有状态、长生命连接。这带来几个问题:
- 海量空闲连接——一个聊天服务可能挂着几百万个大多空闲的套接字;每个都占内存和文件描述符,所以每服务器的连接密度很重要。
- 粘性路由——负载均衡器必须让客户端钉在持有它连接的那台服务器上(而且首先得支持 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 以来错过的消息,这样断口期间不丢东西。
用途
- 聊天与消息——典型的双向场景(见聊天应用设计)。
- 实时通知与在线状态——"正在输入"、在线状态、提醒(若单向,SSE 常够用)。
- 协同编辑——共享光标和编辑(Google Docs 式),需要低延迟双向同步。
- 实时看板、游戏、交易——双向连续、延迟敏感的更新。
常见坑
- 基础设施支持——负载均衡器、代理、网关必须显式支持 WebSocket 升级和长生命连接。
- 连接上限——操作系统的文件描述符和内存限制了每服务器连接数;调优并为空闲套接字规划容量。
- 没有背板——忘了发布/订阅层,消息就到不了别的服务器上的客户端;经典的扩展疏漏。
- 把状态放在连接上——把用户钉在服务器上让部署(重启时优雅排空连接)和故障转移变复杂。
- 过度设计——如果更新不频繁或单向,SSE 甚至轮询比一整套 WebSocket 更简单便宜。
WebSockets 把 HTTP 的单向请求/响应变成一条持久、全双工的通道,实现真正的实时。只推服务器→客户端时选 SSE,客户端也要发送时选 WebSockets,长轮询作兜底。难的不是开一条套接字——而是扩展有状态连接:粘性路由、容纳百万空闲套接字,以及最重要的、把消息跨服务器扇出的发布/订阅背板,再加心跳和重连保证韧性。
实时为什么不直接用 HTTP?HTTP 由客户端发起请求/响应——服务器无法推送;轮询浪费且有延迟。
WebSocket vs SSE?SSE 单向(服务器→客户端)、跑在普通 HTTP 上、更简单;WebSockets 全双工,适合客户端也频繁发送时。
握手怎么工作?一个带 Upgrade: websocket 的 HTTP 请求 → 101 Switching Protocols → 连接变成持久的双向套接字。
怎么扩展 WebSockets?粘性路由到持有连接的服务器、容纳大量空闲套接字,以及一个发布/订阅背板(Redis/Kafka)把消息扇出到别的服务器上的客户端。
怎么保持连接健康?ping/pong 心跳检测死套接字,客户端带退避和消息续传的重连。