WebSocket 详解:实时双向通信是如何实现的?
前面几篇分别介绍了 TCP/IP、Socket、HTTP 和 HTTPS。
到这里,一个问题自然出现了:
HTTP 已经可以完成客户端和服务器之间的通信,为什么还需要 WebSocket?
因为传统 HTTP 的基本模型是:
Client
│
│ Request
▼
Server
│
│ Response
▼
Client
如果服务器想主动通知客户端,就比较麻烦。
例如:
用户收到新消息
订单状态发生变化
股票价格发生变化
游戏状态发生变化
服务器有数据更新,但客户端没有主动发请求。
传统方案只能不断:
Client → Server:有新消息吗?
Server → Client:没有
Client → Server:有新消息吗?
Server → Client:没有
Client → Server:有新消息吗?
Server → Client:有
这就是轮询。
WebSocket 的目标就是解决这类问题:
Client ⇄ Server
建立连接之后,双方都可以主动发送数据。
RFC 6455 将 WebSocket 定义为一种基于 TCP 的双向通信协议,协议由连接建立握手和数据帧传输两部分组成。
1. WebSocket 到底是什么?
WebSocket 是一个应用层通信协议。
它建立在 TCP 之上:
WebSocket
↓
TCP
↓
IP
如果使用加密连接:
WebSocket
↓
TLS
↓
TCP
↓
IP
对应的 URI Scheme 是:
ws://
wss://
其中:
ws://
类似于 HTTP。
wss://
表示通过 TLS 保护的 WebSocket 连接,类似 HTTPS。RFC 6455 明确规定了 ws 和 wss URI Scheme。
例如:
ws://example.com/chat
或者:
wss://example.com/chat
实际生产环境通常使用:
wss://
因为 WebSocket 中可能传输:
用户身份
聊天内容
Token
业务数据
这些数据通常不能以明文形式传输。
2. WebSocket 和 HTTP 到底是什么关系?
这是最容易产生误解的地方。
WebSocket 不是 HTTP 长连接。
它和 HTTP 有关系,但两者是不同的应用层协议。
可以简单理解:
HTTP
↓
请求 / 响应
WebSocket
↓
双向消息通信
传统 HTTP:
Client ── Request ──> Server
Client <─ Response ── Server
WebSocket:
Client ─────────────> Server
<─────────────
<─────────────
─────────────>
<─────────────
双方都可以在连接建立之后主动发送数据。
RFC 6455 对两者关系的描述非常明确:
WebSocket 是独立的 TCP 协议,与 HTTP 的主要关系是其建立握手可以被 HTTP 服务器解释为 Upgrade 请求。
所以更准确的理解是:
HTTP
↓
用于建立 WebSocket 连接的握手
握手成功
↓
WebSocket 协议
↓
持续双向通信
3. 为什么不用 HTTP 直接实现双向通信?
HTTP 本身当然可以实现一些类似效果。
常见方案包括:
短轮询
长轮询
Server-Sent Events
WebSocket
短轮询
Client ──> Server
↓
没消息
Client ──> Server
↓
没消息
Client ──> Server
↓
有消息
问题是:
大量无效请求
HTTP Header 重复传输
服务器需要处理大量轮询
实时性受轮询间隔影响
长轮询
客户端发起请求:
Client ─────────> Server
服务器暂时不立即返回。
如果有消息:
Server ─────────> Client
然后客户端重新发起请求。
比短轮询更合理,但连接仍然以 HTTP Request/Response 为基本单位。
WebSocket
只建立一次连接:
Client
│
│ 建立连接
▼
Server
│
│ 保持连接
▼
Client ⇄ Server
后续双方可以随时发送数据。
因此,对于真正需要长期双向通信的场景,WebSocket 更合适。
4. WebSocket 是如何建立连接的?
这是 WebSocket 最重要的机制之一。
浏览器执行:
const socket = new WebSocket("wss://example.com/chat");
并不会直接发送 WebSocket Frame。
首先需要建立底层连接,然后进行 WebSocket Opening Handshake。
传统 RFC 6455 握手使用 HTTP/1.1 Upgrade。
客户端会发送类似:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
这里几个 Header 非常重要:
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key
Sec-WebSocket-Version
服务端响应
如果服务端同意建立 WebSocket:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
关键状态码:
101 Switching Protocols
表示:
服务端接受协议切换请求。
从这一刻开始,双方不再按照普通 HTTP Request/Response 模型通信,而是进入 WebSocket 数据传输阶段。
5. Sec-WebSocket-Key 有什么作用?
很多人看到:
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
会以为它是:
WebSocket 加密密钥。
不是。
它主要用于证明服务器确实理解 WebSocket 握手。
服务端需要根据:
Sec-WebSocket-Key
加上 WebSocket 固定 GUID:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
计算 SHA-1,再进行 Base64 编码,生成:
Sec-WebSocket-Accept
简化表示:
Sec-WebSocket-Accept
=
Base64(
SHA1(
Sec-WebSocket-Key
+
GUID
)
)
RFC 6455 对这个计算过程进行了明确规定。
它的作用不是保护业务数据,而是帮助服务器证明:
我确实按照 WebSocket 协议处理了你的握手。
因此不要把:
Sec-WebSocket-Key
理解成:
AES 密钥
或者:
用户认证 Token
它们完全不是一回事。
6. WebSocket 握手之后发生了什么?
握手完成之后:
HTTP
↓
101 Switching Protocols
↓
WebSocket
此时通信变成:
Client ⇄ TCP Connection ⇄ Server
双方可以独立发送消息。
例如:
Client ────────> Server
"hello"
Server ────────> Client
"hello"
Server ────────> Client
"new message"
Client ────────> Server
"received"
这和传统 HTTP 最大的区别就在这里:
WebSocket 建立连接之后,不再要求每次通信都由客户端先发送 HTTP Request。
服务器可以主动发送数据。
7. WebSocket 为什么需要 Frame?
TCP 是:
字节流
没有应用层消息边界。
例如:
Client
↓
TCP Byte Stream
↓
Server
假设应用发送:
hello
world
TCP 并不知道:
hello
是一个消息,
还是:
hell
o
world
三个消息。
因此 WebSocket 在 TCP 之上定义了自己的:
Frame
结构。
大致可以理解为:
TCP
│
├── WebSocket Frame
├── WebSocket Frame
├── WebSocket Frame
└── ...
WebSocket Message 又可以由一个或多个 Frame 组成。RFC 6455 明确区分了 Message 和 Frame,并允许一个 Message 被分片成多个 Frame。
8. WebSocket Frame 有哪些类型?
WebSocket 定义了多种 Opcode。
最常见的是:
0x1 → Text
0x2 → Binary
0x8 → Close
0x9 → Ping
0xA → Pong
因此可以简单理解为:
数据帧
├── Text
└── Binary
控制帧
├── Close
├── Ping
└── Pong
Text
用于发送文本数据。
例如:
{
"type": "message",
"content": "hello"
}
WebSocket 文本消息要求使用 UTF-8。
Binary
用于二进制数据。
例如:
图片
音频
压缩数据
自定义二进制协议
WebSocket 本身并不规定 Binary Payload 的具体业务格式。
应用可以自己定义。
9. Ping、Pong 和心跳
这是 WebSocket 生产环境中非常重要的一部分。
假设:
Client ─────────────── Server
连接长时间没有业务数据。
中间可能存在:
Nginx
Load Balancer
Firewall
NAT
Proxy
这些设备可能因为连接长期空闲而关闭连接。
因此需要检测连接是否仍然有效。
WebSocket 定义:
Ping
Pong
服务端发送:
Ping
客户端返回:
Pong
可以表示:
Server ── Ping ──> Client
Server <─ Pong ─── Client
如果长时间收不到预期响应,就可以认为:
连接可能已经失效
RFC 6455 定义了 Ping/Pong 控制帧,用于协议层的链路检测和响应。
10. Ping/Pong 和业务心跳不是完全一样的
工程实践中经常说:
WebSocket 心跳。
实际上需要区分:
协议层 Ping/Pong
属于 WebSocket 协议本身。
Ping
↓
Pong
业务层心跳
应用自己定义:
{
"type": "heartbeat",
"timestamp": 1760000000
}
然后:
{
"type": "heartbeat_ack"
}
业务心跳可以携带更多信息。
例如:
连接 ID
用户 ID
客户端版本
服务器时间
状态信息
但如果只是判断 WebSocket 连接是否存活,优先考虑协议层 Ping/Pong。
11. WebSocket 如何关闭连接?
WebSocket 不是简单:
TCP close()
协议本身定义了 Close Handshake。
例如:
Client ── Close ──> Server
Client <─ Close ─── Server
然后底层 TCP 连接才最终关闭。
Close Frame 可以携带:
Close Code
Close Reason
常见状态码包括:
1000
Normal Closure
1001
Going Away
1002
Protocol Error
1003
Unsupported Data
1008
Policy Violation
1011
Internal Error
例如:
1000
通常表示:
正常关闭。
所以在生产系统中,不应该把所有断开都简单记录成:
WebSocket disconnected
最好区分:
正常关闭
协议错误
服务器异常
客户端主动关闭
网络断开
心跳超时
这样排查问题会容易很多。
12. WebSocket 断线为什么需要重连?
WebSocket 建立的是一条长期 TCP 连接。
因此它天然会受到网络变化影响。
例如:
Wi-Fi
↓
4G
或者:
客户端睡眠
↓
网络恢复
也可能:
Nginx
↓
连接超时
最终:
TCP Connection
↓
断开
WebSocket 本身不会 magically 让网络恢复。
应用通常需要实现:
断线检测
↓
等待
↓
重新连接
↓
重新认证
↓
重新订阅
13. 为什么重连不能一直立即执行?
假设服务器发生故障。
100 万客户端同时断线。
如果所有客户端立即:
disconnect
↓
reconnect
↓
reconnect
↓
reconnect
服务器恢复之后可能瞬间收到:
100 万个连接请求
这会形成:
Thundering Herd(惊群/连接风暴)。
因此客户端通常使用:
Exponential Backoff
例如:
第 1 次:1 秒
第 2 次:2 秒
第 3 次:4 秒
第 4 次:8 秒
第 5 次:16 秒
同时加入:
Random Jitter
例如:
等待时间 = 基础退避时间 + 随机时间
避免大量客户端在同一时间重新连接。
14. WebSocket 中的数据格式怎么设计?
WebSocket 只负责:
连接
Frame
消息传输
它不会规定你的业务 JSON 长什么样。
所以实际项目通常自己设计消息协议。
例如:
{
"type": "message",
"request_id": "abc123",
"data": {
"content": "hello"
}
}
或者:
{
"type": "notification",
"data": {
"id": 1001,
"title": "订单状态更新"
}
}
推荐至少考虑:
type
request_id
data
timestamp
例如:
{
"type": "order.updated",
"request_id": "01JABC",
"timestamp": 1760000000,
"data": {
"order_id": 10001,
"status": "paid"
}
}
这样可以建立自己的应用层协议。
15. WebSocket 需要做身份认证吗?
当然需要。
WebSocket 连接建立并不意味着:
用户已经认证
实际系统仍然需要解决:
这个连接是谁的?
常见方式包括:
Cookie
Authorization Token
Session
JWT
短期连接 Token
例如建立连接时:
wss://example.com/ws?token=xxxxx
或者通过支持的握手/客户端机制携带认证信息。
但生产系统需要特别注意:
不要把长期有效、敏感的 Token 随意放进 URL。
因为 URL 可能进入:
访问日志
代理日志
监控系统
浏览器历史
更合理的方案通常是使用合适的认证机制,并让 WebSocket 服务端在连接建立阶段完成身份校验。
16. Origin 为什么重要?
浏览器环境中的 WebSocket 还有一个重要概念:
Origin
例如页面来自:
https://app.example.com
浏览器建立 WebSocket 时可能携带:
Origin: https://app.example.com
服务器可以检查:
Origin 是否允许?
例如:
允许:
https://app.example.com
拒绝:
https://evil.example.com
这可以帮助服务端实施基于 Origin 的访问控制。
但需要注意:
Origin 校验不是完整的身份认证机制。
真正的认证仍然应该依靠:
Session
Token
JWT
其他认证凭据
RFC 6455 将 WebSocket 的浏览器安全模型与 Origin 机制结合起来,并明确讨论了 Origin-based security model。
17. WebSocket 与 HTTP/2、HTTP/3 的关系
这里需要稍微注意一些协议细节。
传统 WebSocket RFC 6455 的经典握手是:
HTTP/1.1 Upgrade
也就是:
HTTP/1.1
↓
101 Switching Protocols
↓
WebSocket
后来 RFC 8441 定义了:
通过 HTTP/2 建立 WebSocket 的机制。
HTTP/2 不再简单使用 HTTP/1.1 的 Upgrade 机制,而是使用:
Extended CONNECT
在 HTTP/2 的一个 Stream 上建立 WebSocket。
所以:
HTTP/1.1
↓
Upgrade
↓
WebSocket
和:
HTTP/2
↓
Extended CONNECT
↓
WebSocket
是不同的建立方式。
这也是为什么不能简单说:
WebSocket 就是 HTTP Upgrade。
更准确地说:
经典 WebSocket 使用 HTTP/1.1 Upgrade 建立连接,而后续规范定义了在 HTTP/2 等环境中建立 WebSocket 的机制。
18. WebSocket、SSE、HTTP 该怎么选?
实时通信中经常需要在几个方案之间选择。
| 技术 | 通信方向 | 连接模型 | 典型场景 |
|---|---|---|---|
| HTTP | 请求 → 响应 | 短请求/持久连接 | 普通 API |
| SSE | Server → Client | 长连接 | 实时通知、AI 输出 |
| WebSocket | 双向 | 长连接 | 聊天、协作、游戏 |
| Long Polling | 双向但请求驱动 | 长请求 | 兼容性场景 |
如果需求是:
客户端请求数据
服务器返回结果
使用普通 HTTP 即可。
如果需求是:
服务器不断向浏览器推送事件
SSE 往往更加简单。
如果需求是:
客户端 ⇄ 服务器
双方都需要主动发送
WebSocket 更合适。
例如:
聊天
在线客服
多人协作
实时游戏
实时行情
通常非常适合 WebSocket。
19. WebSocket 在后端架构中是什么位置?
以一个典型系统为例:
Internet
│
▼
Load Balancer
│
┌─────────┴─────────┐
▼ ▼
HTTP API WebSocket
│ │
▼ ▼
Application WS Server
│ │
└─────────┬─────────┘
▼
Redis
│
▼
MySQL
例如用户发送一条聊天消息:
Client A
│
│ WebSocket
▼
WS Server
│
├── 写入消息
│
├── Redis Pub/Sub
│
└── 推送
│
▼
Client B
如果有多个 WebSocket 节点:
Load Balancer
│
┌──────────┼──────────┐
▼ ▼ ▼
WS-1 WS-2 WS-3
│ │ │
└──────────┼──────────┘
▼
Redis
此时一个用户连接在:
WS-1
而消息产生在:
WS-2
就需要通过:
Redis Pub/Sub
Redis Streams
Kafka
RabbitMQ
其他消息系统
进行节点间消息传播。
所以:
WebSocket 本身只解决连接和双向通信,不会自动解决分布式消息路由问题。
这是生产环境设计 WebSocket 服务时非常重要的一点。
20. WebSocket 的几个常见生产问题
连接数
WebSocket 最大特点之一是:
连接长期存在
因此系统需要重点关注:
当前连接数
每台机器连接数
连接建立速率
断开速率
消息吞吐
发送队列
内存
文件描述符
它与传统 HTTP 最大的区别之一就是:
HTTP:
大量短请求
WebSocket:
大量长期连接
因此不能只看:
QPS
还要看:
Concurrent Connections
反向代理超时
如果:
Browser
↓
Nginx
↓
WebSocket Server
Nginx 如果配置了过短的超时时间:
proxy_read_timeout
可能导致长时间没有业务数据的 WebSocket 被关闭。
所以生产环境通常需要:
代理层超时配置
+
Ping/Pong
+
合理心跳
共同保证连接稳定。
消息积压
如果服务器发送速度:
1000 msg/s
而客户端只能处理:
100 msg/s
发送队列就可能不断增长。
最终导致:
内存增长
延迟增加
连接异常
因此生产级 WebSocket 服务必须考虑:
背压
发送队列
消息丢弃策略
连接限速
单连接消息上限
21. 从 TCP 到 WebSocket,把整个网络系列串起来
第一层:TCP/IP
解决:
数据如何跨网络传输?
IP
TCP
UDP
第二层:Socket
解决:
应用程序如何使用网络通信能力?
例如:
socket()
bind()
listen()
accept()
connect()
read()
write()
第三层:HTTP
解决:
客户端和服务器如何进行 Web 请求/响应?
GET
POST
PUT
DELETE
Header
Body
Status Code
第四层:HTTPS
解决:
HTTP 通信如何保证安全?
TLS
Certificate
Encryption
Authentication
Integrity
第五层:WebSocket
解决:
客户端和服务器如何进行长期双向通信?
Handshake
Frame
Text
Binary
Ping
Pong
Close
最终可以画成:
┌──────────────────────────────┐
│ HTTP / WebSocket │
│ 应用层协议 │
├──────────────────────────────┤
│ TLS │
│ HTTPS / WSS 安全保护 │
├──────────────────────────────┤
│ TCP │
│ 可靠字节流 │
├──────────────────────────────┤
│ IP │
│ 网络寻址与路由 │
└──────────────────────────────┘
WebSocket 与 TCP 的关系尤其值得记住:
WebSocket
↓
TCP 长连接
↓
Frame
↓
Message
而不是:
WebSocket = HTTP 长轮询
22. 总结
WebSocket 的核心并不复杂。
它解决的是:
如何让 Web 应用在建立连接之后,实现客户端和服务器之间持续的双向通信。
核心过程可以记成:
① 建立 TCP 连接
↓
② HTTP/1.1 WebSocket Upgrade
↓
③ 101 Switching Protocols
↓
④ WebSocket Connection
↓
⑤ Frame 双向传输
↓
⑥ Ping/Pong 保活
↓
⑦ Close Handshake
↓
⑧ TCP 连接关闭
其中最重要的几个概念:
WebSocket
→ 双向通信协议
ws://
→ 普通 WebSocket
wss://
→ TLS 加密的 WebSocket
Upgrade
→ 经典 HTTP/1.1 握手方式
Frame
→ WebSocket 在线上的基本传输单位
Message
→ 应用层看到的消息
Ping/Pong
→ 协议层连接检测
Close
→ WebSocket 关闭握手
Origin
→ 浏览器场景下的重要安全控制
Reconnect
→ 应用层需要自行设计的连接恢复机制
对于后端开发者来说,WebSocket 最值得掌握的不是 API 本身,而是背后的连接模型:
HTTP:
一次请求 → 一次响应
WebSocket:
一次连接 → 长期双向通信
当连接数量从几十增长到几万、几十万时,真正需要考虑的问题就从“如何发送消息”变成:
连接管理
心跳与超时
断线重连
负载均衡
消息路由
Redis / MQ
背压
连接数
内存
文件描述符
故障转移
这也是 WebSocket 从一个简单的前端 API,真正进入后端架构领域的地方。