WebSocket 即時應用開發:從連線管理到生產部署
6 分鐘閱讀 1,050 字
WebSocket 即時應用開發:從連線管理到生產部署
WebSocket 讓瀏覽器和伺服器之間建立持久的雙向通訊通道。相較於 HTTP 的「請求-回應」模式,WebSocket 讓伺服器可以主動推送資料,客戶端也可以隨時發送訊息。這讓即時聊天、多人協作、線上遊戲等應用成為可能。
WebSocket 與 HTTP 的差異
HTTP(短連線):
客戶端 → [建立連線] → 伺服器
客戶端 → [發送請求] → 伺服器
客戶端 ← [回傳回應] ← 伺服器
客戶端 → [關閉連線] → 伺服器
(每次請求重複上述流程)
WebSocket(長連線):
客戶端 → [HTTP Upgrade 握手] → 伺服器
客戶端 ↔ [持久雙向通道] ↔ 伺服器
(一次連線,持續通訊)伺服器端實作(Node.js + ws 套件)
npm install ws
npm install --save-dev @types/wsimport { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
const server = createServer();
const wss = new WebSocketServer({ server });
// 儲存所有連線
const clients = new Map<string, WebSocket>();
wss.on('connection', (ws, req) => {
const clientId = generateId();
clients.set(clientId, ws);
console.log(`客戶端 ${clientId} 已連線,目前共 ${clients.size} 個連線`);
// 發送歡迎訊息
ws.send(JSON.stringify({
type: 'welcome',
clientId,
connectedCount: clients.size
}));
// 接收訊息
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
handleMessage(clientId, message);
} catch {
ws.send(JSON.stringify({ type: 'error', message: '無效的 JSON 格式' }));
}
});
// 處理斷線
ws.on('close', (code, reason) => {
clients.delete(clientId);
console.log(`客戶端 ${clientId} 已斷線(code: ${code})`);
// 通知其他客戶端
broadcast({ type: 'user-left', clientId });
});
// 錯誤處理
ws.on('error', (error) => {
console.error(`客戶端 ${clientId} 錯誤:`, error);
clients.delete(clientId);
});
});
function broadcast(message: object, excludeId?: string) {
const payload = JSON.stringify(message);
clients.forEach((ws, id) => {
if (id !== excludeId && ws.readyState === WebSocket.OPEN) {
ws.send(payload);
}
});
}
function handleMessage(clientId: string, message: any) {
switch (message.type) {
case 'chat':
broadcast({ type: 'chat', from: clientId, text: message.text }, clientId);
break;
case 'ping':
clients.get(clientId)?.send(JSON.stringify({ type: 'pong' }));
break;
}
}
server.listen(3000, () => console.log('WebSocket 伺服器啟動在 port 3000'));心跳機制(Heartbeat)
網路中間設備(NAT、防火牆)可能會在一段時間後斷開閒置的連線。心跳機制可以保持連線活躍,並偵測失效的連線:
const HEARTBEAT_INTERVAL = 30_000; // 30 秒
const HEARTBEAT_TIMEOUT = 10_000; // 10 秒內未回應視為斷線
interface ExtendedWebSocket extends WebSocket {
isAlive: boolean;
clientId: string;
}
wss.on('connection', (ws: ExtendedWebSocket) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true; // 收到 pong,連線存活
});
});
// 定期發送 ping
const heartbeatTimer = setInterval(() => {
wss.clients.forEach((ws: ExtendedWebSocket) => {
if (!ws.isAlive) {
// 上次 ping 沒有回應,強制斷線
console.log(`客戶端 ${ws.clientId} 心跳超時,斷開連線`);
return ws.terminate();
}
ws.isAlive = false;
ws.ping(); // 發送 WebSocket 協定層的 ping
});
}, HEARTBEAT_INTERVAL);
wss.on('close', () => clearInterval(heartbeatTimer));客戶端實作(帶自動重連)
class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
private shouldReconnect = true;
constructor(
private url: string,
private onMessage: (data: any) => void
) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 連線成功');
this.reconnectDelay = 1000; // 重置延遲時間
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch {
console.error('訊息解析失敗');
}
};
this.ws.onclose = (event) => {
if (this.shouldReconnect && !event.wasClean) {
console.log(`連線中斷,${this.reconnectDelay}ms 後重連...`);
setTimeout(() => this.connect(), this.reconnectDelay);
// 指數退避:每次重連延遲加倍,最多 30 秒
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket 錯誤:', error);
};
}
send(data: object) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.warn('連線尚未建立,訊息未發送');
}
}
close() {
this.shouldReconnect = false;
this.ws?.close(1000, 'Client initiated close');
}
}水平擴展:多台伺服器同步
當需要多台 WebSocket 伺服器時,必須有機制讓連線在不同伺服器的客戶端互相接收訊息:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
// 使用 Redis Pub/Sub 在伺服器間傳遞訊息
async function broadcastViaRedis(message: object) {
await pubClient.publish('ws:broadcast', JSON.stringify(message));
}
// 每台伺服器訂閱並轉發給本地客戶端
await subClient.subscribe('ws:broadcast', (payload) => {
const message = JSON.parse(payload);
// 發送給這台伺服器上的所有客戶端
localClients.forEach(ws => ws.send(payload));
});在 Nginx 後面的設定
map $http_upgrade $connection_upgrade {
default upgrade;
' close;
}
server {
listen 80;
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebSocket 連線的超時時間(0 = 永不超時)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}使用 Socket.io 簡化開發
對於大型應用,直接使用原生 WebSocket 有不少重複工作。Socket.io 在 WebSocket 之上提供了更豐富的功能:
import { Server } from 'socket.io';
const io = new Server(httpServer, {
cors: { origin: '*' }
});
io.on('connection', (socket) => {
// 房間功能
socket.join('general');
// 收發訊息
socket.on('chat:message', (msg) => {
io.to('general').emit('chat:message', {
from: socket.id,
text: msg.text,
timestamp: Date.now()
});
});
// 自動處理斷線重連
// 自動降級到 Long Polling(不支援 WebSocket 時)
});小結
WebSocket 是即時應用的基石,但生產環境需要考慮心跳機制、自動重連、水平擴展、Nginx 代理等面向。對於新專案,可以先用 Socket.io 快速開發,待需要精細控制時再考慮原生 WebSocket。關鍵是理解底層機制,才能在遇到問題時快速診斷和解決。
分享這篇文章