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
SSEServer → 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,真正进入后端架构领域的地方。

参考资料