在开始设计之前,必须先明确一个行业级认知误区:
前端不可能建立“百万个 WebSocket 连接”——浏览器对单个域名的 WebSocket 连接数有严格限制(Chrome/Edge 为 6 个,Firefox 虽为 200 个但实际不推荐超过 10 个),单个前端页面根本无法支撑百万级连接。
这个问题的真实研究与落地目标是:设计一个能高效消费后端百万级并发推送、同时保证前端页面流畅不卡顿的 WebSocket 系统——核心是前端如何“高效处理高频、海量的推送数据”,而非“建立百万个连接”。
结合金融交易场景(订单簿、K线、实时成交),这个设计目标更具现实意义,因为交易场景的推送频率、数据量、实时性要求,是所有 Web 应用中最高的之一。
我将整个系统分为 5 层核心架构,从连接到渲染全链路优化,形成一个闭环的高可用系统:
加载图表中...
连接层是整个系统的基础,核心目标是保证连接永远在线、断线快速重连、连接开销极低,哪怕后端有百万级并发,前端也能稳定消费。
type 字段区分业务类型。{
"type": "orderbook", // 业务类型:orderbook/kline/trade/notice
"symbol": "BTC-USDT",
"data": { /* 业务数据 */ },
"timestamp": 1711345678901,
"seq": 10001 // 消息序列号,用于丢包检测
}
token_refresh 消息,前端通过 HTTP 接口刷新 Token,通过 WebSocket 发送 refresh_token 消息更新连接鉴权,不需要断开重连(这是保证连接稳定的关键)。class WebSocketManager {
private ws: WebSocket | null = null;
private token: string = '';
private tokenExpireTime: number = 0;
// 1. 获取鉴权Token
private async getToken(): Promise<{ token: string; expireTime: number }> {
const res = await fetch('/api/ws/token', { credentials: 'include' });
return await res.json();
}
// 2. 建立连接
public async connect(): Promise<void> {
const { token, expireTime } = await this.getToken();
this.token = token;
this.tokenExpireTime = expireTime;
const url = `wss://api.example.com/ws`;
this.ws = new WebSocket(url, [], {
headers: { 'Authorization': `Bearer ${this.token}` }
});
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.startHeartbeat();
this.startTokenRefreshCheck();
this.subscribeChannels();
};
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
}
// 3. Token刷新检查
private startTokenRefreshCheck(): void {
setInterval(() => {
const now = Date.now();
if (this.tokenExpireTime - now < 10 * 60 * 1000) { // 剩余10分钟刷新
this.refreshToken();
}
}, 60 * 1000); // 每分钟检查一次
}
private async refreshToken(): Promise<void> {
const { token, expireTime } = await this.getToken();
this.token = token;
this.tokenExpireTime = expireTime;
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'refresh_token', token }));
}
}
}
这是金融交易场景的核心,必须保证连接永远在线,断线后快速重连,同时避免重连风暴。
ping 消息;ping 后,立即回复 pong 消息;pong,认为连接已断开,主动关闭连接,触发重连;ping,主动关闭连接,释放资源(避免僵尸连接占用后端资源)。private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private lastPongTime: number = Date.now();
private startHeartbeat(): void {
this.lastPongTime = Date.now();
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
// 检查是否超时
if (Date.now() - this.lastPongTime > 60000) {
console.warn('心跳超时,主动断开连接');
this.ws?.close();
}
}, 30000);
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
private handleMessage(msg: any): void {
if (msg.type === 'pong') {
this.lastPongTime = Date.now();
return;
}
// 处理业务消息
this.updateData(msg);
}
2^n * 1000 毫秒后再重连(n 是重连次数),最大等待时间不超过 30 秒;private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private reconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('重连次数超过上限,停止自动重连');
alert('连接已断开,请刷新页面重试');
return;
}
const delay = Math.min(Math.pow(2, this.reconnectAttempts) * 1000, 30000);
console.log(`等待${delay}ms后尝试第${this.reconnectAttempts + 1}次重连`);
this.reconnectTimer = setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
// 在ws.onclose里调用
this.ws.onclose = () => {
console.log('WebSocket连接关闭');
this.stopHeartbeat();
this.reconnect();
};
orderbook:BTC-USDT、kline:BTC-USDT:1m、trade:BTC-USDT;private subscribe(channels: string[]): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'subscribe',
channels: channels
}));
}
}
private unsubscribe(channels: string[]): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'unsubscribe',
channels: channels
}));
}
}
// 页面进入时订阅
public onPageEnter(page: string, symbol?: string): void {
if (page === 'trade' && symbol) {
this.subscribe([
`orderbook:${symbol}`,
`kline:${symbol}:1m`,
`trade:${symbol}`
]);
}
}
// 页面离开时取消订阅
public onPageLeave(page: string, symbol?: string): void {
if (page === 'trade' && symbol) {
this.unsubscribe([
`orderbook:${symbol}`,
`kline:${symbol}:1m`,
`trade:${symbol}`
]);
}
}
数据层是百万级并发的核心,后端每秒可能推送几万条数据,前端必须高效处理、缓存、增量更新,否则主线程会被阻塞,页面直接卡死。
// orderbook.proto
syntax = "proto3";
message OrderBook {
string symbol = 1;
repeated PriceLevel bids = 2;
repeated PriceLevel asks = 3;
int64 timestamp = 4;
uint64 seq = 5;
}
message PriceLevel {
double price = 1;
double amount = 2;
}
message OrderBookIncrement {
string symbol = 1;
repeated PriceLevel bid_updates = 2;
repeated PriceLevel ask_updates = 3;
uint64 seq = 4;
}
这是金融交易场景的核心优化,订单簿、K线这些数据,每次变化的只是很小一部分,全量推送会浪费大量带宽和计算资源。
Map 存储,O(1) 时间复杂度查找和更新),根据增量消息更新本地数据;seq 检测丢包,如果丢包,请求后端重新推送快照。get/set/delete 都是 O(1) 时间复杂度,比数组的 O(n) 快得多,高频更新下性能差异巨大。interface PriceLevel {
price: number;
amount: number;
}
interface OrderBookData {
symbol: string;
bids: Map<number, number>; // price -> amount
asks: Map<number, number>;
lastSeq: number;
}
class OrderBookManager {
private orderBook: OrderBookData | null = null;
// 处理全量快照
private handleSnapshot(msg: any): void {
this.orderBook = {
symbol: msg.symbol,
bids: new Map(msg.bids.map((level: PriceLevel) => [level.price, level.amount])),
asks: new Map(msg.asks.map((level: PriceLevel) => [level.price, level.amount])),
lastSeq: msg.seq
};
this.renderOrderBook();
}
// 处理增量更新
private handleIncrement(msg: any): void {
if (!this.orderBook) return;
// 检测丢包
if (msg.seq !== this.orderBook.lastSeq + 1) {
console.warn('丢包,请求重新推送快照');
this.requestSnapshot();
return;
}
// 更新买单:amount为0则删除,否则更新
msg.bid_updates.forEach((level: PriceLevel) => {
if (level.amount === 0) {
this.orderBook!.bids.delete(level.price);
} else {
this.orderBook!.bids.set(level.price, level.amount);
}
});
// 更新卖单
msg.ask_updates.forEach((level: PriceLevel) => {
if (level.amount === 0) {
this.orderBook!.asks.delete(level.price);
} else {
this.orderBook!.asks.set(level.price, level.amount);
}
});
this.orderBook.lastSeq = msg.seq;
this.renderOrderBook();
}
// 渲染订单簿(只渲染前20档)
private renderOrderBook(): void {
if (!this.orderBook) return;
// 排序买单(从高到低),取前20档
const sortedBids = Array.from(this.orderBook.bids.entries())
.sort((a, b) => b[0] - a[0])
.slice(0, 20)
.map(([price, amount]) => ({ price, amount }));
// 排序卖单(从低到高),取前20档
const sortedAsks = Array.from(this.orderBook.asks.entries())
.sort((a, b) => a[0] - b[0])
.slice(0, 20)
.map(([price, amount]) => ({ price, amount }));
// 更新UI
this.updateUI(sortedBids, sortedAsks);
}
}
百万级并发下,数据的解析、排序、计算会占用大量 CPU 资源,如果在主线程做,会直接阻塞 UI 渲染,导致页面卡顿。
策略:
postMessage 把最终需要渲染的数据发给主线程;为什么 WebWorker 能解决问题?
代码示例(WebWorker 数据处理):
// worker.ts (WebWorker文件)
import * as protobuf from 'protobufjs';
import { OrderBook, OrderBookIncrement } from './orderbook.proto';
self.onmessage = (e) => {
const { type, data } = e.data;
let result;
switch (type) {
case 'orderbook_snapshot':
// 解析Protobuf,排序订单簿
const snapshot = OrderBook.decode(new Uint8Array(data));
result = processOrderBookSnapshot(snapshot);
break;
case 'orderbook_increment':
// 解析Protobuf,更新本地数据
const increment = OrderBookIncrement.decode(new Uint8Array(data));
result = processOrderBookIncrement(increment);
break;
default:
return;
}
// 把结果发给主线程
self.postMessage({
type,
data: result
});
};
function processOrderBookSnapshot(snapshot: any): any {
// heavy computation:解析、排序、过滤
const sortedBids = snapshot.bids
.sort((a: any, b: any) => b.price - a.price)
.slice(0, 20);
const sortedAsks = snapshot.asks
.sort((a: any, b: any) => a.price - b.price)
.slice(0, 20);
return { bids: sortedBids, asks: sortedAsks, seq: snapshot.seq };
}
function processOrderBookIncrement(increment: any): any {
// heavy computation:更新本地数据、排序
// ...
return { /* 更新后的数据 */ };
}
// 主线程
class WebSocketManager {
private worker: Worker = new Worker(new URL('./worker.ts', import.meta.url));
constructor() {
// 接收WebWorker的计算结果
this.worker.onmessage = (e) => {
this.renderUI(e.data);
};
}
private handleMessage(event: MessageEvent): void {
// 把原始二进制数据发给WebWorker处理
this.worker.postMessage({
type: event.data.type,
data: event.data.rawData // Protobuf二进制数据
});
}
private renderUI(data: any): void {
// 只做UI渲染,不做计算
switch (data.type) {
case 'orderbook_snapshot':
case 'orderbook_increment':
this.updateOrderBookUI(data.data);
break;
}
}
}
数据层处理完后,渲染层要保证高频数据更新下,页面 FPS 稳定在 60 帧,不卡顿、不闪烁。
订单簿、成交记录这些长列表,哪怕有几千几万条数据,用户能看到的只有前 20-50 条,虚拟列表只渲染可视区域的 DOM,DOM 节点数量可以减少 95% 以上。
transform 把渲染的列表项定位到可视区域,同时用空白元素填充上下未渲染的区域,保持滚动条的高度。react-window(轻量、高性能)、react-virtualized(功能更全)。百万级并发下,后端每秒可能推送几十次数据,如果每次都更新 UI,主线程会被频繁的重排重绘占满,导致卡顿。
requestAnimationFrame 里执行,保证每次更新都在浏览器的渲染周期里,避免掉帧。requestAnimationFrame 会在浏览器每次重绘前执行,大约 16ms 一次(对应 60 帧)。import { throttle } from 'lodash-es';
class OrderBookManager {
// 节流:每16ms更新一次UI(60帧)
private throttledRender = throttle(() => {
requestAnimationFrame(() => {
this.renderOrderBook();
});
}, 16);
private handleIncrement(msg: any): void {
// 更新本地数据
this.updateLocalData(msg);
// 节流更新UI
this.throttledRender();
}
}
React 18 的并发特性(Concurrent Mode)可以完美适配这个场景,核心 UI(比如最新价格、买卖一价)优先更新,非核心 UI(比如历史成交)在浏览器空闲时更新。
import { useTransition, useDeferredValue, useState } from 'react';
function TradePage() {
const [orderBook, setOrderBook] = useState<any>(null);
const [trades, setTrades] = useState<any[]>([]);
const [isPending, startTransition] = useTransition();
// 延迟更新历史成交(非核心UI)
const deferredTrades = useDeferredValue(trades);
const handleWebSocketMessage = (msg: any) => {
if (msg.type === 'orderbook') {
// 核心UI:同步更新订单簿
setOrderBook(msg.data);
}
if (msg.type === 'trade') {
// 非核心UI:低优先级更新历史成交
startTransition(() => {
setTrades(prev => [...prev, msg.data].slice(-100));
});
}
};
return (
<div>
{/* 核心UI:订单簿,同步更新,无延迟 */}
<OrderBook data={orderBook} />
{/* 非核心UI:历史成交,延迟更新,显示加载状态 */}
{isPending ? <div>加载中...</div> : <TradeList data={deferredTrades} />}
</div>
);
}
加载图表中...
这套架构在金融交易场景已经经过验证,支撑过每秒几万条的实时行情推送,页面 FPS 稳定在 60 帧,完全符合百万级并发的要求。