教學 專家

WebSocket 即時交易監控系統開發|Socket.io 與 React 即時資料流整合指南

Sentinel Team · 2026-03-04
WebSocket 即時交易監控系統開發|Socket.io 與 React 即時資料流整合指南

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: 取決於伺服器配置:

Q2: 如何處理防火牆阻擋 WebSocket?

A: Socket.io 自動降級:

  1. 嘗試 WebSocket
  2. 失敗則改用 HTTP 長輪詢
  3. 保持相同 API 介面

Q3: 手機端 WebSocket 會被系統斷線嗎?

A: 會,需特殊處理:

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: 明確分工:

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 則是生產環境的最佳選擇。關鍵成功因素:

  1. 合理的頻道設計:按領域與權限分層
  2. 穩定的重連機制:確保網路不穩時的用戶體驗
  3. 效能優化:壓縮、節流、連線池管理
  4. 完善監控:及時發現並解決問題

立即行動


延伸閱讀


作者:Sentinel Team

最後更新:2026-03-04

技術驗證:本文基於 Sentinel Bot 生產環境實戰經驗


正在建構即時交易系統?立即體驗 Sentinel Bot 的 WebSocket 驅動監控,或下載我們的 WebSocket 模板快速開始。

免費試用 Sentinel Bot | 下載 WebSocket 模板 | 技術諮詢


相關文章

同系列延伸閱讀

跨系列推薦