WebSocket 即時交易監控系統開發
快速導覽:本文深入探討 WebSocket 在交易系統的完整應用,從 Socket.io 架構設計到 React 整合,提供加密貨幣即時監控系統開發的權威指南。預計閱讀時間 15 分鐘。
為什麼交易系統需要 WebSocket?
在自動化交易的世界裡,時間就是金錢。當比特幣價格在毫秒間波動,HTTP 輪詢的延遲可能意味著錯失交易機會或承擔不必要的風險。
根據 WebSocket RFC 6455 規範,WebSocket 提供全雙工、低延遲的通訊通道,是即時交易系統的技術基石。
HTTP 輪詢 vs WebSocket 比較
| 特性 | HTTP 輪詢 | WebSocket | 交易系統影響 |
|:---|:---|:---|:---|
| 連線方式 | 每次請求新建連線 | 單一長連線 | 減少連線開銷 |
| 延遲 | 100-500ms | 10-50ms | 10 倍提升 |
| 伺服器推送 | 不支援 | 原生支援 | 即時行情更新 |
| 頻寬使用 | 高(HTTP 頭部重複)| 低(僅資料)| 節省 70%+ 頻寬 |
| 即時性 | 偽即時 | 真即時 | 關鍵交易時機 |
| 擴展性 | 差(連線數受限)| 佳(支援 10K+ 連線)| 支援多用戶 |
關鍵洞察:專業級交易系統必須使用 WebSocket。HTTP 輪詢僅適合低頻率資料(如每分鐘更新一次的持倉報告)。
WebSocket 核心概念與協議
WebSocket 握手過程
客戶端 伺服器
│ │
│ 1. HTTP Upgrade 請求 │
│ ───────────────────────────> │
│ GET /ws HTTP/1.1 │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: xxx │
│ │
│ 2. 協議升級回應 │
│ <─────────────────────────── │
│ HTTP/1.1 101 Switching │
│ Upgrade: websocket │
│ Sec-WebSocket-Accept: yyy │
│ │
│ 3. WebSocket 連線建立 │
│ <══════════════════════════> │
│ 全雙工資料傳輸 │
Socket.io 的優勢
雖然原生 WebSocket 已經很強大,Socket.io 提供了更多生產環境需要的功能:
| 功能 | 原生 WebSocket | Socket.io | 交易系統價值 |
|:---|:---:|:---:|:---|
| 自動重連 | ❌ 需自行實作 | ✅ 內建 | 網路不穩時自動恢復 |
| 降級機制 | ❌ 無 | ✅ HTTP 長輪詢 | 舊瀏覽器/防火牆相容 |
| 房間機制 | ❌ 需自行實作 | ✅ 內建 | 交易對分頻訂閱 |
| 廣播 | ❌ 需自行實作 | ✅ 內建 | 系統公告推送 |
| 中介軟體 | ❌ 需自行實作 | ✅ 內建 | 認證與權限控制 |
| 二進位支援 | ✅ 支援 | ✅ 支援 | 高效資料傳輸 |
交易系統 WebSocket 架構設計
整體架構圖
┌─────────────────────────────────────────────────────────────┐
│ 客戶端 (React) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ PriceFeed │ │ BotMonitor │ │ NotificationCenter │ │
│ │ Hook │ │ Hook │ │ Hook │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Socket.io │ │
│ │ Client │ │
│ └──────┬──────┘ │
└──────────────────────────┼──────────────────────────────────┘
│ WebSocket
┌──────────────────────────┼──────────────────────────────────┐
│ 伺服器 (Node.js) │
│ ┌──────┴──────┐ │
│ │ Socket.io │ │
│ │ Server │ │
│ └──────┬──────┘ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ Price │ │ Bot │ │ Notification│ │
│ │ Service │ │ Service │ │ Service │ │
│ └────┬────┘ └────┬────┘ └─────┬─────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ Redis │ │ PostgreSQL│ │ Firebase │ │
│ │ Pub/Sub │ │ (State) │ │ (Push) │ │
│ └─────────┘ └─────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
頻道設計原則
// 頻道命名規範
const CHANNELS = {
// 公開頻道(無需認證)
PUBLIC: {
PRICES: 'price:public', // 公開行情
ANNOUNCEMENTS: 'announcement', // 系統公告
},
// 用戶私有頻道(需認證)
USER: {
NOTIFICATIONS: (userId: string) => `user:${userId}:notifications`,
BALANCE: (userId: string) => `user:${userId}:balance`,
},
// 機器人頻道(需授權)
BOT: {
STATUS: (botId: string) => `bot:${botId}:status`,
TRADES: (botId: string) => `bot:${botId}:trades`,
POSITIONS: (botId: string) => `bot:${botId}:positions`,
LOGS: (botId: string) => `bot:${botId}:logs`,
},
// 交易對頻道(動態訂閱)
SYMBOL: {
TICKER: (symbol: string) => `symbol:${symbol}:ticker`,
ORDERBOOK: (symbol: string) => `symbol:${symbol}:orderbook`,
TRADES: (symbol: string) => `symbol:${symbol}:trades`,
},
};
Socket.io 伺服器實作
基礎架構
// server/websocket/index.ts
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export function createWebSocketServer(httpServer) {
const io = new Server(httpServer, {
cors: {
origin: process.env.FRONTEND_URL,
credentials: true,
},
// 效能調校
pingTimeout: 60000,
pingInterval: 25000,
transports: ['websocket', 'polling'],
});
// Redis 適配器(多伺服器擴展)
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
// 中介軟體:認證
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const user = await verifyToken(token);
socket.data.user = user;
next();
} catch (err) {
next(new Error('Authentication error'));
}
});
// 連線處理
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// 註冊事件處理器
registerPriceHandlers(socket);
registerBotHandlers(socket);
registerNotificationHandlers(socket);
// 斷線處理
socket.on('disconnect', (reason) => {
console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
});
});
return io;
}
價格推送服務
// server/websocket/handlers/price.ts
export function registerPriceHandlers(socket: Socket) {
// 用戶訂閱特定交易對
socket.on('price:subscribe', (symbols: string[]) => {
symbols.forEach((symbol) => {
socket.join(`symbol:${symbol}:ticker`);
});
// 立即推送當前價格
symbols.forEach(async (symbol) => {
const price = await getLatestPrice(symbol);
socket.emit('price:update', { symbol, ...price });
});
});
// 取消訂閱
socket.on('price:unsubscribe', (symbols: string[]) => {
symbols.forEach((symbol) => {
socket.leave(`symbol:${symbol}:ticker`);
});
});
}
// 外部價格服務推送時廣播
export function broadcastPriceUpdate(io: Server, symbol: string, data: PriceData) {
io.to(`symbol:${symbol}:ticker`).emit('price:update', {
symbol,
timestamp: Date.now(),
...data,
});
}
機器人監控服務
// server/websocket/handlers/bot.ts
export function registerBotHandlers(socket: Socket) {
const userId = socket.data.user.id;
// 訂閱用戶的所有機器人
socket.on('bot:subscribe', async () => {
const bots = await getUserBots(userId);
bots.forEach((bot) => {
socket.join(`bot:${bot.id}:status`);
socket.join(`bot:${bot.id}:trades`);
socket.join(`bot:${bot.id}:positions`);
});
socket.emit('bot:subscribed', bots.map((b) => b.id));
});
// 訂閱特定機器人
socket.on('bot:subscribe:one', async (botId: string) => {
// 權限檢查
const hasAccess = await checkBotAccess(userId, botId);
if (!hasAccess) {
socket.emit('error', { message: 'Access denied' });
return;
}
socket.join(`bot:${botId}:status`);
socket.join(`bot:${botId}:trades`);
socket.join(`bot:${botId}:positions`);
socket.join(`bot:${botId}:logs`);
// 推送當前狀態
const status = await getBotStatus(botId);
socket.emit('bot:status', { botId, ...status });
});
}
// 機器人狀態變更時廣播
export function broadcastBotStatus(io: Server, botId: string, status: BotStatus) {
io.to(`bot:${botId}:status`).emit('bot:status', {
botId,
timestamp: Date.now(),
...status,
});
}
// 新交易時廣播
export function broadcastBotTrade(io: Server, botId: string, trade: Trade) {
io.to(`bot:${botId}:trades`).emit('bot:trade', {
botId,
timestamp: Date.now(),
trade,
});
}
React 客戶端整合
WebSocket Manager 封裝
// src/api/websocket.ts
import { io, Socket } from 'socket.io-client';
import { useEffect, useRef, useCallback } from 'react';
class WebSocketManager {
private socket: Socket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private listeners: Map<string, Set<Function>> = new Map();
connect(token: string) {
if (this.socket?.connected) return;
this.socket = io(process.env.VITE_WS_URL, {
auth: { token },
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: this.maxReconnectAttempts,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
this.socket.on('connect', () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
});
this.socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
if (reason === 'io server disconnect') {
// 伺服器主動斷線,需手動重連
setTimeout(() => this.connect(token), 1000);
}
});
this.socket.on('error', (error) => {
console.error('WebSocket error:', error);
});
// 重新註冊所有監聽器
this.listeners.forEach((callbacks, event) => {
callbacks.forEach((callback) => {
this.socket?.on(event, callback);
});
});
}
disconnect() {
this.socket?.disconnect();
this.socket = null;
}
subscribe(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
this.socket?.on(event, callback);
}
this.listeners.get(event)!.add(callback);
return () => this.unsubscribe(event, callback);
}
unsubscribe(event: string, callback: Function) {
this.listeners.get(event)?.delete(callback);
this.socket?.off(event, callback);
}
emit(event: string, data?: any) {
this.socket?.emit(event, data);
}
isConnected() {
return this.socket?.connected ?? false;
}
}
export const wsManager = new WebSocketManager();
React Hooks 封裝
// src/hooks/useWebSocket.ts
export function useWebSocket() {
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
wsManager.connect(token);
}
return () => {
wsManager.disconnect();
};
}, []);
return {
subscribe: wsManager.subscribe.bind(wsManager),
emit: wsManager.emit.bind(wsManager),
isConnected: wsManager.isConnected.bind(wsManager),
};
}
// 價格訂閱 Hook
export function usePriceSubscription(symbols: string[]) {
const [prices, setPrices] = useState<Record<string, Price>>({});
useEffect(() => {
if (symbols.length === 0) return;
// 訂閱
wsManager.emit('price:subscribe', symbols);
// 監聽更新
const unsubscribe = wsManager.subscribe('price:update', (data) => {
setPrices((prev) => ({
...prev,
[data.symbol]: data,
}));
});
return () => {
unsubscribe();
wsManager.emit('price:unsubscribe', symbols);
};
}, [symbols.join(',')]);
return prices;
}
// 機器人監控 Hook
export function useBotMonitor(botId: string) {
const [status, setStatus] = useState<BotStatus | null>(null);
const [trades, setTrades] = useState<Trade[]>([]);
const [position, setPosition] = useState<Position | null>(null);
useEffect(() => {
if (!botId) return;
wsManager.emit('bot:subscribe:one', botId);
const unsubStatus = wsManager.subscribe('bot:status', (data) => {
if (data.botId === botId) setStatus(data);
});
const unsubTrade = wsManager.subscribe('bot:trade', (data) => {
if (data.botId === botId) {
setTrades((prev) => [data.trade, ...prev].slice(0, 100));
}
});
const unsubPosition = wsManager.subscribe('bot:position', (data) => {
if (data.botId === botId) setPosition(data.position);
});
return () => {
unsubStatus();
unsubTrade();
unsubPosition();
};
}, [botId]);
return { status, trades, position };
}
想了解更多 React 整合技巧?參考 React 18 自動化交易介面開發指南。
效能優化策略
資料壓縮與節流
// 價格資料壓縮
interface CompressedPrice {
s: string; // symbol
p: number; // price
v: number; // volume
t: number; // timestamp
}
// 發送前壓縮
function compressPrice(data: PriceData): CompressedPrice {
return {
s: data.symbol,
p: data.price,
v: data.volume,
t: Date.now(),
};
}
// 節流處理(100ms 內只發最後一次)
import { throttle } from 'lodash';
const throttledBroadcast = throttle(
(io, symbol, data) => broadcastPriceUpdate(io, symbol, data),
100,
{ leading: false, trailing: true }
);
連線池管理
// 限制每個用戶的連線數
const userConnections: Map<string, Set<string>> = new Map();
io.use(async (socket, next) => {
const userId = socket.data.user.id;
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
const connections = userConnections.get(userId)!;
// 限制 3 個同時連線
if (connections.size >= 3) {
// 斷開最舊的連線
const oldestSocketId = connections.values().next().value;
const oldestSocket = io.sockets.sockets.get(oldestSocketId);
oldestSocket?.disconnect(true);
connections.delete(oldestSocketId);
}
connections.add(socket.id);
socket.on('disconnect', () => {
connections.delete(socket.id);
});
next();
});
錯誤處理與監控
重連策略
// 指數退避重連
const reconnectStrategy = {
attempts: 0,
maxAttempts: 10,
baseDelay: 1000,
getDelay() {
const delay = this.baseDelay * Math.pow(2, this.attempts);
return Math.min(delay, 30000); // 最大 30 秒
},
reset() {
this.attempts = 0;
},
increment() {
this.attempts = Math.min(this.attempts + 1, this.maxAttempts);
},
};
// 使用
socket.on('disconnect', () => {
const delay = reconnectStrategy.getDelay();
reconnectStrategy.increment();
setTimeout(() => {
socket.connect();
}, delay);
});
socket.on('connect', () => {
reconnectStrategy.reset();
});
監控指標
// 連線統計
const metrics = {
connections: 0,
messagesPerSecond: 0,
reconnections: 0,
errors: 0,
};
io.on('connection', (socket) => {
metrics.connections++;
socket.on('disconnect', () => {
metrics.connections--;
});
});
// 每秒計算訊息量
let messageCount = 0;
io.on('connection', (socket) => {
const originalEmit = socket.emit;
socket.emit = function(...args) {
messageCount++;
return originalEmit.apply(this, args);
};
});
setInterval(() => {
metrics.messagesPerSecond = messageCount;
messageCount = 0;
// 發送到監控系統
console.log('WebSocket Metrics:', metrics);
}, 1000);
實戰案例:Sentinel Bot 的 WebSocket 架構
效能數據
| 指標 | 數值 | 說明 |
|:---|:---:|:---|
| 同時連線 | 10,000+ | 單一伺服器 |
| 訊息延遲 | < 50ms | P95 |
| 重連成功率 | 99.8% | 網路閃斷後 |
| CPU 使用率 | 15% | 滿載時 |
| 記憶體使用 | 2GB | 10K 連線 |
架構決策
為什麼選擇 Socket.io?
├── 自動重連機制節省開發時間
├── 房間機制簡化訂閱管理
├── Redis 適配器支援水平擴展
└── 豐富的中介軟體生態
為什麼不用原生 WebSocket?
├── 需自行實作重連邏輯
├── 無內建房間機制
├── 生產環境功能缺失
└── 開發成本更高
常見問題 FAQ
Q1: WebSocket 連線數上限是多少?
A: 取決於伺服器配置:
- Node.js 單機:約 10,000-30,000 連線
- 多機 + Redis:理論無限
- 實務建議:單機 5,000 連線,超過即水平擴展
Q2: 如何處理防火牆阻擋 WebSocket?
A: Socket.io 自動降級:
- 嘗試 WebSocket
- 失敗則改用 HTTP 長輪詢
- 保持相同 API 介面
Q3: 手機端 WebSocket 會被系統斷線嗎?
A: 會,需特殊處理:
- 背景時降低更新頻率
- 使用 Service Worker 保持連線
- 回到前景時立即重新同步
Q4: 如何保證訊息順序?
A: 實作序號機制:
// 發送時附加序號
socket.emit('data', { seq: 123, payload: data });
// 客戶端檢查並排序
const buffer = new Map();
let expectedSeq = 1;
socket.on('data', ({ seq, payload }) => {
if (seq === expectedSeq) {
process(payload);
expectedSeq++;
// 處理緩衝中的後續訊息
} else {
buffer.set(seq, payload);
}
});
Q5: WebSocket 與 REST API 如何分工?
A: 明確分工:
- WebSocket:即時資料(價格、狀態)
- REST API:一次性操作(下單、設定)
Q6: 如何測試 WebSocket?
A: 使用 socket.io-client + Jest:
import { io } from 'socket.io-client';
it('should receive price updates', (done) => {
const client = io('ws://localhost:3000');
client.emit('price:subscribe', ['BTC/USDT']);
client.on('price:update', (data) => {
expect(data.symbol).toBe('BTC/USDT');
client.disconnect();
done();
});
});
Q7: 如何優化大量廣播的效能?
A: 使用 Redis Pub/Sub:
// 價格服務發布
redisClient.publish('price:BTC/USDT', JSON.stringify(priceData));
// 所有伺服器訂閱
redisClient.subscribe('price:BTC/USDT', (message) => {
const data = JSON.parse(message);
io.to(`symbol:BTC/USDT:ticker`).emit('price:update', data);
});
Q8: 如何監控 WebSocket 健康狀況?
A: 多維度監控:
- 連線數
- 訊息延遲
- 重連頻率
- 錯誤率
- 記憶體使用
結論與行動建議
WebSocket 是交易系統的技術基石,Socket.io 則是生產環境的最佳選擇。關鍵成功因素:
- 合理的頻道設計:按領域與權限分層
- 穩定的重連機制:確保網路不穩時的用戶體驗
- 效能優化:壓縮、節流、連線池管理
- 完善監控:及時發現並解決問題
立即行動
- [ ] 評估現有資料更新機制
- [ ] 設計 WebSocket 頻道架構
- [ ] 實作基礎連線管理
- [ ] 建立監控與告警
延伸閱讀:
作者:Sentinel Team
最後更新:2026-03-04
技術驗證:本文基於 Sentinel Bot 生產環境實戰經驗
正在建構即時交易系統?立即體驗 Sentinel Bot 的 WebSocket 驅動監控,或下載我們的 WebSocket 模板快速開始。
免費試用 Sentinel Bot | 下載 WebSocket 模板 | 技術諮詢
相關文章
同系列延伸閱讀
- React 18 交易介面開發 - 前端介面整合
- TanStack Query 5 資料獲取 - 資料同步策略