本文 AI 產出,尚未審核

ExpressJS (TypeScript) – WebSocket 與 Real‑time:使用 Socket.IO 或 ws


簡介

在現代 Web 應用程式中,即時互動已成為提升使用者體驗的關鍵功能。無論是線上聊天、即時通知、協同編輯或是遊戲即時對戰,都離不開雙向、低延遲的通訊機制。傳統的 HTTP 請求是單向的、每次都需要重新建立連線,無法滿足這類需求。

Node.js 生態系統提供了兩套主流的 WebSocket 解決方案:Socket.IO(封裝了 WebSocket 以及多種 fallback)與 ws(純粹的 WebSocket 實作)。在使用 ExpressJS + TypeScript 建置伺服器時,如何選擇、整合與最佳化,直接影響到專案的可維護性與效能。本文將從概念講起,示範完整範例,並探討常見陷阱與實務應用,幫助你在 Express 專案中快速上手即時功能。


核心概念

1. WebSocket 基礎

WebSocket 是一種在單一 TCP 連線上提供 全雙工(full‑duplex) 通訊的協定。瀏覽器與伺服器完成一次 HTTP 握手(Upgrade),之後即進入持久連線,雙方可自由地傳送文字或二進位資料,延遲通常在毫秒等級。

重點:WebSocket 只是一個協定,實作上仍需要自行處理斷線重連、訊息編碼、房間管理等功能。這也是 Socket.IO 出現的原因。

2. Socket.IO 與 ws 的差異

項目 Socket.IO ws
抽象層級 高階封裝,提供事件機制、房間(rooms)與命名空間(namespaces) 純粹 WebSocket,僅提供 send / onmessage
Fallback 支援 HTTP long‑polling、XHR‑polling 等,確保在不支援 WebSocket 的環境仍能運作 僅在瀏覽器支援時才能使用
協議 自己的協議(在 WebSocket 基礎上攜帶額外訊息頭) 標準 RFC6455
體積 較大(包含額外協商、心跳等) 輕量、效能最佳
使用情境 需要快速開發、房間管理、回傳 ACK 等功能的應用 需要最高效能、只傳遞原始資料的場景(如金融行情)

3. 在 Express + TypeScript 中使用 Socket.IO

3.1 安裝與型別定義

npm i express socket.io
npm i -D @types/express @types/socket.io ts-node-dev typescript

3.2 建立基本伺服器

// src/server.ts
import express, { Request, Response } from 'express';
import { createServer } from 'http';
import { Server as SocketIOServer, Socket } from 'socket.io';

const app = express();
const httpServer = createServer(app);
const io = new SocketIOServer(httpServer, {
  // 這裡可以設定 CORS、心跳間隔等
  cors: { origin: '*', methods: ['GET', 'POST'] },
});

app.get('/', (req: Request, res: Response) => {
  res.sendFile(__dirname + '/index.html');
});

// 監聽連線事件
io.on('connection', (socket: Socket) => {
  console.log(`🟢 使用者已連線:${socket.id}`);

  // 接收來自客戶端的訊息
  socket.on('chatMessage', (msg: string) => {
    console.log(`收到訊息:${msg}`);
    // 廣播給所有人(包括自己)
    io.emit('chatMessage', msg);
  });

  // 斷線處理
  socket.on('disconnect', (reason) => {
    console.log(`🔴 使用者斷線:${socket.id}(${reason})`);
  });
});

const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
  console.log(`🚀 Server listening on http://localhost:${PORT}`);
});

3.3 前端簡易範例(index.html)

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>Socket.IO Chat Demo</title>
</head>
<body>
  <h2>即時聊天</h2>
  <ul id="messages"></ul>
  <input id="msgInput" autocomplete="off" placeholder="輸入訊息..."/>
  <button id="sendBtn">送出</button>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io(); // 直接連到同源的 Socket.IO 伺服器

    const messages = document.getElementById('messages');
    const input = document.getElementById('msgInput');
    const btn = document.getElementById('sendBtn');

    btn.onclick = () => {
      const msg = input.value.trim();
      if (msg) {
        socket.emit('chatMessage', msg); // 向伺服器送出事件
        input.value = '';
      }
    };

    // 接收伺服器廣播的訊息
    socket.on('chatMessage', (msg) => {
      const li = document.createElement('li');
      li.textContent = msg;
      messages.appendChild(li);
    });
  </script>
</body>
</html>

小技巧:在 TypeScript 中使用 socket.io 時,建議把 SocketServer 型別寫進 io.on('connection') 的 callback,這樣 IDE 會自動補全事件名稱與參數。

4. 使用 ws 實作純粹的 WebSocket

4.1 安裝

npm i ws
npm i -D @types/ws

4.2 建立伺服器

// src/ws-server.ts
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer, WebSocket } from 'ws';

const app = express();
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer });

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/ws-client.html');
});

// Broadcast 函式
function broadcast(data: string) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

// 監聽連線
wss.on('connection', (ws: WebSocket) => {
  console.log('🔹 新的 WebSocket 連線');

  ws.on('message', (msg: string) => {
    console.log(`收到訊息:${msg}`);
    broadcast(msg); // 把訊息廣播給所有連線
  });

  ws.on('close', () => {
    console.log('🔸 連線已關閉');
  });
});

const PORT = 4000;
httpServer.listen(PORT, () => {
  console.log(`🚀 ws server listening on http://localhost:${PORT}`);
});

4.3 前端簡易範例(ws-client.html)

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>ws Chat Demo</title>
</head>
<body>
  <h2>WebSocket 即時聊天</h2>
  <ul id="msgList"></ul>
  <input id="msgInput" placeholder="說點什麼..."/>
  <button id="sendBtn">送出</button>

  <script>
    const ws = new WebSocket(`ws://${location.host}`);

    const list = document.getElementById('msgList');
    const input = document.getElementById('msgInput');
    const btn = document.getElementById('sendBtn');

    ws.onopen = () => console.log('✅ 連線已開啟');
    ws.onmessage = (event) => {
      const li = document.createElement('li');
      li.textContent = event.data;
      list.appendChild(li);
    };
    ws.onclose = () => console.log('❌ 連線已關閉');

    btn.onclick = () => {
      const msg = input.value.trim();
      if (msg) {
        ws.send(msg);
        input.value = '';
      }
    };
  </script>
</body>
</html>

注意ws 只提供原始的 send/onmessage,若要實作房間、ACK 或自訂協議,需要自行在訊息中加入 JSON 標記或使用第三方庫。

5. 事件與房間的抽象(Socket.IO 範例)

// 在 io.on('connection') 內加入房間管理
io.on('connection', (socket) => {
  // 加入房間
  socket.on('joinRoom', (roomId: string) => {
    socket.join(roomId);
    socket.to(roomId).emit('notice', `${socket.id} 加入了房間`);
  });

  // 只向特定房間廣播訊息
  socket.on('roomMessage', ({ roomId, message }) => {
    io.to(roomId).emit('roomMessage', { sender: socket.id, message });
  });
});

此段程式展示 命名空間房間 的概念,讓多人聊天室、遊戲分區等需求變得簡單。


常見陷阱與最佳實踐

陷阱 說明 解決方式
斷線未重連 客戶端斷網或伺服器重啟時,連線會直接失效。 在前端使用 socket.io 時,預設會自動重連;若使用 ws,自行實作 onclosesetTimeout(() => new WebSocket(...), 3000)
訊息大小未限制 大量二進位或文字訊息會占滿記憶體,導致 OOM。 設定 maxHttpBufferSize(Socket.IO)或在 ws 中檢查 message.length
跨來源 CORS 設定錯誤 前端與 API 不同來源時,WebSocket 會被瀏覽器阻擋。 SocketIOServerws 建構子裡正確設定 cors 參數;若使用 Nginx 代理,記得放行 UpgradeConnection 標頭。
事件名稱拼寫不一致 客戶端與伺服器使用不同字串,訊息不會被捕捉。 建議把事件名稱抽成常數檔(enum SocketEvent { Chat = 'chatMessage', Join = 'joinRoom' }),雙方共用。
忘記關閉資源 停止服務時未關閉 httpServerws / socket.io,會佔用埠口。 process.on('SIGINT') 裏呼叫 httpServer.close(),以及 io.close()wss.close()

最佳實踐

  1. 使用 TypeScript 定義事件介面

    interface ChatPayload {
      roomId: string;
      message: string;
    }
    socket.on('roomMessage', (payload: ChatPayload) => { ... });
    
  2. 心跳與超時

    • socket.io 預設每 25 秒發送一次 ping,若需要自訂可在 ServerOptions 裡調整 pingIntervalpingTimeout
    • ws 需要自行實作 setInterval(() => ws.ping(), 30000),並在 pong 事件中更新最後回應時間。
  3. 分層架構
    把 WebSocket 邏輯抽成服務(service)或控制器(controller),保持 routerapp 的乾淨。例如:src/socket/chat.service.ts 內處理所有聊天室相關事件。

  4. 日誌與監控
    使用 winstonpino 記錄連線、斷線、錯誤訊息;配合 Prometheus 暴露 ws_active_connectionssocketio_events_total 等指標。

  5. 安全性

    • 在生產環境強制使用 wss(TLS)與 https
    • 透過 JWT 或 Session 於握手階段驗證身分(io.use((socket, next) => { /* verify token */ }))。

實際應用場景

場景 推薦方案 為什麼
即時聊天 / 社群 Socket.IO 內建房間、回傳 ACK、斷線自動重連,開發成本低。
多人協作白板 Socket.IO + 命名空間 可將不同文件分別放在不同命名空間,減少訊息干擾。
金融行情或 IoT 感測 ws 需要極低延遲、純二進位傳輸,避免額外封裝開銷。
遊戲即時對戰 Socket.IO(或 colyseus 房間與狀態同步功能便利,若對效能要求極高再考慮純 ws
推播通知 Socket.IO(或 socket.io-redis-adapter 多台伺服器間需要跨實例廣播,使用 redis adapter 可輕鬆擴展。

案例:在一個線上教學平台,我們使用 socket.io 讓老師與學生在課堂中即時互動。每個課程對應一個 命名空間,學生加入課程時自動加入對應 房間,老師的投票、問答即時推送給該房間內的所有學生,且透過 JWT 在 io.use 中驗證身分,確保只有已註冊的使用者能連線。


總結

  • WebSocket 為即時雙向通訊的核心協定,配合 Express + TypeScript 可快速建構高效能伺服器。
  • Socket.IO 提供事件化、房間、fallback 等高階功能,適合大部分即時應用;ws 則是輕量、純粹的選擇,適合對效能有極致要求的場景。
  • 在 TypeScript 中,利用介面與型別定義能大幅提升開發體驗與程式安全性。
  • 常見的斷線、CORS、訊息大小等問題,只要遵循最佳實踐(心跳、重連、資源釋放、日誌監控)即可避免。
  • 依照不同的業務需求(聊天、協作、行情、遊戲),選擇合適的套件與架構,才能在 Real‑time 的領域發揮最大效益。

祝你在 ExpressJS + TypeScript 的即時應用開發旅程中,快速上手、穩定運行,並持續探索更進階的分散式即時架構! 🚀