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時,建議把Socket、Server型別寫進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,自行實作 onclose → setTimeout(() => new WebSocket(...), 3000)。 |
| 訊息大小未限制 | 大量二進位或文字訊息會占滿記憶體,導致 OOM。 | 設定 maxHttpBufferSize(Socket.IO)或在 ws 中檢查 message.length。 |
| 跨來源 CORS 設定錯誤 | 前端與 API 不同來源時,WebSocket 會被瀏覽器阻擋。 | 在 SocketIOServer 或 ws 建構子裡正確設定 cors 參數;若使用 Nginx 代理,記得放行 Upgrade 與 Connection 標頭。 |
| 事件名稱拼寫不一致 | 客戶端與伺服器使用不同字串,訊息不會被捕捉。 | 建議把事件名稱抽成常數檔(enum SocketEvent { Chat = 'chatMessage', Join = 'joinRoom' }),雙方共用。 |
| 忘記關閉資源 | 停止服務時未關閉 httpServer、ws / socket.io,會佔用埠口。 |
在 process.on('SIGINT') 裏呼叫 httpServer.close(),以及 io.close()、wss.close()。 |
最佳實踐
使用 TypeScript 定義事件介面
interface ChatPayload { roomId: string; message: string; } socket.on('roomMessage', (payload: ChatPayload) => { ... });心跳與超時
socket.io預設每 25 秒發送一次 ping,若需要自訂可在ServerOptions裡調整pingInterval、pingTimeout。ws需要自行實作setInterval(() => ws.ping(), 30000),並在pong事件中更新最後回應時間。
分層架構
把 WebSocket 邏輯抽成服務(service)或控制器(controller),保持router與app的乾淨。例如:src/socket/chat.service.ts內處理所有聊天室相關事件。日誌與監控
使用winston、pino記錄連線、斷線、錯誤訊息;配合Prometheus暴露ws_active_connections、socketio_events_total等指標。安全性
- 在生產環境強制使用
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 的即時應用開發旅程中,快速上手、穩定運行,並持續探索更進階的分散式即時架構! 🚀