十六、一个更完整的 Express + WebSocket 架构
实际 Node.js 项目中,我们往往不会让 Express 和 WebSocket 各自启动一个端口。
而是:
import express from "express";
import { createServer } from "node:http";
import { WebSocketServer } from "ws";
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
app.get("/api/matches", (req, res) => {
res.json([]);
});
wss.on("connection", (socket) => {
console.log("WebSocket connected");
});
server.listen(3000);
这里有三个东西:
Express App
↓
Node HTTP Server
↓
┌───────────────┐
│ │
REST WebSocket
express() 创建的是 Express 应用。
而:
createServer(app) 创建的是底层 Node HTTP Server。
最终:
HTTP Server :3000
│
├── HTTP Request
│ ↓
│ Express
│
└── Upgrade
↓
WebSocket
因此:
REST 和 WebSocket 完全可以共享同一个 HTTP Server 和端口。
十七、WebSocket 可靠性:ACK 与 Retry
WebSocket 本身并不等于:
消息一定被业务成功处理
假设:
Client
│
│ message #123
↓
Server
如果网络突然断开:
Client ✕ Server
客户端并不知道:
Server 到底有没有收到消息?
对于普通聊天消息可能问题不大。
但如果消息非常重要,就可以设计 ACK:
Client ── message #123 ──> Server
Client <──── ACK #123 ──── Server
如果:
timeout
客户端可以:
retry
于是业务层拥有了:
Message ID
+
ACK
+
Retry
进一步还可以考虑:
- 幂等
- 去重
- 顺序
- 重放
- 消息持久化
这时你会发现:
WebSocket 只是传输层,可靠消息系统需要更上层的设计。
十八、真正容易被忽略的问题:Backpressure
假设服务器每秒产生:
10,000 messages
但是某个客户端处理能力只有:
100 messages/s
那么:
Server
↓↓↓↓↓↓↓↓↓↓↓
Slow Client
消息就可能不断堆积。
如果服务器无脑:
socket.send(message);
可能导致:
Buffer ↑
Memory ↑
Latency ↑
这就是:
Backpressure(背压)
生产系统需要考虑:
- 慢客户端
- 消息缓冲
- 发送队列
- 限流
- 丢弃策略
- 消息优先级
例如实时股票系统里:
价格变化:
100.01
100.02
100.03
100.04
100.05
如果客户端已经落后很多消息,那么有时候没有必要把所有历史价格逐条发送过去。
可能更合理的是:
直接发送最新状态。
所以实时系统经常需要在:
实时性、完整性、资源消耗 之间做取舍。
十九、单机 WebSocket 到多实例
单机情况下:
Client A ──┐
Client B ──┼──> WebSocket Server
Client C ──┘
服务器知道所有连接。
但是生产环境通常需要水平扩展:
Load Balancer
/ \
↓ ↓
Server A Server B
↓ ↓
Clients A Clients B
现在问题出现了。
假设:
User A → Server A
User B → Server B
User A 发送消息。
Server A 知道。
但是 Server B 怎么知道?
如果 Server A 直接广播:
Server A
↓
自己的 Clients
User B 收不到。
这就是 WebSocket 分布式化以后最核心的问题之一:
连接状态分散在不同服务器上。
二十、Redis Pub/Sub
一个常见的解决方式是使用 Redis Pub/Sub:
Redis Pub/Sub
/ \
↑ ↑
Server A Server B
↓ ↓
Clients Clients
Server A 收到事件:
Match 123 Updated
然后:
PUBLISH match:123
Server B 订阅:
SUBSCRIBE match:123
于是:
Server A
↓
Redis
↓
Server B
↓
Client B
这样不同 WebSocket Server 就能够共享事件。
整个系统从:
单机 WebSocket
演进成:
Load Balancer
↓
WebSocket Servers
↓
Redis Pub/Sub
↓
Database
这已经开始进入分布式系统的范畴。
二十一、生产级 WebSocket 的安全问题
WebSocket 是长连接,因此不能只按照普通 HTTP API 的方式考虑安全。
至少需要解决几个问题。
1. Authentication
连接建立以后,需要知道:
这个用户是谁?
可以使用:
Cookie / Session
JWT
其他认证机制
2. Authorization
知道用户是谁以后,还需要判断:
他有没有权限访问这个 Room?
例如:
User A
↓
subscribe match:123
服务器不能只因为请求来了就允许。
应该:
Authentication
↓
Authorization
↓
Subscribe
3. Rate Limit
客户端不能无限发送消息:
message
message
message
message
...
否则可能成为资源消耗攻击。
所以需要限制:
连接建立频率
+
消息发送频率
4. Payload Limit
不能允许客户端发送无限大的消息。
例如 ws 支持配置:maxPayload
限制单个 WebSocket 消息大小。
二十二、WebSocket 的监控和 HTTP 完全不同
普通 HTTP 服务可能关注:
QPS
HTTP 200
HTTP 500
Response Time
但 WebSocket 最大的特点是:
连接可能持续很长时间。
所以需要额外关注:
Concurrent Connections
Message Throughput
Message Latency
Disconnect Rate
Connection Errors
CPU
Memory
Event Loop Lag
例如:
Active Connections: 100,000
Messages/sec: 50,000
Avg Latency: 25ms
Event Loop Lag: 10ms
这些指标对于实时系统来说非常重要。
尤其是 Node.js。
因为大量连接和消息处理最终都会影响:
Event Loop
所以 WebSocket 的性能分析不能只看:
“接口是不是 200。” 而应该看: 整个实时连接系统现在到底处于什么状态。
二十三、WebSocket、SSE 和 WebRTC 怎么选?
实时通信并不只有 WebSocket。
可以简单做一个区分:
| 技术 | 方向 | 典型场景 |
|---|---|---|
| HTTP | 请求→响应 | CRUD |
| WebSocket | 双向 | 聊天、实时协作 |
| SSE | 服务端→客户端 | AI Streaming、通知 |
| WebRTC | P2P | 音视频 |
| WebTransport | 双向 | 特殊低延迟场景 |
例如:
AI → Browser
如果只是服务器不断输出:
你
好
,
这
是
AI
的
回
答
SSE 就可能已经足够。
但如果是:
Browser ↔ Server
双方都需要持续发送消息:
- 聊天
- 多人协作
- 实时游戏
- 实时控制
WebSocket 通常更加合适。
所以:
不要因为 WebSocket 能做实时通信,就什么实时需求都使用 WebSocket。
二十四、WebSocket 和 Socket.IO
Node.js 生态中还经常看到 Socket.IO。
可以简单理解:
ws
更接近 WebSocket:
Application
↓
ws
↓
WebSocket
它比较轻量,也给开发者更多控制权。
但是:
- Room
- 重连
- 事件协议
- 一些可靠性能力
可能需要自己实现。
而 Socket.IO 提供了更高层的抽象:
Application
↓
Socket.IO
↓
实时通信能力
它提供:
Event
Room
Broadcast
Reconnect
Adapter 等能力
因此选择哪一个,不应该简单理解成:
“Socket.IO 比 WebSocket 高级。”
而应该根据需求选择抽象层级。
如果你想学习 WebSocket 原理,ws 很适合。
如果希望快速构建复杂实时应用,Socket.IO 往往更加方便。
二十五、最终形成一个生产级架构
把前面所有东西串起来:
Load Balancer
│
┌─────────────┴─────────────┐
↓ ↓
WebSocket Server A WebSocket Server B
│ │
└─────────────┬─────────────┘
↓
Redis Pub/Sub
│
↓
Business Logic
│
↓
PostgreSQL
客户端:
React
/ \
/ \
↓ ↓
REST WS
│ │
↓ ↓
Initial State Events
一次完整的业务流程可能是:
用户打开比赛页面
↓
GET /matches/123
↓
获取当前完整状态
↓
建立 WebSocket
↓
subscribe match:123
↓
等待实时事件
↓
比赛发生进球
↓
Database 更新
↓
MatchUpdated Event
↓
Redis Pub/Sub
↓
WebSocket Servers
↓
找到 match:123 subscribers
↓
推送给客户端
↓
React 更新 UI
这时候 WebSocket 已经不再只是:
socket.send()
而是一整套实时通信基础设施。
二十六、从几十行代码到生产系统
回过头看整个演进过程,其实非常有意思。
最开始:
WebSocket
只是:
Client ═══ Server
然后发现需要知道谁连接了:
Connection State
发现连接可能死掉:
Heartbeat
发现所有人都收到消息太浪费:
Broadcast
↓
Unicast
↓
Room / Subscription
发现字符串消息无法扩展:
Message Protocol
发现网络可能断:
ACK / Retry
发现客户端处理不过来:
Backpressure
发现恶意客户端:
Authentication
Authorization
Rate Limit
Payload Limit
发现一台服务器不够:
Multi-instance
发现服务器之间无法共享消息:
Redis Pub/Sub
发现系统出了问题却不知道:
Observability
最终:
WebSocket
↓
Connection Management
↓
Message Protocol
↓
Routing
↓
Reliability
↓
Security
↓
Backpressure
↓
Observability
↓
Pub/Sub
↓
Distributed System
这其实就是一个非常典型的后端系统演进过程。
二十七、我认为 WebSocket 最值得理解的三个问题
学 WebSocket 的时候,我觉得最重要的并不是记住所有 API。
而是想明白三个问题。
第一个问题:服务器为什么能主动发消息?
因为建立 WebSocket 连接以后:
Client ═════════ Server
它不再是传统的一问一答,而是双方都可以主动发送数据。
第二个问题:为什么 WebSocket 越深入越复杂?
因为它是长连接。
长连接意味着:
连接状态
+
心跳
+
清理
+
认证
+
订阅
+
消息路由
+
资源管理
都变成了服务器需要负责的问题。
第三个问题:为什么分布式以后更复杂?
因为:
Connection State
通常存在于具体的 WebSocket Server 内。
当:
Server A
Server B
Server C
同时存在时,就需要解决:
不同服务器之间如何共享事件?
于是才有了:
Redis Pub/Sub
以及进一步的分布式架构。
二十八、最后总结
如果把 WebSocket 的核心知识压缩成一条线:
HTTP
↓
Upgrade
↓
101 Switching Protocols
↓
WebSocket
↓
Persistent Connection
↓
Connection State
↓
Heartbeat
↓
Message Protocol
↓
Broadcast / Unicast / Room
↓
ACK / Retry
↓
Backpressure
↓
Authentication / Authorization
↓
Rate Limit
↓
Observability
↓
Redis Pub/Sub
↓
Multi-instance
WebSocket 真正解决的问题其实只有一个:
让客户端和服务器之间拥有一条可以长期存在、双向通信的实时连接。
但从这条连接出发,会自然产生一系列工程问题:
- 连接怎么管理?
- 消息怎么定义?
- 消息发给谁?
- 连接死了怎么办?
- 消息丢了怎么办?
- 客户端处理不过来怎么办?
- 多个服务器怎么办?
- 如何鉴权?
- 如何限流?
- 如何监控?
所以:
WebSocket API 很简单,生产级实时通信系统一点也不简单。
而这可能也是学习 WebSocket 最有价值的地方。
它表面上是在学习一种通信协议,实际上是在通过“实时通信”这个问题,逐渐接触:
网络、连接管理、状态管理、消息系统、缓存、Pub/Sub、可靠性、安全、性能以及分布式系统。
当你能够从:
“我会用 WebSocket”
进一步回答:
“为什么需要 WebSocket?”
“为什么需要心跳?”
“为什么需要 Room?”
“为什么需要 Redis?”
“为什么数据库才是 Source of Truth?”
“为什么 WebSocket 和 REST 应该配合?”
“为什么生产环境必须考虑背压和可观测性?”
这时候,才算真正理解了 WebSocket。