十六、一个更完整的 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、通知
WebRTCP2P音视频
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。