WebSocket 详解:让服务器也能主动开口的协议
HTTP 生来建立在「客户端发问、服务器作答」这种单向模型之上。而聊天、通知、实时同步这类需要服务器主动推送数据的场景,恰恰长期受制于这一结构。本文从握手过程讲起,深入帧结构的细节,再到 WebSocket 作为 P2P 信令通道的角色,以及它与新一代 WebTransport 之间的关系,全面梳理这个双向通信协议。
因何而生:请求/响应模型的局限
Web 从一开始就建立在「客户端发出请求、服务器给出响应」的模型之上。在这种结构下,即便服务器一侧产生了新数据,也没有办法主动送达客户端,只能等客户端再次发起请求。对于聊天、股价实时更新、消息通知这类需要由服务器主动推送数据的场景,业界长期依赖两种权宜之计:客户端每隔几秒钟就发起一次请求的轮询(polling),以及服务器故意拖延响应、直到有新内容才返回的长轮询(long polling,即 Comet)。但这两种方式都存在明显缺陷:每次新建连接都要承担 HTTP 头部的开销,连接反复建立与断开带来额外损耗,响应延迟也难以令人满意。为了从根本上解决这一低效问题,RFC 6455 于 2011 年将 WebSocket 标准化。它在一条 TCP 连接之上提供全双工(full-duplex)的通信通道,客户端与服务器双方都可以在任意时刻主动发送消息,互不依赖对方是否先发起请求。
握手与连接建立
WebSocket 连接并非一开始就使用某种全新的协议,而是以一次普通的 HTTP 请求作为起点。客户端发送一个带有 Upgrade: websocket 与 Connection: Upgrade 头部的 HTTP/1.1 GET 请求,并附上一个 Sec-WebSocket-Key,由 16 字节随机数经 Base64 编码而成。若服务器同意切换协议,会以状态码 101 Switching Protocols 作出响应,并附带一个 Sec-WebSocket-Accept 头部。该值的计算方式是:把收到的 key 与一个固定的 GUID 字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接后取 SHA-1 哈希,再做 Base64 编码。客户端会在本地做同样的计算并比对返回值,以此确认这个响应确实来自一台理解 WebSocket 协议的服务器。握手完成后,同一条 TCP 连接上便不再具有 HTTP 语义,此后的通信全部切换为下文所述的帧格式。协议方案分为明文的 ws://(默认端口 80)与经 TLS 加密的 wss://(默认端口 443,与 HTTPS 一样需要校验证书)两种。
帧结构:消息是如何被切分传输的
握手完成之后,WebSocket 并不会把消息原样直接发送,而是将其切分为格式固定的帧(frame)逐个收发。一条逻辑上的消息也可能被拆分(分片)到多个帧中传输。
- FIN 位
- 一个用来标记「本帧是否为该消息最后一个分片」的比特位。若为 0,则说明后面还有更多帧,接收方需要把 opcode 为「continuation」(0x0)的若干分片依次拼接,还原出完整的消息。
- opcode
- 一个 4 比特的字段,用来标识帧的类型。除了文本(0x1,UTF-8 编码的负载)与二进制(0x2)这两种数据帧之外,还有用于宣告连接结束的close(0x8),以及用于心跳检测的ping(0x9)与pong(0xA)等控制帧。
- 负载长度
- 采用可变长编码:当长度超出基本的 7 比特字段所能表示的范围时,用数值 126 表示后面紧跟一个 16 位的扩展长度字段,用 127 表示后面紧跟一个 64 位的扩展长度字段。这样既能让小消息的开销保持很低,又能容纳较大的负载。
- 掩码(masking)
- 客户端发往服务器的帧必须用一个 4 字节的掩码密钥对整个负载做异或运算(服务器发往客户端的帧则不得加掩码)。这么做的目的并非为了保密,而是 RFC 明确要求的安全措施:防止恶意网页精心构造的负载,污染链路上透明代理的缓存。
心跳检测、子协议与来源模型
- ping/pong:发送一个 opcode 为 0x9 的 ping 帧后,对方理应回复一个携带相同负载的 pong 帧(0xA)。若迟迟收不到回应,即可判定连接已经失效;而这种周期性的往返本身,也能防止代理或 NAT 设备因为连接长时间空闲而擅自将其超时断开。
- 子协议:在握手阶段,客户端可以通过
Sec-WebSocket-Protocol头部列出候选项,由服务器从中挑选并确认一个,从而让双方在同一条 WebSocket 连接之上约定应用层的消息格式(例如 MQTT over WebSocket,或用于 GraphQL 订阅的graphql-ws)。 - 来源模型:与 XMLHttpRequest、fetch 不同,WebSocket 连接不受同源策略(Same-Origin Policy)约束。浏览器会在握手请求中附带一个
Origin头部,但是否校验它、是否据此拒绝请求,完全取决于服务器端的实现。若疏于校验,就可能被利用发起跨站 WebSocket 劫持(CSWSH,Cross-Site WebSocket Hijacking)攻击,滥用受害者已登录的会话。
WebSocket 在 P2P 场景中的角色
WebSocket 本身是一种客户端/服务器技术,但在 P2P 系统的周边,它同样扮演着重要角色。
- WebRTC 的信令通道:正如NAT 穿透一文所述,WebRTC 定义了对等方之间直接传输媒体与数据的方式,但刻意没有规定在建立连接之前,双方该如何交换 SDP offer/answer 与 ICE 候选地址,这套「信令」机制被留给具体实现自行选择。大多数实现都会用 WebSocket 与信令服务器通信,完成这一步预先协商。
- Nostr 中客户端与中继的通信:在 Nostr 中,客户端与中继(relay)之间交换
EVENT、REQ、CLOSE等 JSON 消息,直接构建在 WebSocket 之上,是协议规范本身的核心组成部分,而不只是某个实现的细节选择。 - 区块链节点的订阅接口:以 Ethereum 节点为代表的 JSON-RPC 接口,往往通过 WebSocket 提供
eth_subscribe这类订阅端点,让客户端能够实时接收新区块或日志,而无需依赖轮询。 - 元宇宙场景中的实时状态同步:在P2P 与元宇宙一类需要由服务器协调、让众多参与者的实时状态保持同步的场景中,WebSocket 同样是实现低延迟双向通信的常规选择。
运维层面的挑战
与短暂、无状态的 HTTP 请求不同,WebSocket 要求同时维持大量有状态、长时间存活的连接。这一特性给实际运维带来了一系列特有的挑战。
- 扩展性:许多通用的七层(L7)负载均衡器都是围绕短连接设计的,未必能很好地分发长连接。单台服务器能同时维持的连接数受到文件描述符等操作系统资源的限制;此外,在一次发布或网络抖动之后,大批客户端同时尝试重连所形成的「重连风暴」,也需要用指数退避加抖动等策略加以缓解。
- 与代理、企业防火墙的兼容性:一些较老的代理或防火墙无法正确处理
Upgrade头部,或者会以较短的超时时间强制断开空闲连接,这使得企业内网环境下的连通性经常成为问题。 - 在 HTTP/2、HTTP/3 时代的定位:HTTP/2 的设计初衷是在一条连接上复用多个请求,但其最初的规范与 WebSocket 的
Upgrade机制并不兼容。RFC 8441(2018 年)填补了这一空白,定义了如何利用扩展的 CONNECT 方法,把 WebSocket 隧道封装进 HTTP/2 的一条流之中。而基于 QUIC 的 HTTP/3,以及 WebTransport,则是沿着这一思路发展出的更新选项。
值得一提的是,常被混为一谈的 Socket.IO 并不等同于 WebSocket 本身。它是一个拥有自己的握手流程与消息封装格式的上层库,在 WebSocket 不可用的环境下会自动回退到 HTTP 长轮询,并额外提供断线重连、房间(room)广播等功能。它无法与一个原生的 WebSocket 客户端直接互通,这一点需要特别留意。
轮询、SSE、WebSocket 与 WebTransport 的比较
把数据实时地从服务器送到客户端,WebSocket 远不是唯一的手段:更简单的方案,或是更新的方案,往往才是特定场景下更合适的选择。
| 维度 | 轮询/长轮询 | SSE | WebSocket | WebTransport |
|---|---|---|---|---|
| 方向性 | 由客户端发起,实际效果接近服务器到客户端的单向传输 | 服务器到客户端的单向传输 | 客户端与服务器双向、全双工 | 双向传输,可同时使用多条流与数据报 |
| 多路复用 | 每一轮都要重新发起一次请求 | 单条 HTTP 连接持续承载单向事件流 | 一条 TCP 连接上承载单条逻辑流 | 在 QUIC 之上复用多条流与数据报 |
| 可靠性与顺序 | 按单次请求逐一保证 | TCP 保证顺序,并可通过 Last-Event-ID 实现断点续传 | TCP 保证顺序;断线后的重连需由应用层自行实现 | 可在可靠有序的流与轻量、不保证顺序的数据报之间自由选择 |
| 传输层 | HTTP/TCP | HTTP/TCP | TCP(通过 HTTP Upgrade 建立) | QUIC/UDP |
| 浏览器支持成熟度 | 最为成熟稳定 | 主流浏览器均已支持,但实际使用范围有限 | 所有浏览器均已成熟支持,是目前应用最广泛的方案 | 相对较新,仍在发展中,详见WebTransport 详解 |
相关页面
WebSocket 的思路与本站介绍的其他技术也多有交集:WebTransport 详解是基于 QUIC、同时处理流与数据报的新一代替代方案;Nostr直接把 WebSocket 作为中继通信的通道;NAT 穿透则是找到信令对端、为建立连接打好基础的前提;而在对实时性要求极高的P2P 与元宇宙场景中,这一切最终都建立在 WebSocket 所提供的双向通信能力之上。