一文吃透 WebSocket 原理
Category(分类): NET Protocol Status: 未知
本文尽量保留原文的章节和示例,并补充 RFC 6455、HTTP/2、HTTP/3、心跳、断线重连和浏览器安全模型的现代说明。
一、什么是 WebSocket
WebSocket 是一种支持双向通信的应用层协议。经典 WebSocket 在一条 TCP 连接上进行全双工通信,允许客户端和服务端在连接建立后随时发送消息。
在浏览器中,WebSocket 通常先通过 HTTP/1.1 Upgrade 握手建立协议切换,然后在同一连接上使用 WebSocket 帧传输数据。这个过程并不是“只进行一次网络握手”:底层可能还包括 TCP 三次握手,以及 wss:// 场景下的 TLS 握手。
WebSocket 本质上是计算机网络应用层协议,用来提供比传统 HTTP 请求—响应模型更适合实时双向消息的通信方式。它不是 HTTP 的简单长连接模式,也不是 TCP Socket API 的直接替代品。
WebSocket 协议在 2011 年由 RFC 6455 标准化。RFC 7936 主要澄清了 WebSocket 子协议名称的注册流程,并不是 WebSocket 核心协议的新版。现在主流浏览器普遍支持 WebSocket,但实际支持情况仍应以目标浏览器、运行环境和网络设备为准。

WebSocket 的主要特点
- 建立在可靠传输之上:经典 RFC 6455 WebSocket 运行在 TCP 上,现代也可以通过 HTTP/2 或 HTTP/3 承载;
- 双向通信:客户端和服务端都可以主动发送消息;
- 握手兼容 HTTP:HTTP/1.1 场景使用 Upgrade 机制,便于经过常见代理和网关;
- 支持文本和二进制数据:消息帧可以承载 UTF-8 文本或任意二进制内容;
- 协议开销相对较小:建立连接后不需要每条消息都携带完整 HTTP 请求头,但每个 WebSocket 帧仍有帧头,浏览器发送的帧还需要掩码;
- URL 方案为
ws和wss:wss表示通过 TLS 保护的 WebSocket; - 支持子协议协商:客户端和服务端可以通过
Sec-WebSocket-Protocol约定应用层消息格式。
ws://example.com:80/some/path
wss://example.com:443/some/path
ws 默认端口通常为 80,wss 默认端口通常为 443,但两者都可以使用显式端口。
WebSocket 不等于“没有同源限制”
浏览器可以发起跨源 WebSocket 握手,且 WebSocket 不使用普通 CORS 预检流程,但浏览器会在握手中发送 Origin。服务端必须根据 Origin、用户身份、Cookie、Token 和权限明确决定是否接受连接,不能因为 WebSocket “没有同源限制”就允许任意网站连接。
生产环境通常使用 wss://,并在服务端校验 Origin 和认证信息。WebSocket 的掩码也不是加密,不能代替 TLS。
二、为什么需要 WebSocket?
我们已经有了 HTTP 协议,为什么还需要 WebSocket?
传统 HTTP 的主要交互模式是客户端发起请求,服务端返回响应。如果服务端有新的实时状态变化,客户端通常需要:
- 定时轮询;
- 长轮询;
- SSE 单向推送;
- 或建立其他长连接机制。

轮询会带来额外的请求头和请求处理开销。长轮询可以减少空响应,但仍然需要不断重新建立请求;SSE 适合服务端到客户端的单向事件流,但客户端到服务端仍需要单独请求。
WebSocket 适合在一条长期连接上进行客户端和服务端的双向消息传输,例如聊天室、协同编辑、实时行情和多人游戏。
需要注意,HTTP 并不是完全没有持久通信能力:HTTP/1.1 支持持久连接,HTTP/2 支持多路复用,HTTP 还可以使用流式响应。WebSocket 解决的是任意时刻的双向消息通信,而不是简单解决 TCP 连接复用。
三、WebSocket 与 HTTP 的区别

相同点
- 都属于应用层通信协议;
- 经典 WebSocket 和 HTTP/1.1 都可以运行在 TCP 之上;
wss://和 HTTPS 一样可以使用 TLS 保护传输;- 都需要考虑认证、授权、超时、代理和连接关闭。
联系
在经典 HTTP/1.1 模式下,WebSocket 使用 HTTP 请求发起握手,服务端通过 101 Switching Protocols 同意升级。握手完成后,双方不再按照普通 HTTP 请求—响应格式传输,而是使用 WebSocket 帧。
HTTP/2 和 HTTP/3 使用不同的引导方式:
- HTTP/2 使用 RFC 8441 定义的 Extended CONNECT;
- HTTP/3 使用 RFC 9220 定义的 Extended CONNECT;
- 这两种方式不使用 HTTP/1.1 的
Connection: Upgrade和101 Switching Protocols,而是把 WebSocket 放到对应的 HTTP Stream 中。

主要区别
- HTTP 传统上是请求—响应模型;WebSocket 建立后支持双方主动发送消息;
- HTTP 消息通常具有请求和响应语义;WebSocket 使用独立的消息帧;
- HTTP/1.1 可以复用连接,但不能让服务端在没有对应请求的情况下随意发送普通响应;
- WebSocket 适合实时双向消息,但需要长期占用连接和服务端资源;
- HTTP/2 Server Push 不是通用双向消息通道,且现代浏览器对其支持已经很有限。
四、WebSocket 协议的原理
1. 经典 HTTP/1.1 WebSocket 握手
下面是典型的 RFC 6455 握手请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: https://example.com
这些字段的作用如下:
Upgrade: websocket:请求升级到 WebSocket;Connection: Upgrade:声明该 HTTP/1.1 连接正在请求协议升级;Sec-WebSocket-Key:客户端生成的随机 nonce,通常是 16 字节随机值的 Base64 表示,不是加密密钥;Sec-WebSocket-Protocol:客户端提供的子协议候选列表;Sec-WebSocket-Version: 13:表示 RFC 6455 使用的 WebSocket 版本;Origin:浏览器提供的来源,服务端应根据它进行访问控制;Host、路径和其他首部:仍然遵循 HTTP/1.1 请求格式。
服务端接受后返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Protocol 必须是客户端提供的候选值之一。服务端也可以不返回它,表示没有协商子协议。
2. Sec-WebSocket-Accept 如何计算
服务端不是把 Sec-WebSocket-Key 加密后返回,而是按照固定算法计算:
accept = Base64(
SHA-1(
Sec-WebSocket-Key 的原始 Base64 字符串
+ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
)
)
客户端收到响应后重新计算并比较 Sec-WebSocket-Accept。这个值用于确认服务端理解 WebSocket 握手,不是服务器身份认证,也不是加密过程。
真正的服务器身份认证通常由 wss:// 连接中的 TLS 证书、域名匹配和信任链完成。
3. 握手完成后使用 WebSocket 帧
HTTP/1.1 握手成功后,双方开始传输 WebSocket Frame。WebSocket 帧包含 FIN、RSV、Opcode、Mask、Payload Length、Masking Key 和 Payload 等字段。
常见 Opcode 包括:
0x1:文本帧;0x2:二进制帧;0x0:连续帧,用于消息分片;0x8:Close;0x9:Ping;0xA:Pong。
一条较大的消息可以被拆成多个帧,接收端根据 FIN 和连续帧重新组装消息。控制帧不能被分片,且载荷长度不能超过 125 字节。
4. 掩码机制
RFC 6455 要求:
- 客户端发送给服务端的每个 WebSocket 帧都必须使用掩码;
- 服务端发送给客户端的帧不能使用掩码;
- 客户端每个帧应使用新的、不可预测的 32 位掩码密钥;
- 掩码通过 XOR 改变帧载荷,不能提供保密性。
掩码主要用于避免恶意浏览器脚本构造出可被早期中间缓存误识别的字节流,不能替代 wss:// 的 TLS 加密。
5. 关闭连接
应用可以调用:
ws.close(1000, 'normal closure')
WebSocket Close 帧可以携带状态码和 UTF-8 原因。常见状态码包括:
1000:正常关闭;1001:端点离开,例如页面关闭;1002:协议错误;1003:不支持的数据类型;1008:违反策略;1011:服务端遇到异常。
发送 Close 后,端点不应继续发送应用数据。收到 Close 后,如果本端还没有发送 Close,应回复 Close,随后关闭底层连接。
五、WebSocket 的优缺点
优点
- 一次握手后可以长期复用连接;
- 支持客户端和服务端双向主动发送消息;
- 比重复建立 HTTP 请求减少了请求头和轮询开销;
- 支持文本和二进制数据;
- 适合实时交互和事件驱动场景。
缺点
- 需要长期占用服务端连接、文件描述符、内存和事件循环资源;
- 断线、重连、心跳、消息确认和状态恢复需要应用自己设计;
- 代理、负载均衡器、防火墙和 NAT 可能对长时间空闲连接设置超时;
- 浏览器 WebSocket API 的协议级 Ping/Pong 能力有限;
- 消息顺序、重复处理、背压和大消息限制需要应用控制;
- 不适合所有请求,单次读取或低频查询使用普通 HTTP 更简单。
六、WebSocket 应用场景
- 即时聊天通信;
- 多人在线游戏;
- 在线协同编辑;
- 实时数据流拉取与推送;
- 体育比赛或游戏实况;
- 实时地图位置;
- 交易行情和价格变化;
- 需要双向控制的实时 Web 应用。
不能只因为“实时”就使用 WebSocket
如果只需要服务端向客户端单向推送事件,SSE 可能更简单;如果只是获取一次数据,普通 HTTP 请求就足够;如果需要可靠的双向消息,可以使用 WebSocket;如果需要更复杂的低延迟、数据报或 HTTP/3 能力,也可以评估 WebTransport。
选择协议时应考虑:
- 通信方向;
- 是否需要消息边界;
- 是否需要可靠和有序传输;
- 是否需要广播或组播;
- 客户端和代理兼容性;
- 长连接数量和服务端资源;
- 认证、重连和状态恢复成本。
七、WebSocket 断线、心跳和重连
1. 心跳的作用
心跳通常用于:
- 检测连接是否仍然可用;
- 让中间代理和 NAT 看到连接仍有流量;
- 及时发现半开连接;
- 统计业务在线状态。
WebSocket 协议本身定义了 Ping/Pong 控制帧。服务端可以发送协议级 Ping,客户端会按照协议回复 Pong;但浏览器 JavaScript 通常不能直接调用协议级 Ping/Pong API,因此浏览器客户端经常使用应用层消息模拟:
客户端 -> 服务端:{"type":"ping"}
服务端 -> 客户端:{"type":"pong"}
应用层心跳消息不能与协议级 Ping 帧混为一谈。
TCP SO_KEEPALIVE 也可以检测部分连接故障,但它的默认时间、探测次数和行为由操作系统配置,不一定是 2 小时,而且不能替代业务层心跳。
2. 心跳检测流程
一个较完整的心跳流程可以是:
- 连接建立后启动心跳定时器;
- 客户端在连接仍处于
OPEN状态时发送应用层 ping,或由服务端发送协议 Ping; - 收到对应 pong 后记录最近活跃时间;
- 超过允许时间没有收到响应,则主动关闭连接;
close或error事件触发清理和重连逻辑。
示意代码如下:
let heartbeatTimer
let timeoutTimer
let socket
function startHeartbeat() {
clearTimeout(heartbeatTimer)
clearTimeout(timeoutTimer)
heartbeatTimer = setTimeout(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) return
socket.send(JSON.stringify({ type: 'ping' }))
timeoutTimer = setTimeout(() => {
socket.close(4000, 'heartbeat timeout')
}, 10_000)
}, 30_000)
}
function handleMessage(event) {
const message = JSON.parse(event.data)
if (message.type === 'pong') {
clearTimeout(timeoutTimer)
startHeartbeat()
return
}
startHeartbeat()
// 处理其他业务消息
}
实际项目还要处理消息格式错误、心跳并发、页面隐藏、服务端超时、代理超时和连接关闭等情况。心跳间隔应小于最关键中间设备的空闲超时,但不能过于频繁地制造无用流量。
3. 断线重连
WebSocket API 没有内置自动重连。重连时建议:
- 监听
close和error; - 清理旧连接的定时器和事件处理器;
- 使用指数退避和随机抖动,避免大量客户端同时重连;
- 设置最大重试次数或等待用户重新操作;
- 重连后重新认证并恢复订阅;
- 使用消息 ID、序列号或同步接口补偿断线期间的消息;
- 防止重复发送造成重复业务操作;
- 页面
pagehide时主动关闭,恢复后再创建连接。
let reconnectAttempt = 0
let reconnectTimer
function connect() {
socket = new WebSocket('wss://example.com/socket')
socket.onopen = () => {
reconnectAttempt = 0
startHeartbeat()
// 重新认证、订阅和同步状态
}
socket.onmessage = handleMessage
socket.onclose = () => {
clearTimeout(heartbeatTimer)
clearTimeout(timeoutTimer)
const delay = Math.min(30_000, 1_000 * 2 ** reconnectAttempt)
const jitter = Math.random() * 500
reconnectAttempt += 1
reconnectTimer = setTimeout(connect, delay + jitter)
}
}
重连代码只是示意。实际应用应区分正常关闭和异常断线,并根据认证失败、服务器拒绝、网络断开等原因采取不同策略。
4. Nginx 和代理超时
Nginx、负载均衡器或防火墙可能对空闲 WebSocket 连接设置超时。解决方式通常包括:
- 正确配置 HTTP/1.1 Upgrade 转发;
- 配置合适的读取超时和连接超时;
- 使用协议级或应用层心跳;
- 在多个实例之间配置连接路由或共享会话状态;
- 监控连接数、关闭原因、心跳延迟和重连次数。
心跳只能帮助发现或减少空闲超时,不能修复网络断开、服务端崩溃或认证失效等问题。
八、总结
- WebSocket 是面向实时双向消息的应用层协议;
- 经典 WebSocket 使用 HTTP/1.1 Upgrade 建立在 TCP 上,现代也可以通过 HTTP/2 和 HTTP/3 承载;
- WebSocket 握手完成后使用帧传输文本、二进制和控制消息;
Sec-WebSocket-Key是随机 nonce,Sec-WebSocket-Accept是 SHA-1 + GUID + Base64 的校验结果,不是加密;- 客户端帧必须掩码,服务端帧不能掩码,掩码不提供机密性;
- 浏览器可以跨源发起 WebSocket,但服务端必须验证 Origin 和权限;
- 协议级 Ping/Pong 与浏览器应用层
ping/pong消息不是一回事; - WebSocket 需要设计心跳、重连、认证、背压和断线后的状态恢复;
- 如果只需要单向事件推送,可以优先评估 SSE;
- 如果只是普通查询,HTTP/1.1、HTTP/2 或 HTTP/3 通常更简单。