跳至正文

🔴 WebSocket vs SSE vs 長輪詢

📖 定義

即時通訊是一種使伺服器和客戶端之間能夠立即交換資料的技術。WebSocket 提供雙向即時通訊,SSE(Server-Sent Events)僅從伺服器向客戶端推送資料,長輪詢(Long Polling)是一種使用HTTP的即時通訊方式。每種方式都有其優缺點,需要根據使用場景進行選擇。

🎯 透過類比理解

電話 vs 廣播 vs 即時訊息

普通HTTP = 郵件
你: "你好?" (寄送信件)
↓ (幾天後)
朋友: "嗨!" (回覆)
↓ (幾天後)
你: "還好嗎?" (再寄信件)

- 慢
- 每次都要新建連線
- 不能即時

長輪詢 = 電話(等待通話)
你: "有事就告訴我!" (打電話等待)
↓ (等待中...)
朋友: "現在有話要說!" (回應)
↓ (通話結束)
你: "再有事告訴我!" (再次打電話)

- 基於HTTP
- 保持連線 → 回應 → 重新連線
- 效率低

SSE = 廣播
朋友: "大家好!" (開始廣播)
"今天天氣..." (持續發送)
"下一條新聞..." (持續發送)
你: (只能聽)

- 伺服器 → 客戶端(單向)
- 保持連線
- 簡單

WebSocket = 視訊通話
你: "你好!" (立即發送)
朋友: "很高興見到你!" (立即回應)
你: "在幹嘛?" (立即發送)
朋友: "在寫程式!" (立即回應)

- 雙向即時
- 保持連線
- 快速高效

餐廳點餐

普通HTTP = 自助服務
1. 去櫃台點餐
2. 等待
3. 取餐回座位
4. 再去櫃台點飲料
5. 再次等待

長輪詢 = 按鈴等待
1. 按鈴等待服務員(保持連線)
2. 服務員來 → "請點餐"
3. 點餐後再按鈴
4. 再次等待...

SSE = 廚房顯示螢幕
廚房: "1號客人,您的菜好了!"
廚房: "2號客人,正在準備!"
廚房: "3號客人,馬上就好!"
你: (只能聽)

WebSocket = 餐桌服務
你: "請給我水"
服務員: "好的,馬上就來"
你: "再來點泡菜"
服務員: "馬上就來"
服務員: "您的菜來了"
你: "謝謝"

- 自由對話
- 快速回應

⚙️ 工作原理

1. 普通HTTP vs 即時通訊

========== 普通HTTP(請求-回應) ==========

客戶端 伺服器
│ │
│ 1. 連線(Request) │
│────────────────────────>│
│ │
│ │ 處理中...
│ │
│ 2. 回應(Response) │
│<────────────────────────│
│ │
│ 連線結束 │
╳ ╳

│ 3. 重新連線 │
│────────────────────────>│
│ │

特點:
- 只有客戶端請求時才回應
- 每次重新連線
- 伺服器無法主動發送
- 無法即時

========== 即時通訊 ==========

客戶端 伺服器
│ │
│ 連線 │
│<───────────────────────>│
│ │
│ 保持雙向通訊 │
│<───────────────────────>│
│ │
│ 資料交換 │
│<───────────────────────>│
│ │

特點:
- 保持連線
- 伺服器可以主動發送
- 可以即時

2. 長輪詢(Long Polling)

客戶端                              伺服器
│ │
│ 1. 請求(有新資料嗎?) │
│──────────────────────────────────>│
│ │
│ 保持連線(等待中...) │
│ │
│ │ 沒有資料...
│ │ 繼續等待...
│ │
│ │ 新資料產生!
│ │
│ 2. 回應(有資料) │
│<──────────────────────────────────│
│ │
│ 連線結束 │
╳ ╳
│ │
│ 3. 立即重連 │
│──────────────────────────────────>│
│ │
│ 再次等待... │

流程:
1. 客戶端 → 伺服器: "有新資料嗎?"
2. 伺服器: 等待資料產生
3. 資料產生 → 回應
4. 連線結束
5. 立即重連(重複)

優點:
✅ 基於HTTP(利用現有基礎設施)
✅ 沒有防火牆問題
✅ 實作簡單

缺點:
❌ 效率低(持續重連)
❌ 伺服器負擔大
❌ 請求標頭開銷大

3. Server-Sent Events (SSE)

客戶端                              伺服器
│ │
│ 1. 連線請求 │
│──────────────────────────────────>│
│ │
│ 2. 保持連線(開始串流) │
│<══════════════════════════════════│
│ │
│ 3. 資料推送 │
│<──────────────────────────────────│
│ │
│ 4. 再次資料推送 │
│<──────────────────────────────────│
│ │
│ 連線保持中... │
│<══════════════════════════════════│

特點:
- 伺服器 → 客戶端(單向)
- 保持連線
- 基於HTTP
- 自動重連

優點:
✅ 簡單(瀏覽器內建)
✅ 自動重連
✅ 可透過事件ID恢復
✅ HTTP/2下高效

缺點:
❌ 單向(伺服器 → 客戶端)
❌ 不支援二進位資料
❌ IE不支援

4. WebSocket

客戶端                              伺服器
│ │
│ 1. HTTP Upgrade請求 │
│──────────────────────────────────>│
│ │
│ 2. Upgrade確認 │
│<──────────────────────────────────│
│ │
│ 切換到WebSocket協定 │
│<══════════════════════════════════>│
│ │
│ 3. 發送資料 │
│──────────────────────────────────>│
│ │
│ 4. 接收資料 │
│<──────────────────────────────────│
│ │
│ 5. 發送資料 │
│──────────────────────────────────>│
│ │
│ 雙向通訊持續... │
│<══════════════════════════════════>│

特點:
- 雙向即時通訊
- 獨立協定(ws://, wss://)
- 保持連線
- 低延遲

優點:
✅ 真正的即時
✅ 雙向通訊
✅ 低開銷
✅ 支援二進位

缺點:
❌ 複雜
❌ 可能有代理/防火牆問題
❌ 伺服器負擔(保持連線)

💡 實例

長輪詢示例

// ========== 伺服器(Express.js) ==========
const express = require('express');
const app = express();

let messages = [];
let waitingClients = [];

// 新增訊息(從其他地方呼叫)
function addMessage(message) {
messages.push(message);

// 立即回應等待中的客戶端
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}

// 長輪詢端點
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;

// 如果有新訊息立即回應
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}

// 否則加入等待清單(最多30秒)
waitingClients.push(res);

// 30秒逾時
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // 空回應
}
});
});

app.listen(3000);

// ========== 客戶端 ==========
let lastMessageId = 0;

async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();

// 處理新訊息
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('新訊息:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}

// 立即重連
await longPolling();
} catch (error) {
console.error('錯誤:', error);
// 3秒後重試
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}

// 開始
longPolling();

// ========== 問題 ==========
/*
1. 持續重連(效率低)
2. 網路使用量大
3. 伺服器負擔大
4. 電池消耗(行動裝置)
*/

Server-Sent Events (SSE) 示例

// ========== 伺服器(Express.js) ==========
const express = require('express');
const app = express();

app.use(express.static('public'));

// SSE端點
app.get('/events', (req, res) => {
// 設定SSE標頭
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// CORS(如果需要)
res.setHeader('Access-Control-Allow-Origin', '*');

// 連線確認訊息
res.write('data: Connected\n\n');

// 客戶端ID
const clientId = Date.now();
console.log(`客戶端 ${clientId} 已連線`);

// 每5秒發送時間
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: '你好!'
};

// 以SSE格式發送
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);

// 客戶端斷開連線處理
req.on('close', () => {
console.log(`客戶端 ${clientId} 已斷開`);
clearInterval(intervalId);
res.end();
});
});

// 事件發布API
const clients = [];

app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// 保存客戶端
clients.push(res);

req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});

// 向所有客戶端發送訊息
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;

clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});

res.json({ success: true });
});

app.listen(3000);

// ========== 客戶端(HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>

<script>
// SSE連線
const eventSource = new EventSource('/events');

// 接收訊息
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到資料:', data);

const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};

// 連線開始
eventSource.onopen = () => {
console.log('SSE已連線');
};

// 錯誤處理
eventSource.onerror = (error) => {
console.error('SSE錯誤:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE連線已關閉');
}
};

// 連線關閉(離開頁面時)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/

// ========== 進階功能 ==========

// 1. 指定事件類型
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 多種類型的事件
setInterval(() => {
// 普通訊息
res.write(`event: message\ndata: Hello\n\n`);

// 通知
res.write(`event: notification\ndata: New notification!\n\n`);

// 更新
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});

// 客戶端按事件類型處理
/*
eventSource.addEventListener('message', (e) => {
console.log('訊息:', e.data);
});

eventSource.addEventListener('notification', (e) => {
console.log('通知:', e.data);
});

eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('更新:', data);
});
*/

// 2. 事件ID(重連時從中斷處繼續接收)
let eventId = 0;

app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('最後的事件ID:', lastEventId);

// 只發送lastEventId之後的事件
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});

// 3. 設定重試時間
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 5秒後重連
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});

WebSocket示例(Socket.io)

// ========== 伺服器(Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});

app.use(express.static('public'));

// 管理已連線使用者
const users = new Map();

// WebSocket連線
io.on('connection', (socket) => {
console.log('新使用者連線:', socket.id);

// 保存使用者資訊
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} 已註冊`);

// 通知所有使用者
io.emit('user-joined', {
username,
totalUsers: users.size
});
});

// 接收訊息
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);

// 廣播給所有使用者
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});

// 正在輸入顯示
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});

socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});

// 私訊
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;

if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});

// 斷開連線
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} 已斷開`);
users.delete(socket.id);

// 通知所有使用者
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});

server.listen(3000, () => {
console.log('伺服器執行中: http://localhost:3000');
});

// ========== 客戶端(HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="名稱" />
<input id="message" placeholder="訊息" />
<button onclick="sendMessage()">發送</button>
</div>

<script>
// Socket.io連線
const socket = io('http://localhost:3000');

// 連線成功
socket.on('connect', () => {
console.log('已連線:', socket.id);
});

// 使用者註冊
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}

// 發送訊息
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}

// 接收訊息
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});

// 使用者加入
socket.on('user-joined', (data) => {
console.log(`${data.username} 加入 (共 ${data.totalUsers}人)`);
});

// 使用者離開
socket.on('user-left', (data) => {
console.log(`${data.username} 離開 (共 ${data.totalUsers}人)`);
});

// 正在輸入
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');

clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});

socket.on('user-typing', (username) => {
console.log(`${username}正在輸入...`);
});

// 斷開連線
socket.on('disconnect', () => {
console.log('已斷開連線');
});

// 重新連線
socket.on('reconnect', () => {
console.log('已重新連線');
});
</script>
</body>
</html>
*/

原生WebSocket API

// ========== 伺服器(ws函式庫) ==========
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
console.log('客戶端已連線');

// 接收訊息
ws.on('message', (message) => {
console.log('收到訊息:', message.toString());

// 廣播給所有客戶端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});

// 斷開連線
ws.on('close', () => {
console.log('客戶端已斷開');
});

// 錯誤
ws.on('error', (error) => {
console.error('WebSocket錯誤:', error);
});

// 歡迎訊息
ws.send('已連線到伺服器!');
});

console.log('WebSocket伺服器執行中: ws://localhost:8080');

// ========== 客戶端(瀏覽器) ==========

// WebSocket連線
const ws = new WebSocket('ws://localhost:8080');

// 連線開始
ws.addEventListener('open', (event) => {
console.log('WebSocket已連線');
ws.send('你好!');
});

// 接收訊息
ws.addEventListener('message', (event) => {
console.log('伺服器:', event.data);
});

// 錯誤
ws.addEventListener('error', (error) => {
console.error('WebSocket錯誤:', error);
});

// 斷開連線
ws.addEventListener('close', (event) => {
console.log('WebSocket已斷開');
if (event.code === 1000) {
console.log('正常結束');
} else {
console.log('異常結束:', event.code);
}
});

// 發送訊息
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocket未開啟');
}
}

// 關閉連線
function closeConnection() {
ws.close(1000, '正常結束');
}

// ========== 二進位資料傳輸 ==========

// 檔案傳輸
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}

// 伺服器接收二進位
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('收到二進位資料:', event.data.byteLength, 'bytes');
} else {
console.log('文字資料:', event.data);
}
});

// ========== Ping/Pong(保持連線) ==========

// 伺服器
wss.on('connection', (ws) => {
ws.isAlive = true;

ws.on('pong', () => {
ws.isAlive = true;
});
});

// 每30秒Ping一次
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 無回應則關閉連線
}

ws.isAlive = false;
ws.ping();
});
}, 30000);

// ========== 自動重連 ==========

let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;

function connect() {
ws = new WebSocket('ws://localhost:8080');

ws.addEventListener('open', () => {
console.log('已連線');
reconnectAttempts = 0;
});

ws.addEventListener('close', (event) => {
console.log('已斷開');

// 自動重連
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`${delay}ms後嘗試重連...`);
setTimeout(connect, delay);
} else {
console.error('重連失敗');
}
});

ws.addEventListener('error', (error) => {
console.error('錯誤:', error);
ws.close();
});
}

connect();

🤔 常見問題

Q1. 應該選擇哪種方式?

A:

✅ 選擇長輪詢的情況:
├─ 不支援WebSocket/SSE的環境
├─ 舊版系統
├─ 簡單通知
└─ 例如: 舊瀏覽器支援、簡單輪詢

使用案例:
- 股票行情(更新頻率低)
- 郵件檢查
- 簡單通知

✅ 選擇SSE的情況:
├─ 伺服器 → 客戶端單向
├─ 即時更新
├─ 希望實作簡單
├─ 需要自動重連
└─ 例如: 新聞推送、通知、儀表板

使用案例:
- 新聞推送(即時更新)
- 股票圖表(伺服器推送)
- 進度顯示
- 日誌串流
- 伺服器監控

✅ 選擇WebSocket的情況:
├─ 雙向即時通訊
├─ 低延遲重要
├─ 頻繁訊息交換
├─ 二進位資料
└─ 例如: 聊天、遊戲、協作工具

使用案例:
- 聊天應用
- 多人遊戲
- 協同文件編輯(Google Docs)
- 視訊會議
- IoT即時控制

📊 對比表:

特性 | 長輪詢 | SSE | WebSocket
------------|---------------|----------|----------
方向性 | 雙向 | 單向 | 雙向
協定 | HTTP | HTTP | WebSocket
延遲 | 高 | 低 | 非常低
開銷 | 高 | 中等 | 低
瀏覽器 | 全部 | 大部分 | 全部
複雜度 | 低 | 低 | 高
重連 | 手動 | 自動 | 手動
二進位 | 可能 | 不可 | 可能

建議:
- 簡單即時: SSE
- 聊天/遊戲: WebSocket
- 舊版支援: 長輪詢

Q2. 性能差異?

A:

// ========== 基準測試示例 ==========

// 1. 長輪詢
// 每次請求:
// - HTTP標頭: ~800 bytes
// - 重連時間: ~50ms
// - 伺服器資源: 每連線1執行緒

// 100並發連線時:
// - 每秒請求數: 100次
// - 資料傳輸: 80KB/s(僅標頭)
// - 伺服器負擔: 高

// 2. SSE
// 每個連線:
// - HTTP標頭: 僅初次(~800 bytes)
// - 訊息開銷: ~10 bytes
// - 伺服器資源: Keep-Alive連線

// 100並發連線時:
// - 初始連線: 80KB
// - 每條訊息: 1KB(無標頭)
// - 伺服器負擔: 中等

// 3. WebSocket
// 每個連線:
// - Upgrade標頭: 僅一次(~500 bytes)
// - 訊框開銷: 2~6 bytes
// - 伺服器資源: 保持連線

// 100並發連線時:
// - 初始連線: 50KB
// - 每條訊息: ~0.002KB(僅訊框)
// - 伺服器負擔: 低

// ========== 實際測量 ==========

// 測試: 100人每秒發送1條訊息

// 長輪詢
const longPollingBenchmark = {
每秒請求數: 100,
平均延遲: '200ms',
頻寬: '8MB/分',
伺服器CPU: '70%',
記憶體: '500MB'
};

// SSE
const sseBenchmark = {
每秒請求數: 0, // 保持連線
平均延遲: '10ms',
頻寬: '600KB/分',
伺服器CPU: '30%',
記憶體: '200MB'
};

// WebSocket
const webSocketBenchmark = {
每秒請求數: 0, // 保持連線
平均延遲: '2ms',
頻寬: '100KB/分',
伺服器CPU: '15%',
記憶體: '100MB'
};

// ========== 實用優化 ==========

// 1. 限制連線數
const MAX_CONNECTIONS = 1000;

io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});

// 2. 訊息壓縮
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // 只壓縮1KB以上
}
});

// 3. 批次處理
const messageQueue = [];

setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // 每100ms批次發送

// 4. 使用房間
socket.join('room1');
io.to('room1').emit('message', data); // 只發給room1

// 5. 二進位傳輸
// WebSocket二進位更高效
const buffer = Buffer.from('Hello');
socket.send(buffer); // 比文字快

// 6. 心跳優化
const HEARTBEAT_INTERVAL = 25000; // 25秒
const HEARTBEAT_TIMEOUT = 30000; // 30秒

setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);

Q3. 行動應用怎麼辦?

A:

// ========== 行動端考慮事項 ==========

// 1. 電池消耗
// 長輪詢: ❌ 高(持續重連)
// SSE: ⚠️ 中等
// WebSocket: ✅ 低(保持連線)

// 2. 網路切換
// WiFi <-> 行動數據切換時

// 自動重連
let ws;

function connect() {
ws = new WebSocket('wss://api.example.com');

ws.onclose = () => {
// 指數退避
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}

// 網路狀態檢測
window.addEventListener('online', () => {
console.log('已上線 - 重連');
connect();
});

window.addEventListener('offline', () => {
console.log('離線');
ws.close();
});

// 3. 背景處理
// 應用進入背景怎麼辦?

// React Native示例
import { AppState } from 'react-native';

AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// 背景 → 保持連線或關閉
ws.close();
} else if (nextAppState === 'active') {
// 前景 → 重連
connect();
}
});

// 4. 資料節省模式
// 根據使用者設定

const isDataSaverMode = await getDataSaverSetting();

if (isDataSaverMode) {
// 長輪詢(頻率降低)
setInterval(poll, 60000); // 每分鐘
} else {
// WebSocket(即時)
connect();
}

// 5. 推播通知聯動
// WebSocket + FCM/APNs組合

// 應用執行中: WebSocket
if (appIsActive) {
useWebSocket();
}

// 背景: 推播通知
if (appIsBackground) {
usePushNotification();
}

// ========== React Native示例 ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');

websocket.onopen = () => {
console.log('已連線');
setIsConnected(true);
};

websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};

websocket.onerror = (error) => {
console.error('WebSocket錯誤:', error);
};

websocket.onclose = () => {
console.log('已斷開');
setIsConnected(false);

// 3秒後重連
setTimeout(() => {
// 重連邏輯
}, 3000);
};

setWs(websocket);

// 清理
return () => {
websocket.close();
};
}, []);

const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};

return (
<View>
<Text>連線: {isConnected ? '是' : '否'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}

Q4. 如何保證安全?

A:

// ========== 1. 使用HTTPS/WSS ==========

// ❌ HTTP/WS(明文傳輸)
const ws = new WebSocket('ws://api.example.com');

// ✅ HTTPS/WSS(加密)
const ws = new WebSocket('wss://api.example.com');

// ========== 2. 認證 ==========

// 方法1: URL中的JWT(簡單但不推薦)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ URL中暴露令牌的風險

// 方法2: 連線後認證(推薦)
const ws = new WebSocket('wss://api.example.com');

ws.onopen = () => {
// 發送認證訊息
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};

// 伺服器
io.use((socket, next) => {
const token = socket.handshake.auth.token;

try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('認證失敗'));
}
});

// 方法3: Cookie(HttpOnly)
// 客戶端
const ws = new WebSocket('wss://api.example.com');
// Cookie自動發送

// 伺服器
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;

if (isValidSession(sessionId)) {
next();
} else {
next(new Error('認證失敗'));
}
});

// ========== 3. CORS配置 ==========

const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // 僅特定網域
credentials: true
}
});

// 多個網域
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS拒絕'));
}
},
credentials: true
}
});

// ========== 4. 速率限制 ==========

const rateLimitMap = new Map();

io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();

// 追蹤每個使用者的訊息數
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}

const userMessages = rateLimitMap.get(userId);

// 過濾最近1分鐘的訊息
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);

// 限制每分鐘10條
if (recentMessages.length >= 10) {
socket.emit('error', '超過訊息發送限制');
return;
}

recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);

// 處理訊息
handleMessage(data);
});
});

// ========== 5. 輸入驗證 ==========

socket.on('message', (data) => {
// 類型驗證
if (typeof data !== 'object') {
return socket.emit('error', '無效的資料格式');
}

// 必填欄位驗證
if (!data.type || !data.content) {
return socket.emit('error', '缺少必填欄位');
}

// 長度驗證
if (data.content.length > 1000) {
return socket.emit('error', '訊息太長');
}

// 防止XSS
const sanitizedContent = sanitizeHtml(data.content);

// 處理
broadcastMessage(sanitizedContent);
});

// ========== 6. 命名空間和房間 ==========

// 用命名空間隔離
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');

chatNamespace.on('connection', (socket) => {
// 普通聊天
});

adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// 僅管理員
});

// 用房間控制權限
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', '無房間存取權限');
}
});

// ========== 7. 防止DDoS ==========

// 限制每個IP的連線數
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();

io.on('connection', (socket) => {
const ip = socket.handshake.address;

const count = connectionsByIP.get(ip) || 0;

if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}

connectionsByIP.set(ip, count + 1);

socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});

Q5. 如何處理錯誤?

A:

// ========== 客戶端錯誤處理 ==========

class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}

connect() {
try {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('連線成功');
this.reconnectAttempts = 0;
this.onConnected();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('訊息解析錯誤:', error);
}
};

this.ws.onerror = (error) => {
console.error('WebSocket錯誤:', error);
this.onError(error);
};

this.ws.onclose = (event) => {
console.log('連線關閉:', event.code, event.reason);

// 正常關閉(1000)
if (event.code === 1000) {
console.log('正常結束');
return;
}

// 嘗試重連
this.reconnect();
};
} catch (error) {
console.error('連線失敗:', error);
this.reconnect();
}
}

reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('放棄重連');
this.onReconnectFailed();
return;
}

this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);

console.log(`${delay}ms後嘗試重連 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, delay);
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('發送錯誤:', error);
}
} else {
console.error('WebSocket未開啟');
// 保存到訊息佇列
this.queueMessage(data);
}
}

close() {
if (this.ws) {
this.ws.close(1000, '客戶端關閉');
}
}

// 事件處理器(重寫)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}

queueMessage(data) {
// 實作離線訊息佇列
}
}

// 使用
const client = new WebSocketClient('wss://api.example.com');

client.onConnected = () => {
console.log('已連線!');
};

client.onMessage = (data) => {
console.log('訊息:', data);
};

client.onError = (error) => {
console.error('發生錯誤:', error);
// 在UI中顯示錯誤
showErrorNotification('連線錯誤');
};

client.onReconnectFailed = () => {
// 處理重連失敗
showErrorModal('無法連線到伺服器');
};

client.connect();

// ========== 伺服器錯誤處理 ==========

io.on('connection', (socket) => {
// 錯誤處理器
socket.on('error', (error) => {
console.error('套接字錯誤:', error);
});

// 訊息處理錯誤
socket.on('message', async (data) => {
try {
// 輸入驗證
if (!isValidMessage(data)) {
throw new Error('無效訊息');
}

// 處理
await processMessage(data);
} catch (error) {
console.error('訊息處理錯誤:', error);

// 向客戶端發送錯誤
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});

// 連線斷開處理
socket.on('disconnect', (reason) => {
console.log('連線斷開:', reason);

// 清理使用者
cleanupUser(socket.userId);

// 通知其他使用者
socket.broadcast.emit('user-left', socket.userId);
});
});

// 全域錯誤處理器
io.engine.on('connection_error', (error) => {
console.error('連線錯誤:', error);
});

// ========== 逾時處理 ==========

socket.on('message', (data, callback) => {
// 設定逾時(5秒)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: '回應逾時'
});
}, 5000);

// 處理
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});

🎓 下一步

了解了即時通訊後,繼續學習:

  1. HTTP基礎(文件待編寫) - 理解HTTP協定
  2. 什麼是API?(文件待編寫) - API基本概念
  3. REST API vs GraphQL - API設計

動手實作

# ========== 1. Socket.io聊天應用 ==========

mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io

# 編寫server.js後
node server.js

# ========== 2. SSE即時通知 ==========

mkdir sse-demo
cd sse-demo
npm install express

# 編寫server.js
# 編寫index.html

node server.js
# 訪問 http://localhost:3000

# ========== 3. WebSocket遊戲 ==========

mkdir websocket-game
cd websocket-game
npm install ws express

# 實作多人遊戲
node server.js

🎬 總結

即時通訊是現代Web的核心技術:

  • 長輪詢: 舊版支援,基於HTTP
  • SSE: 伺服器推送,簡單實作
  • WebSocket: 雙向即時,最佳效能
  • 選擇標準: 需求、環境、複雜度

為您的專案選擇合適的即時通訊方式,創造出色的使用者體驗! 🔴