编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 计算机原理

  • 网络协议

    • README
    • 通过看新闻熟悉网络
    • 通过购物熟悉加密
    • 从0到1部书电商网站
    • 请求网络的通用流程
    • 网络编程模型的概念
    • 传输协议TCP和UDP
    • Socket的发展和设计
    • 传输数据的设计思想
    • 网络域名解析的流程
    • HTTP服务设计流程
    • HTTP协议设计思想
    • HTTPS协议设计策略
    • HTTP连接和跳转
    • HTTP代理和缓存设计
    • 如何去排查网络故障
    • WebSocket实时通信
      • 01.工作案例引入
        • 1.1 一个"消息已读"功能的技术选型之路
        • 1.2 选型背后的实时通信知识图谱
      • 02.为什么需要WebSocket
        • 2.1 HTTP的单向限制
        • 2.2 短轮询的代价
        • 2.3 长轮询的改进
        • 2.4 SSE单向推送
        • 2.5 四种方案对比
      • 03.WebSocket握手与协议升级
        • 3.1 HTTP升级机制
        • 3.2 握手的完整流程
        • 3.3 Sec-WebSocket-Key/Accept的计算
        • 3.4 扩展协商
      • 04.WebSocket帧格式深度解析
        • 4.1 帧结构概览
        • 4.2 各字段含义
        • 4.3 控制帧:Close/Ping/Pong
        • 4.4 掩码设计的深意
        • 4.5 分片传输
      • 05.心跳与保活机制
        • 5.1 为什么需要心跳
        • 5.2 Ping/Pong帧的使用
        • 5.3 中间代理的超时
        • 5.4 重连策略设计
      • 06.服务端架构设计
        • 6.1 连接管理
        • 6.2 消息广播
        • 6.3 水平扩展
        • 6.4 背压处理
      • 07.安全与优化
        • 7.1 WebSocket安全风险
        • 7.2 消息压缩
        • 7.3 二进制传输
        • 7.4 连接数优化
      • 08.综合案例:从0到1搭建实时聊天系统
        • 8.1 案例背景与目标
        • 8.2 第一代:短轮询——能跑就行
        • 8.3 第二代:长轮询——假装实时
        • 8.4 第三代:WebSocket——真正的全双工
        • 8.5 第四代:生产级WebSocket——全场景覆盖
        • 8.6 四种方案横向对比
        • 8.7 案例升华:WebSocket的设计哲学
        • 8.8 全文知识图谱回顾
      • 09.思考题与作业
        • 9.1 基础思考题
        • 9.2 进阶思考题
        • 9.3 动手作业
    • HTTP3与QUIC协议
  • 操作系统

  • 数据库原理

  • 计算机
  • 网络协议
杨充
2020-06-18
目录

WebSocket实时通信

# 16.WebSocket实时通信

# 目录介绍

  • 01.工作案例引入
    • 1.1 一个"消息已读"功能的技术选型之路
    • 1.2 选型背后的实时通信知识图谱
  • 02.为什么需要WebSocket
    • 2.1 HTTP的单向限制
    • 2.2 短轮询的代价
    • 2.3 长轮询的改进
    • 2.4 SSE单向推送
    • 2.5 四种方案对比
  • 03.WebSocket握手与协议升级
    • 3.1 HTTP升级机制
    • 3.2 握手的完整流程
    • 3.3 Sec-WebSocket-Key/Accept的计算
    • 3.4 扩展协商
  • 04.WebSocket帧格式深度解析
    • 4.1 帧结构概览
    • 4.2 FIN/RSV/Opcode/MASK/Payload
    • 4.3 控制帧:Close/Ping/Pong
    • 4.4 掩码设计的深意
    • 4.5 分片传输
  • 05.心跳与保活机制
    • 5.1 为什么需要心跳
    • 5.2 Ping/Pong帧的使用
    • 5.3 中间代理的超时
    • 5.4 重连策略设计
  • 06.服务端架构设计
    • 6.1 连接管理
    • 6.2 消息广播
    • 6.3 水平扩展
    • 6.4 背压处理
  • 07.安全与优化
    • 7.1 WebSocket安全风险
    • 7.2 消息压缩
    • 7.3 二进制传输
    • 7.4 连接数优化
  • 08.综合案例:从0到1搭建实时聊天系统
    • 8.1 案例背景与目标
    • 8.2 第一代:短轮询(能跑就行)
    • 8.3 第二代:长轮询(假装实时)
    • 8.4 第三代:WebSocket(真正的全双工)
    • 8.5 第四代:生产级WebSocket(心跳+重连+分布式)
    • 8.6 四种方案横向对比
    • 8.7 案例升华:WebSocket的设计哲学
    • 8.8 全文知识图谱回顾
  • 09.思考题与作业
    • 9.1 基础思考题
    • 9.2 进阶思考题
    • 9.3 动手作业

# 01.工作案例引入

# 1.1 一个"消息已读"功能的技术选型之路

场景:小钱是一名后端工程师,负责公司社交 App 的即时通讯模块。产品提了一个需求——"对方看到你发的消息后,你要能立刻看到'已读'标记"。比如微信里那条"对方正在输入..."和"已读"的小字。

小钱的第一反应很简单——客户端在打开聊天页面时,调一个 POST /api/messages/read 标记已读,服务端推送一个"消息已读"通知给对方客户端。问题来了:服务端怎么"推送"给客户端? HTTP 是请求-响应模型,服务端不能主动给客户端发消息。

小钱开始了技术选型。

方案A:短轮询。客户端每隔 2 秒发一个 GET /api/messages/updates,问"有没有新消息"。服务端查一下 Redis,有就返回,没有就返回空。上线第一天,测试反馈——"消息已读"的更新延迟最高 2 秒,体感明显。而且 App 后台时还在轮询,一晚上耗了 30% 的电。

方案B:长轮询。客户端发请求,服务端不立即返回,而是"hold 住"这个连接——直到有新消息时才返回。客户端收到响应后立刻发起下一个长轮询请求。延迟降到了毫秒级,但运维发现——10 万在线用户,服务端维持了 10 万个"悬空"的 HTTP 连接(每个线程 hold 一个),线程池资源吃紧。

方案C:WebSocket。小钱用 ws 库搭建了一个 WebSocket 服务端,客户端建立 WebSocket 连接后,服务端可以随时推送消息。延迟 < 50ms,一个服务器 2GB 内存轻松支撑 5 万并发连接。但新问题又来了——用户切到后台后,iOS 系统会在几分钟后断开 WebSocket 连接,切回来时需要重新建立连接并拉取离线消息。

方案D:WebSocket + 心跳 + 重连 + 离线消息。小钱在 WebSocket 基础上加了 30 秒心跳检测,监听网络状态变化自动重连,服务端在连接断开期间缓存离线消息,重连后批量推送。

疑惑链条:

  • "HTTP 为什么不能服务端主动推送?" → HTTP 是请求-响应模型,TCP 连接在响应完成后就释放(或进入 Keep-Alive 等待下一个请求),服务端没有"偷跑"消息的通道
  • "短轮询和长轮询差在哪?" → 短轮询是客户端定时问"有新消息吗"→ 大部分问都是白问;长轮询是服务端 hold 住连接 → 有新消息才回 → 减少了无意义的空请求
  • "长轮询为什么不能取代 WebSocket?" → 长轮询每个连接对应一个线程 → 10 万在线 = 10 万线程 = 不可行。WebSocket 用事件驱动模型,一个线程可以管理几万个连接
  • "WebSocket 断了怎么办?" → TCP 连接不是永不失联的 → 心跳检测断连 → 自动重连 → 拉取离线期间的消息补漏
  • "为什么 WebSocket 心跳间隔通常用 30 秒而不是 5 秒或 5 分钟?" → 5 秒太频繁,耗电+浪费带宽;5 分钟太长,中间代理(NAT/防火墙/Nginx)可能在 1~2 分钟内断开空闲连接。30 秒是"安全且不浪费"的甜点

小钱这一串问题,本质都是在问:WebSocket 和 HTTP 到底有什么不同?它怎么做到全双工的?心跳为什么要那样设计?如何从玩具级进化到生产级?——这正是"WebSocket 实时通信"要回答的。

# 1.2 选型背后的实时通信知识图谱

四种方案在服务端资源模型中的位置:

短轮询:
  客户端 ──请求──→ 服务端(立即查DB/Redis,立刻返回)
  循环:每2秒一次 → 浪费带宽  ×  低延迟  ×

长轮询:
  客户端 ──请求──→ 服务端(hold住,有消息才返回)
  返回后立即发起下一个 → 低延迟  ✓  服务端压力  ×

WebSocket:
  客户端 ←──TCP全双工连接──→ 服务端
  服务端随时推送 → 低延迟  ✓  低开销  ✓  复杂度  △

生产级WebSocket:
  客户端 ←──TCP全双工连接──→ 服务端(心跳+重连+离线消息)
  全场景覆盖 → 低延迟  ✓  低开销  ✓  高可用  ✓
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

四类方案与后续章节的映射关系:

方案 延迟 服务端开销 对应章节
短轮询 ~2000ms 低 2.2
长轮询 ~50ms 高 2.3
基础WebSocket ~10ms 极低 3/4章
生产级WebSocket ~10ms 极低+ 5/6/7章

本章的主线就是沿着这四种方案的进化,一层一层拆解 WebSocket 的握手升级、帧格式、心跳保活和服务端架构。读完之后,你不仅能理解 WebSocket 的原理,还能搭建一个生产级的实时通信系统。

# 02.为什么需要WebSocket

# 2.1 HTTP的单向限制

HTTP 是请求-响应模型:客户端发请求,服务器才回响应。服务器不能主动给客户端发消息。

HTTP的局限:

  客户端 ──请求──→ 服务器
  客户端 ←──响应── 服务器

  ✗ 服务器不能主动推送数据给客户端

对于实时应用(聊天、股票行情、在线游戏)来说,这是致命的限制。
1
2
3
4
5
6
7
8

疑惑:HTTP/2 有 Server Push,能不能替代 WebSocket?

答疑:HTTP/2 Server Push 只能推送资源(如 CSS/JS 文件)——它在浏览器请求 HTML 前就把关联的 CSS 推过去。但它不能推送动态消息——"有人给你发了一条新微信"这不是"资源",Server Push 无法处理这类场景。Server Push 解决的是"静态资源预加载"问题,WebSocket 解决的是"双向实时通信"问题——两者的目标完全不同。

# 2.2 短轮询的代价

短轮询是最简单粗暴的方案:客户端每隔 N 秒发一次 HTTP 请求,问"有没有新消息"。

// 短轮询——最朴素的实现
setInterval(async () => {
  const res = await fetch('/api/messages/updates');
  const data = await res.json();
  if (data.newMessages.length > 0) {
    console.log('有新消息:', data.newMessages);
  }
}, 2000); // 每2秒查一次
1
2
3
4
5
6
7
8
短轮询分析(10万在线用户,每2秒轮询一次):

服务端QPS:      10万 / 2秒 = 5万 QPS
有效请求比例:    假设平均每分钟1条新消息
                 每分钟30次轮询,只有1次有数据
                 有效比例 = 1/30 ≈ 3.3%
                 96.7%的请求是白发的!
带宽消耗:        每次请求+响应约500字节(含HTTP头)
                 5万 × 500B = 25MB/s = 200Mbps
                 200Mbps只是"能跑"的带宽,还没算TLS和TCP开销
1
2
3
4
5
6
7
8
9
10

回到小钱的方案A:短轮询在 10 万在线时会产生 5 万 QPS 的无效请求,而且每条消息的最大延迟 = 轮询间隔(2 秒),用户体感明显。

# 2.3 长轮询的改进

长轮询的改进思路:客户端发请求,服务端不立即返回,而是"hold 住"——直到有新消息时才返回。客户端收到响应后立即发起下一个长轮询。

// 服务端长轮询处理(Node.js 示例)
app.get('/api/messages/updates', async (req, res) => {
  const userId = req.user.id;
  
  // 等待新消息(带超时)
  const timeout = 30000; // 30秒超时
  const result = await waitForNewMessage(userId, timeout);
  
  if (result) {
    res.json({ newMessages: result.messages });
  } else {
    res.json({ newMessages: [] }); // 超时,返回空
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
长轮询优势:
  ✅ 延迟低:有消息立即返回(&lt;50ms)
  ✅ 无无效请求:hold住连接直到有消息
  ✅ 兼容性好:纯HTTP,所有代理/防火墙都支持

长轮询劣势(对应方案B):
  ❌ 服务端资源:每个hold住的请求占用一个线程
     10万用户 = 10万线程 = 10万MB栈空间 = 100GB内存!
  ❌ 队头阻塞:同一个客户端的请求是串行的
     如果服务端 hold 住请求A,无法同时处理请求B
  ❌ 中间代理超时:Nginx默认60秒,超出后可能断开连接
1
2
3
4
5
6
7
8
9
10
11

# 2.4 SSE单向推送

SSE(Server-Sent Events)是 HTML5 的标准:服务端向客户端单向推送文本事件流。

// 客户端
const evtSource = new EventSource('/api/events');
evtSource.onmessage = (event) => {
  console.log('收到消息:', JSON.parse(event.data));
};

// 服务端
res.writeHead(200, {
  'Content-Type': 'text/event-stream',
  'Connection': 'keep-alive',
  'Cache-Control': 'no-cache',
});
res.write(`data: ${JSON.stringify(msg)}\n\n`);
1
2
3
4
5
6
7
8
9
10
11
12
13

SSE 的限制:仅支持文本、仅单向(服务端→客户端)、浏览器支持不如 WebSocket 普及。

# 2.5 四种方案对比

方案 方向 协议 延迟 服务端开销 适用场景
短轮询 客户端→服务端 HTTP 秒级 低(每次请求独立) 不需要实时的场景
长轮询 客户端→服务端 HTTP 毫秒级 极高(hold连接) 兼容性要求高的场景
SSE 服务端→客户端单向 HTTP 毫秒级 中(持久连接) 单向推送(新闻、股票)
WebSocket 双向全双工 WebSocket 毫秒级 极低(事件驱动) 聊天、游戏、协同编辑

# 03.WebSocket握手与协议升级

# 3.1 HTTP升级机制

WebSocket 不是从零开始的新协议——它巧妙地利用了 HTTP 的 Upgrade 机制,通过一次 HTTP 请求"升级"为 WebSocket 连接。这意味着 WebSocket 连接可以复用 80/443 端口,可以穿越现有的 HTTP 代理和防火墙。

WebSocket 协议栈:

  应用层:    WebSocket 协议(帧/消息)
  传输层:    TCP
  网络层:    IP

  和 HTTP 共享传输层和网络层,只是应用层协议不同
1
2
3
4
5
6
7

# 3.2 握手的完整流程

WebSocket握手过程:

客户端发送HTTP升级请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket                    ← 请求升级为WebSocket
Connection: Upgrade                   ← 使用Upgrade机制
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==   ← 随机16字节Base64
Sec-WebSocket-Version: 13             ← 协议版本(目前最新是13)
Sec-WebSocket-Protocol: chat, superchat    ← 可选的子协议
Sec-WebSocket-Extensions: permessage-deflate  ← 可选的扩展

服务端同意升级:
HTTP/1.1 101 Switching Protocols      ← 101状态码:协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← Key的SHA-1+Base64
Sec-WebSocket-Protocol: chat          ← 服务端选择的子协议

之后的通信就不再是HTTP,而是WebSocket二进制帧协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

疑惑:握手为什么还要走 HTTP 格式?直接用二进制握手不行吗?

答疑:HTTP 握手的核心价值是"兼容性":

  1. 复用 80/443 端口,不需要新开端口
  2. 可以穿越 HTTP 代理、Nginx 反向代理、CDN
  3. 可以在 HTTP 层面做认证、路由、负载均衡(Nginx 可以基于握手中的 Host 头做路由)
  4. 如果升级失败,可以优雅降级回 HTTP

# 3.3 Sec-WebSocket-Key/Accept的计算

这是握手过程中最核心的安全设计——防止跨协议攻击。

Sec-WebSocket-Accept 的计算:

1. 服务端取客户端发来的 Sec-WebSocket-Key
   例:dGhlIHNhbXBsZSBub25jZQ==

2. 拼接固定的魔术字符串(RFC 6455 定义):
   dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

3. 计算 SHA-1 哈希:
   SHA-1 → b37a4f2cc0624f1690f64606cf385945b2bec4ea

4. Base64 编码:
   s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
1
2
3
4
5
6
7
8
9
10
11
12
13

为什么需要这个魔术字符串?

场景:攻击者在浏览器中构建一个恶意请求,试图让 HTTP 服务器把它误
认为是合法的 WebSocket 握手,从而建立非预期的全双工连接。

没有 Key/Accept 机制:
  攻击者可以抓取一个合法的 WebSocket 请求,重放给服务器
  
有 Key/Accept 机制:
  Sec-WebSocket-Key 是随机生成的 → 服务端用特定算法处理后返回
  Sec-WebSocket-Accept
  → 确认服务端"理解并同意"WebSocket Upgrade
  → 这不是 HTTP 服务器意外触发的行为
  → 而是服务端明确地选择了升级
1
2
3
4
5
6
7
8
9
10
11
12

魔术字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是 RFC 6455 固定定义的 GUID。使用固定字符串确保只有理解 WebSocket 协议的服务器才能计算出正确的 Accept 值。

# 3.4 扩展协商

WebSocket 支持在握手时协商扩展(Extensions):

客户端请求:
  Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15

服务端确认:
  Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15

permessage-deflate:对每条消息进行 deflate 压缩
  → 减少带宽消耗(文本消息可压缩 60~80%)
  → 增加 CPU 开销(每条消息需要压缩/解压)
  → 权衡:高延迟网络中值得,内网低延迟场景不必要
1
2
3
4
5
6
7
8
9
10

# 04.WebSocket帧格式深度解析

# 4.1 帧结构概览

WebSocket 通信不再是 HTTP 文本协议,而是二进制帧协议。这是 WebSocket 低开销的关键——HTTP/1.1 每次请求都要携带几百字节的文本头部,WebSocket 的帧头最少只有 2 字节。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if payload len == 127  |
+-------------------------------+-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------+-------------------------------+
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最小帧:

[FIN=1, Opcode=1(文本)] [MASK=1, Len=5] [Masking-key(4B)] [Payload(5B)]
             1字节                 1字节          4字节          5字节
帧头12字节 + 数据5字节 = 17字节
1
2
3

# 4.2 各字段含义

字段 位数 含义
FIN 1位 是否为消息的最后一个分片。1=最后一帧/单帧消息
RSV1/2/3 各1位 保留位,用于扩展(如压缩)
Opcode 4位 帧类型:0x1文本/0x2二进制/0x8关闭/0x9 Ping/0xA Pong
MASK 1位 是否使用掩码。客户端→服务端必须为1,服务端→客户端必须为0
Payload Len 7位 载荷长度:0~125=实际长度,126=后续2字节,127=后续8字节
Masking-Key 0或4字节 掩码密钥,仅当MASK=1时存在
Payload Data 可变 实际数据

Payload Len 的三种编码:

Len = 0~125:    直接用这7位表示长度
                  例:Len=5 → Payload 5字节

Len = 126:       后续2字节表示长度(16位无符号整数,网络字节序)
                  例:0x7E 0x01 0x00 → Payload 256字节

Len = 127:       后续8字节表示长度(64位无符号整数,网络字节序)
                  例:0x7F 0x00...0x01 0x00 0x00 → Payload 65536字节
1
2
3
4
5
6
7
8

回到小钱的场景:聊天消息通常 100~500 字节,直接用 7 位长度 + 4 字节掩码 = 每次发送增加 6 字节帧头即可,开销极低。

# 4.3 控制帧:Close/Ping/Pong

Opcode 名称 作用
0x8 Close 关闭连接。携带可选的状态码和原因
0x9 Ping 心跳检测——发送方询问"你还活着吗"
0xA Pong 心跳响应——"我还在"

控制帧可以穿插在数据帧之间发送,并且有优先级——即使数据帧排队,控制帧也要优先处理。

Close 帧的状态码:

1000: 正常关闭
1001: 端点离开(浏览器关闭标签页)
1002: 协议错误
1003: 收到不支持的数据类型
1008: 违反策略
1011: 服务端异常
1
2
3
4
5
6

# 4.4 掩码设计的深意

疑惑:为什么客户端→服务端必须用掩码,而服务端→客户端不需要?

答疑:掩码的目的不是加密,而是防止缓存投毒攻击。

攻击场景(没有掩码时):

1. 攻击者控制了一个恶意网站 evil.com
2. 用户访问 evil.com,恶意JavaScript通过WebSocket发送数据
3. 攻击者精心构造的WebSocket数据:
   0x47455420... (即 "GET /secret HTTP/1.1\r\n...")
4. 这段数据经过透明代理服务器时,代理服务器解析到
   "GET /secret HTTP/1.1" → 误认为是一个HTTP请求
5. 代理服务器把响应缓存下来(缓存投毒)
6. 后续其他用户访问 /secret 时,得到的是被投毒的响应

掩码的作用:
  攻击者发送同样的数据 → 但与4字节随机掩码Key做XOR
  → 变成乱码 → 透明代理无法将其识别为HTTP请求
  → 攻击失效
  
  因为Key是随机的(客户端每次生成),透明代理无法预测最终XOR后的数据
  
服务端→客户端为什么不需要掩码?
  → 浏览器没有"解析响应为HTTP请求"的机制
  → 缓存投毒攻击对客户端不安全的风险不存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

掩码不是加密——因为 Key 就在帧头里,任何人拿到帧都可以还原出原始数据。掩码的真实目的是让数据"看起来不像 HTTP 请求"。

# 4.5 分片传输

一条 WebSocket 消息可以被分成多个帧(分片)。通过 FIN 位来控制:

发送一条大消息"Hello World"(不必要分片时):
  [FIN=1, opcode=0x1, payload="Hello World"]  ← 单帧

分片发送:
  [FIN=0, opcode=0x1, payload="Hello "]        ← 第1帧(开始)
  [FIN=0, opcode=0x0, payload="World"]          ← 第2帧(继续)
  [FIN=1, opcode=0x0, payload="!"]              ← 第3帧(结束)

分片规则:
  - 第一帧:opcode!==0x0,FIN=0(除非正好一帧)
  - 中间帧:opcode===0x0,FIN=0
  - 最后帧:opcode===0x0,FIN=1
  - 控制帧可以穿插在分片帧之间
1
2
3
4
5
6
7
8
9
10
11
12
13

# 05.心跳与保活机制

# 5.1 为什么需要心跳

WebSocket 连接基于 TCP,TCP 连接本身有 Keep-Alive 机制。那为什么还需要应用层心跳?

TCP Keep-Alive vs WebSocket Ping/Pong:

TCP Keep-Alive(传输层):
  - 默认间隔:2小时
  - 只检测TCP连接是否存活
  - 不检测应用层是否正常(进程 hang 住但 TCP 连接还活着)
  - 无法穿越某些中间设备

WebSocket Ping/Pong(应用层):
  - 自定义间隔:通常30~60秒
  - 检测服务端/客户端是否能正常响应
  - 可以携带业务数据
  - 维持中间设备的连接表
1
2
3
4
5
6
7
8
9
10
11
12
13

穿透 NAT/防火墙/负载均衡器:

中间设备的连接超时:

  NAT:        通常 1~5 分钟
  防火墙:      通常 30~60 分钟
  Nginx:      默认 keepalive_timeout 65秒
  AWS ELB:    默认 60 秒

如果心跳间隔 > 最短的中间设备超时 → 空闲期间连接被设备关闭
→ 双发方然不知道 → 出现"僵尸连接"

这就是为什么心跳间隔一般为 30 秒的原因:
  → 比 Nginx 的 65 秒短
  → 比 NAT 的 1~5 分钟短
  → 比 TCP Keep-Alive 的 2 小时短得多
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5.2 Ping/Pong帧的使用

// 服务端心跳实现
const HEARTBEAT_INTERVAL = 30000; // 30秒
const HEARTBEAT_TIMEOUT = 10000;  // 等待Pong的超时时间

ws.on('connection', (socket) => {
  socket.isAlive = true;
  
  // 收到客户端的Pong → 标记存活
  socket.on('pong', () => {
    socket.isAlive = true;
  });
  
  // 定时发送Ping
  const pingTimer = setInterval(() => {
    if (!socket.isAlive) {
      // 上次Ping没有收到Pong → 连接已死
      clearInterval(pingTimer);
      socket.terminate();
      return;
    }
    
    socket.isAlive = false;
    socket.ping(); // 发送Ping帧
    
    // 10秒后检查(给Pong响应留时间)
    setTimeout(() => {
      if (!socket.isAlive) {
        socket.terminate();
      }
    }, HEARTBEAT_TIMEOUT);
  }, HEARTBEAT_INTERVAL);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 5.3 中间代理的超时

回到小钱的方案D:即使心跳正确配置,Nginx 作为反向代理时,也需要配置合理的超时:

server {
    location /ws/ {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # ⚠️ 关键:代理的超时必须大于心跳间隔
        proxy_read_timeout 60s;     # 默认60秒,心跳30秒需要这个 > 30
        proxy_send_timeout 60s;
        
        # 禁用缓冲(WebSocket是实时协议)
        proxy_buffering off;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果 proxy_read_timeout < 心跳间隔,即使客户端和 WebSocket 服务端之间有心跳交互,Nginx 在超时后会主动断开连接。

# 5.4 重连策略设计

// 客户端重连策略——指数退避 + 随机抖动
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.reconnectAttempts = 0;
    this.maxReconnectDelay = 30000; // 最大30秒
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      this.reconnectAttempts = 0; // 连接成功,重置计数器
      console.log('WebSocket 已连接');
    };
    
    this.ws.onclose = (event) => {
      if (!event.wasClean) {
        this.reconnect();
      }
    };
  }
  
  reconnect() {
    this.reconnectAttempts++;
    // 指数退避:1s → 2s → 4s → 8s → 16s → 30s(Max)
    const delay = Math.min(
      this.maxReconnectDelay,
      1000 * Math.pow(2, this.reconnectAttempts - 1)
    );
    // 加随机抖动:避免大量客户端同时重连
    const jitter = Math.random() * 1000;
    
    setTimeout(() => this.connect(), delay + jitter);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 06.服务端架构设计

# 6.1 连接管理

生产级的 WebSocket 服务需要维护一个"连接注册表"——记录谁在线、哪个连接属于哪个用户、用户的元信息。

// 连接管理器
class ConnectionManager {
  constructor() {
    // userId → Set<WebSocket>
    this.connections = new Map();
  }
  
  add(userId, socket) {
    if (!this.connections.has(userId)) {
      this.connections.set(userId, new Set());
    }
    this.connections.get(userId).add(socket);
  }
  
  remove(userId, socket) {
    const sockets = this.connections.get(userId);
    if (sockets) {
      sockets.delete(socket);
      if (sockets.size === 0) {
        this.connections.delete(userId);
      }
    }
  }
  
  // 给特定用户发消息(支持多端登录)
  sendToUser(userId, message) {
    const sockets = this.connections.get(userId);
    if (sockets) {
      const data = JSON.stringify(message);
      sockets.forEach(socket => {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(data);
        }
      });
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 6.2 消息广播

// 广播给所有在线用户
broadcast(message) {
  const data = JSON.stringify(message);
  this.connections.forEach((sockets) => {
    sockets.forEach(socket => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(data);
      }
    });
  });
}

// 广播给指定房间
broadcastToRoom(roomId, message) {
  const room = this.rooms.get(roomId);
  if (room) {
    const data = JSON.stringify(message);
    room.forEach(userId => {
      this.sendToUser(userId, message);
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 6.3 水平扩展

单机 WebSocket 服务有连接数上限(通常 5~10 万)。需要水平扩展时,最大的挑战是:不同服务器上的用户如何互相通信?

方案1:粘性会话(Sticky Session)
  负载均衡器根据 Cookie/IP 将同一用户始终路由到同一服务器
  简单但不够灵活

方案2:消息队列(推荐)
  WebSocket Server1 ─┐
                      ├── Redis Pub/Sub ── WebSocket Server2
  WebSocket Server2 ─┘
  
  用户A在Server1 → 发送消息 → Redis Pub/Sub → Server2 → 推送给用户B

方案3:外部存储 + 拉模式
  发消息时写入Redis → 各服务器定期拉取或订阅 → 推送给连接在本机的用户
1
2
3
4
5
6
7
8
9
10
11
12
13
// 基于 Redis Pub/Sub 的跨服务器消息同步
const redis = require('redis');
const pub = redis.createClient();
const sub = redis.createClient();

sub.subscribe('chat:messages');
sub.on('message', (channel, message) => {
  const { targetUserId, data } = JSON.parse(message);
  // 只推送给在本服务器的目标用户
  connectionManager.sendToUser(targetUserId, data);
});

// 发送消息时
function sendMessage(fromUserId, toUserId, content) {
  // 1. 持久化到DB
  db.saveMessage({ from: fromUserId, to: toUserId, content });
  
  // 2. 发布到Redis → 所有服务器都能收到
  pub.publish('chat:messages', JSON.stringify({
    targetUserId: toUserId,
    data: { from: fromUserId, content }
  }));
  
  // 3. 如果目标用户在本服务器,也会通过订阅收到并推送
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 6.4 背压处理

当消息生产速度 > 消费速度时,需要处理背压(Back Pressure):

// 背压处理
ws.on('message', (data) => {
  // 检查发送缓冲区
  if (ws.bufferedAmount > 1024 * 1024) { // 超过1MB
    console.warn('客户端背压,缓冲区超过1MB');
    // 策略1:丢弃消息
    // 策略2:暂停读取
    ws.pause(); // Node.js 流
  }
});

// 缓冲区降到安全水平时恢复
ws.on('drain', () => {
  ws.resume();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 07.安全与优化

# 7.1 WebSocket安全风险

WebSocket主要安全风险:

1. 跨站WebSocket劫持(Cross-Site WebSocket Hijacking)
   攻击者网站利用用户已认证的Cookie发起WebSocket连接
   防御:验证Origin头部 + CSRF Token

2. 未授权访问
   WebSocket握手可以携带Cookie/Token → 必须验证身份
   防御:在upgrade阶段验证JWT/Cookie

3. 拒绝服务(DoS)
   恶意客户端创建大量连接不释放
   防御:限制单IP最大连接数、设置连接超时

4. 消息注入
   攻击者发送恶意消息(XSS、SQL注入等)
   防御:对消息内容做校验和转义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.2 消息压缩

WebSocket 支持 permessage-deflate 扩展,对每条消息进行压缩。对于文本消息(JSON聊天),压缩率通常在 60~80%。

权衡:
  压缩开启:CPU ↑ 10~20%,带宽 ↓ 60~80%
  适合:高延迟网络、大量文本消息
  不适合:内网低延迟、已经压缩的二进制数据(图片、视频)
1
2
3
4

# 7.3 二进制传输

WebSocket 支持二进制帧(opcode=0x2),可用于传输图片、文件、Protobuf 等。

// 发送二进制数据
const buffer = new ArrayBuffer(1024);
ws.send(buffer);

// 接收二进制数据
ws.on('message', (data) => {
  if (data instanceof ArrayBuffer) {
    // 处理二进制数据
  } else {
    // 处理文本数据
  }
});
1
2
3
4
5
6
7
8
9
10
11
12

二进制传输避免了 Base64 编码带来的 33% 体积膨胀。

# 7.4 连接数优化

单机WebSocket连接数上限:

操作系统限制:
  Linux:ulimit -n 65536(可调到 100万+)
  macOS:默认 256

应用层限制:
  Node.js:默认无硬限制(受限于内存)
  Go:goroutine 开销量极低,轻松 10 万+
  Java Netty:受限于线程模型(通常 5~10 万)

优化建议:
  - 每个连接内存开销约 50~100KB
  - 10万连接 ≈ 5~10GB 内存
  - 使用消息队列做水平扩展
  - 心跳间隔不宜过短(30秒是甜点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 08.综合案例:从0到1搭建实时聊天系统

本章用一个贯穿全文的实战案例——从一个"能发消息就行"的聊天系统开始,经历四次进化,最终成为生产级的实时通信系统。

# 8.1 案例背景与目标

社交 App 聊天模块:支持单聊,在线用户 10 万,消息延迟目标 < 100ms。

版本 方案 延迟 10万在线代价
V1 短轮询 ~2000ms 低(5万QPS但96%无效)
V2 长轮询 ~200ms 极高(10万线程)
V3 WebSocket ~10ms 极低(事件驱动)
V4 生产级WebSocket ~10ms 极低+(心跳+重连+分布式)

# 8.2 第一代:短轮询——能跑就行

// 客户端
setInterval(async () => {
  const res = await fetch('/api/messages/poll');
  const data = await res.json();
  data.messages.forEach(msg => renderMessage(msg));
}, 2000);
1
2
3
4
5
6
V1 表现:
  延迟:平均 1000ms(2秒轮询的一半)
  无效请求比例:96.7%
  10万用户带宽:200Mbps +
  
瓶颈:延迟高(对应方案A的"已读延迟2秒")
1
2
3
4
5
6

# 8.3 第二代:长轮询——假装实时

// 服务端(Node.js)
app.get('/api/messages/long-poll', async (req, res) => {
  const userId = req.user.id;
  const lastMsgId = parseInt(req.query.since) || 0;
  
  // hold住连接,等待新消息(最多30秒超时)
  const timeout = 30000;
  const messages = await waitForMessages(userId, lastMsgId, timeout);
  
  res.json({ messages: messages || [] });
});
1
2
3
4
5
6
7
8
9
10
11
V2 表现:
  延迟:~200ms(有消息立即返回)
  10万用户服务端:10万线程 = 无法部署
  中间代理:Nginx超时/断开风险

瓶颈:资源消耗(对应方案B的"线程池资源吃紧")
1
2
3
4
5
6

# 8.4 第三代:WebSocket——真正的全双工

// 服务端(Node.js ws库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Map(); // userId → WebSocket

wss.on('connection', (ws, req) => {
  // 从URL参数中提取userId(生产环境应验证Token)
  const userId = new URL(req.url, 'http://localhost').searchParams.get('userId');
  clients.set(userId, ws);
  
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    // 转发给目标用户
    const targetWs = clients.get(msg.to);
    if (targetWs && targetWs.readyState === WebSocket.OPEN) {
      targetWs.send(JSON.stringify(msg));
    }
  });
  
  ws.on('close', () => {
    clients.delete(userId);
  });
});

// 客户端
const ws = new WebSocket('ws://localhost:8080?userId=123');
ws.onopen = () => ws.send(JSON.stringify({ to: 456, text: 'Hello!' }));
ws.onmessage = (event) => renderMessage(JSON.parse(event.data));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
V3 表现:
  延迟:~10ms
  10万用户内存:约5GB
  消息延迟目标达成 ✓
  
缺陷:
  ❌ 无心跳 → 连接断开不知道
  ❌ 无重连 → 网络切换后永久断连
  ❌ 无离线消息 → 断连期间消息丢失
  ❌ 单机瓶颈 → 无法水平扩展
1
2
3
4
5
6
7
8
9
10

# 8.5 第四代:生产级WebSocket——全场景覆盖

// 服务端(完整生产级实现)
const WebSocket = require('ws');
const redis = require('redis');
const jwt = require('jsonwebtoken');

const wss = new WebSocket.Server({ port: 8080 });
const pub = redis.createClient();
const sub = redis.createClient();
sub.subscribe('chat:messages');

// 连接管理器
class ChatServer {
  constructor() {
    this.clients = new Map();  // userId → WebSocket
    this.heartbeatInterval = 30000;
    this.heartbeatTimeout = 10000;
  }
  
  handleConnection(ws, req) {
    // 1. 身份验证
    const token = new URL(req.url, 'http://localhost').searchParams.get('token');
    let userId;
    try {
      userId = jwt.verify(token, SECRET).userId;
    } catch(e) {
      ws.close(1008, '认证失败');
      return;
    }
    
    // 2. 注册连接
    this.clients.set(userId, ws);
    ws.isAlive = true;
    ws.userId = userId;
    
    // 3. 拉取离线消息
    this.sendOfflineMessages(ws, userId);
    
    // 4. 心跳检测
    ws.on('pong', () => { ws.isAlive = true; });
    const pingTimer = setInterval(() => {
      if (!ws.isAlive) { ws.terminate(); clearInterval(pingTimer); return; }
      ws.isAlive = false;
      ws.ping();
    }, this.heartbeatInterval);
    
    // 5. 消息处理
    ws.on('message', (data) => this.handleMessage(userId, JSON.parse(data)));
    
    // 6. 断连清理
    ws.on('close', () => {
      clearInterval(pingTimer);
      this.clients.delete(userId);
      // 记录最后在线时间,供离线消息拉取使用
      redis.set(`user:${userId}:lastOnline`, Date.now());
    });
  }
  
  async handleMessage(fromUserId, msg) {
    // 持久化
    await db.saveMessage({ from: fromUserId, to: msg.to, text: msg.text });
    
    // 在线 → 直接推送
    if (this.clients.has(msg.to)) {
      this.clients.get(msg.to).send(JSON.stringify(msg));
    } else {
      // 离线 → 缓存到Redis
      redis.lpush(`offline:${msg.to}`, JSON.stringify(msg));
      redis.expire(`offline:${msg.to}`, 86400 * 7); // 7天
    }
    
    // 跨服务器同步(Redis Pub/Sub)
    pub.publish('chat:messages', JSON.stringify({ to: msg.to, msg }));
  }
  
  async sendOfflineMessages(ws, userId) {
    const messages = await redis.lrange(`offline:${userId}`, 0, -1);
    if (messages.length > 0) {
      ws.send(JSON.stringify({ type: 'offline', messages }));
      redis.del(`offline:${userId}`);
    }
  }
}

// Redis Pub/Sub → 跨服务器接收消息
sub.on('message', (channel, data) => {
  const { to, msg } = JSON.parse(data);
  if (chatServer.clients.has(to)) {
    chatServer.clients.get(to).send(JSON.stringify(msg));
  }
});

const chatServer = new ChatServer();
wss.on('connection', (ws, req) => chatServer.handleConnection(ws, req));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
V4 表现:
  延迟:~10ms
  心跳检测:30秒 Ping/Pong
  自动重连:指数退避 + 随机抖动
  离线消息:Redis 缓存 → 重连后补推
  水平扩展:Redis Pub/Sub 跨服务器同步
  10万用户:单机 5GB 内存,通过负载均衡可做多台
1
2
3
4
5
6
7

# 8.6 四种方案横向对比

维度 V1 短轮询 V2 长轮询 V3 基础WebSocket V4 生产级WebSocket
消息延迟 ~1000ms ~200ms ~10ms ~10ms
10万在线开销 5万QPS(96%无效) 10万线程(不可行) 5GB内存 5GB内存
全双工 ❌ ❌ ✅ ✅
心跳检测 ❌ ❌ ❌ ✅(30s Ping/Pong)
自动重连 ❌ ❌ ❌ ✅(指数退避)
离线消息 ❌ ❌ ❌ ✅(Redis缓存)
水平扩展 ✅(无状态) ✅(无状态) ❌(单机) ✅(Pub/Sub)
对应章节 2.2 2.3 3/4章 5/6/7章

# 8.7 案例升华:WebSocket的设计哲学

WebSocket 协议的三个设计哲学:

1. "升级而不重来"
   WebSocket没有自建一套传输层协议
   而是借用了HTTP的Upgrade机制 + TCP
   → 复用80/443端口、穿越代理、兼容现有基础设施

2. "帧头极简,数据高效"
   最小2字节帧头 vs HTTP几百字节请求头
   → 适合高频小消息(聊天、股票、游戏状态同步)

3. "应用层控制,传输层兜底"
   WebSocket自己管理心跳(Ping/Pong)、重连、会话
   不完全依赖TCP Keep-Alive
   → 应用层比传输层更了解"业务健康"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

WebSocket vs HTTP 关键差异总结:

维度 HTTP/1.1 WebSocket
通信模式 请求-响应(半双工) 全双工双向
消息边界 无(字节流) 有(帧)
头部开销 每次请求~500字节 最少2字节/帧
连接生命周期 短(请求完释放) 长(持久连接)
服务端推送 ❌ ✅
二进制支持 需Base64编码 原生支持

# 8.8 全文知识图谱回顾

                    小钱的技术选型
                    │
    ┌───────┬───────┼───────┬───────┐
    │       │       │       │       │
 短轮询   长轮询  WebSocket  心跳   分布式
 延迟2s  线程多   升级握手   Ping   Pub/Sub
 [2.2]   [2.3]   [3.2]    [5.2]  [6.3]
    │       │       │       │       │
    └───────┴───────┼───────┴───────┘
                    │
        ┌───────────┴───────────┐
        │                       │
  帧格式 [4章]             安全与优化 [7章]
  FIN/Opcode/MASK          压缩/二进制/DoS
        │                       │
        └───────────┬───────────┘
                    │
  V1→V2→V3→V4  实时聊天系统的四次进化
   [第8章]      从轮询到生产级WebSocket
                    │
                    ▼
    升级而不重来 / 帧头极简 / 应用层控制
       WebSocket的三大设计哲学 [8.7节]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

最终的方法论沉淀——设计实时通信系统时,都应该问自己三个问题:

  1. 延迟能接受吗?(轮询 vs 长轮询 vs WebSocket → 不同场景不同取舍)
  2. 断了怎么办?(心跳 + 重连 + 离线消息 → 网络不是 100% 可靠的)
  3. 能扩展吗?(单机 vs Pub/Sub 集群 → 用户量增长时的架构演进)

把这三个问题问到位,你就从"会用 WebSocket"进化到了"能设计实时通信系统的工程师"。

# 09.思考题与作业

# 9.1 基础思考题

  1. WebSocket vs HTTP 长轮询:在 10 万用户同时在线、每条消息 500 字节的场景下,分别计算 WebSocket 和长轮询的服务端线程/内存开销。为什么长轮询不能支撑 10 万并发?

  2. 掩码不是为了加密:客户端→服务端的 WebSocket 帧必须用掩码。掩码 Key 就在帧头里,任何人都可以还原。那掩码到底保护了什么?还原一个具体的缓存投毒攻击场景。

  3. Ping/Pong 的间隔:心跳间隔设为 5 秒和 5 分钟各有什么问题?为什么 30 秒是"甜点"?如果 Nginx 的 proxy_read_timeout=30s,心跳间隔应该设为多少?

  4. Sec-WebSocket-Key/Accept 的计算:给定 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==,手动计算 Sec-WebSocket-Accept。(提示:拼接魔术字符串 → SHA-1 → Base64)

# 9.2 进阶思考题

  1. HTTP/2 Server Push 能否替代 WebSocket?:HTTP/2 的 Server Push 可以实现服务端推送。它和 WebSocket 的本质区别是什么?在什么场景下 Server Push 可以部分替代 WebSocket?什么场景绝对不能?

  2. WebSocket 分片的工程价值:WebSocket 支持分片传输(FIN 位控制),但在实际开发中很少使用。在什么场景下分片是必要的?如果不分片,直接 send(smallMessage) 和 send(largeMessage) 有什么区别?(提示:从内存分配、TCP 拥塞控制角度思考)

  3. 粘性会话 vs Pub/Sub 的取舍:WebSocket 水平扩展有 Sticky Session 和 Pub/Sub 两种方案。粘性会话简单但不够灵活,Pub/Sub 灵活但有额外延迟。在什么业务场景下,粘性会话就够了?在什么场景下必须用 Pub/Sub?

  4. WebTransport 对 WebSocket 的挑战:WebTransport(基于 HTTP/3 QUIC)正在标准化,它提供了比 WebSocket 更多的能力(无序传输、部分可靠性、多流)。WebSocket 会被替代吗?什么场景仍然适合用 WebSocket?

# 9.3 动手作业

作业一(必做):用 Node.js 实现一个简单的 WebSocket 聊天室。

  • 服务端:ws 库监听 8080 端口,接收消息并广播给所有连接的客户端。
  • 客户端:浏览器原生 WebSocket API 连接服务端,发送和接收消息。
  • 测试:打开两个浏览器标签页,验证消息能双向互通。
  • 进阶:添加 Ping/Pong 心跳,断开时在服务端日志中打印"连接断开"。

作业二(选做):对比轮询 vs WebSocket 的性能。

  • 分别实现短轮询(2秒间隔)和 WebSocket 两个版本的简单聊天。
  • 用浏览器 DevTools 的 Network 面板对比:
    • 1 分钟内的请求数
    • 总传输字节数
    • 消息从发送到接收的延迟
方案 1分钟请求数 总传输字节 平均延迟
短轮询(2s)
WebSocket

作业三(架构思考):设计一个支持 100 万在线的 WebSocket 架构。

  • 画出架构图:负载均衡器 → WebSocket 集群 → Redis Pub/Sub → 消息持久化(MySQL/Kafka)。
  • 标注每层的连接数、内存消耗、单点故障风险。
  • 给出具体的技术选型(WebSocket 框架、消息队列、持久化方案)。
上次更新: 2026/06/09, 16:25:38
如何去排查网络故障
HTTP3与QUIC协议

← 如何去排查网络故障 HTTP3与QUIC协议→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式