跳至正文

🔌 什麼是WebSocket?

📖 定義

WebSocket是一種實現客戶端和伺服器之間雙向即時通訊的協定。與HTTP不同,一旦建立連線就會持續保持,伺服器可以主動向客戶端發送資料(push)。用於聊天、即時通知、遊戲、協作工具等。

🎯 用比喻理解

電話 vs 郵件

HTTP (郵件)
├─ 寄信 (請求)
├─ 收到回信 (回應)
├─ 再寫信 (請求)
└─ 每次都需要新信件 (開銷)

WebSocket (電話)
├─ 一次接通電話
├─ 可以持續對話
├─ 雙方都可以先說話
└─ 掛斷前保持連線

快遞 vs 直通管道

HTTP
┌─────────┐ 請求 ┌─────────┐
│ 客戶端 │ ──────────→ │ 伺服器 │
└─────────┘ └─────────┘
┌─────────┐ 回應 ┌─────────┐
│ 客戶端 │ ←────────── │ 伺服器 │
└─────────┘ └─────────┘
每次都是新連線!

WebSocket
┌─────────┐ ┌─────────┐
│ 客戶端 │ ←─────────→ │ 伺服器 │
└─────────┘ 持久連線 └─────────┘
↕ 雙向

⚙️ 運作原理

1. WebSocket連線過程

1. HTTP握手請求
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade

2. 伺服器批准
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

3. WebSocket連線建立
↕ 開始雙向通訊

4. 資料傳輸
訊息 ←→ 訊息 ←→ 訊息

5. 關閉連線
呼叫 close()

2. HTTP vs WebSocket

HTTP (單向, 請求-回應)
客戶端 → 伺服器: 請求
客戶端 ← 伺服器: 回應
[連線關閉]

WebSocket (雙向, 持久)
客戶端 ↔ 伺服器
- 伺服器可以主動發送訊息
- 保持連線
- 即時通訊

💡 實際例子

基本WebSocket (客戶端)

// 在網頁瀏覽器中
const ws = new WebSocket('ws://localhost:8080');

// 連線成功
ws.onopen = () => {
console.log('連線完成!');
ws.send('你好!'); // 發送訊息
};

// 接收訊息
ws.onmessage = (event) => {
console.log('收到訊息:', event.data);
};

// 錯誤處理
ws.onerror = (error) => {
console.error('錯誤:', error);
};

// 連線關閉
ws.onclose = () => {
console.log('連線關閉');
};

// 發送訊息
ws.send('Hello Server!');
ws.send(JSON.stringify({ type: 'chat', message: 'Hi' }));

// 關閉連線
ws.close();

基本WebSocket伺服器 (Node.js)

const WebSocket = require('ws');

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

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

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

// 歡迎訊息
ws.send('歡迎!');

// 接收訊息
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('錯誤:', error);
});
});

Socket.io (實用聊天應用)

// ============ 伺服器 (server.js) ============
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);

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

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

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

// 使用者進入
socket.on('join', (username) => {
users.set(socket.id, username);

// 進入通知 (向所有人)
io.emit('user-joined', {
username,
userCount: users.size
});

console.log(`${username} 進入 (共 ${users.size} 人)`);
});

// 聊天訊息
socket.on('chat-message', (message) => {
const username = users.get(socket.id);

// 向所有人廣播
io.emit('chat-message', {
username,
message,
timestamp: new Date()
});
});

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

// 連線關閉
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);

// 退出通知
io.emit('user-left', {
username,
userCount: users.size
});

console.log(`${username} 退出 (剩餘: ${users.size} 人)`);
});
});

server.listen(3000, () => {
console.log('伺服器執行中: http://localhost:3000');
});
<!-- ============ 客戶端 (public/index.html) ============ -->
<!DOCTYPE html>
<html>
<head>
<title>即時聊天</title>
<style>
#messages {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
}
.system {
color: #999;
font-style: italic;
}
</style>
</head>
<body>
<h1>即時聊天</h1>
<div id="messages"></div>
<div id="typing"></div>
<input id="message-input" type="text" placeholder="輸入訊息...">
<button id="send-btn">發送</button>

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const typingDiv = document.getElementById('typing');

// 輸入使用者名稱
const username = prompt('請輸入使用者名稱:') || '匿名';
socket.emit('join', username);

// 使用者進入通知
socket.on('user-joined', (data) => {
addMessage(`${data.username} 進入了。 (${data.userCount} 人上線)`, 'system');
});

// 使用者退出通知
socket.on('user-left', (data) => {
addMessage(`${data.username} 退出了。 (${data.userCount} 人上線)`, 'system');
});

// 接收聊天訊息
socket.on('chat-message', (data) => {
const time = new Date(data.timestamp).toLocaleTimeString();
addMessage(`[${time}] ${data.username}: ${data.message}`);
});

// 顯示正在輸入
socket.on('user-typing', (username) => {
typingDiv.textContent = `${username} 正在輸入...`;
setTimeout(() => {
typingDiv.textContent = '';
}, 1000);
});

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

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});

// 輸入事件
let typingTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(typingTimeout);
socket.emit('typing');
typingTimeout = setTimeout(() => {
// 停止輸入
}, 500);
});

// 新增訊息
function addMessage(text, className = '') {
const messageEl = document.createElement('div');
messageEl.className = `message ${className}`;
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>

Room功能 (群組聊天)

// 伺服器
io.on('connection', (socket) => {
// 進入房間
socket.on('join-room', (roomId) => {
socket.join(roomId);
console.log(`${socket.id} 進入了房間 ${roomId}`);

// 只向同房間的人發送
socket.to(roomId).emit('user-joined-room', {
userId: socket.id,
roomId
});
});

// 向特定房間發送訊息
socket.on('room-message', ({ roomId, message }) => {
// 只向該房間發送
io.to(roomId).emit('room-message', {
userId: socket.id,
message
});
});

// 退出房間
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left-room', socket.id);
});
});

即時通知系統

// 伺服器
const notifications = io.of('/notifications');

notifications.on('connection', (socket) => {
console.log('通知連線:', socket.id);

// 使用者認證
const userId = socket.handshake.query.userId;

// 進入個人房間
socket.join(`user-${userId}`);

// 向特定使用者發送通知
socket.on('send-notification', ({ targetUserId, notification }) => {
notifications.to(`user-${targetUserId}`).emit('notification', notification);
});
});

// 從任何地方發送通知
function sendNotification(userId, notification) {
notifications.to(`user-${userId}`).emit('notification', {
type: notification.type,
message: notification.message,
timestamp: new Date()
});
}

// 例: 新訂單通知
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body);

// 向賣家發送即時通知
sendNotification(order.sellerId, {
type: 'new-order',
message: `收到新訂單!`,
orderId: order.id
});

res.json(order);
});

即時協作 (協同編輯)

// 伺服器
io.on('connection', (socket) => {
socket.on('join-document', (docId) => {
socket.join(docId);

// 發送目前文件內容
const document = getDocument(docId);
socket.emit('document-loaded', document);
});

// 文件修改
socket.on('document-change', ({ docId, changes }) => {
// 更新文件
updateDocument(docId, changes);

// 向其他使用者發送修改內容
socket.to(docId).emit('document-updated', {
userId: socket.id,
changes
});
});

// 共享游標位置
socket.on('cursor-move', ({ docId, position }) => {
socket.to(docId).emit('cursor-update', {
userId: socket.id,
position
});
});
});

🤔 常見問題

Q1. WebSocket vs HTTP輪詢?

A:

// HTTP輪詢 (低效)
// 客戶端定期向伺服器發送請求

setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(messages => {
// 檢查新訊息
});
}, 1000); // 每秒請求一次

// 問題:
// - 不必要的請求 (即使沒有新訊息也請求)
// - 延遲時間 (最多1秒)
// - 伺服器負載

// WebSocket (高效)
// 即時雙向通訊

const socket = io();

socket.on('new-message', (message) => {
// 立即收到新訊息!
});

// 優點:
// - 立即接收 (無延遲)
// - 伺服器推送
// - 高效

Q2. WebSocket總是更好嗎?

A:

// ✅ WebSocket適合的情況
1. 即時聊天
2. 即時通知
3. 協作工具 (協同編輯)
4. 即時遊戲
5. 股票資訊 (即時資料)
6. IoT裝置控制

// ❌ WebSocket不需要的情況
1. 一般網站 (部落格、新聞)
2. REST API (CRUD)
3. 靜態內容
4. 不需要即時更新

// HTTP更好的情況:
// - 簡單的請求-回應
// - 需要快取
// - RESTful設計

Q3. Socket.io vs 原生WebSocket?

A:

// 原生WebSocket
const ws = new WebSocket('ws://localhost:8080');
ws.send('Hello');

// 優點: 輕量、快速
// 缺點: 沒有額外功能、沒有降級方案

// Socket.io
const socket = io();
socket.emit('message', 'Hello');

// 優點:
// 1. 自動重連
// 2. Room/Namespace 支援
// 3. 降級方案 (WebSocket不可用時使用Polling)
// 4. 基於事件
// 5. 二進位支援

// 缺點:
// 1. 體積大 (函式庫大小)
// 2. 與原生WebSocket不相容

// 選擇標準:
// 簡單專案 → 原生WebSocket
// 複雜專案 → Socket.io

Q4. WebSocket安全性?

A:

// 1. 使用 wss:// (WebSocket Secure)
// 就像 HTTP → HTTPS
// ws:// → wss://

const socket = new WebSocket('wss://example.com'); // ✅ 加密
const socket = new WebSocket('ws://example.com'); // ❌ 明文

// 2. 認證
// Socket.io
io.use((socket, next) => {
const token = socket.handshake.auth.token;

if (isValidToken(token)) {
next();
} else {
next(new Error('認證失敗'));
}
});

// 客戶端
const socket = io({
auth: {
token: 'user-token'
}
});

// 3. 權限驗證
socket.on('delete-message', (messageId) => {
const userId = socket.userId;

if (canDeleteMessage(userId, messageId)) {
deleteMessage(messageId);
} else {
socket.emit('error', '沒有權限');
}
});

// 4. 速率限制
const rateLimit = require('socket.io-rate-limit');

io.use(rateLimit({
max: 10, // 最多10個
interval: 1000 // 每秒
}));

// 5. 輸入驗證
socket.on('message', (message) => {
// 防止XSS
const sanitized = sanitizeHtml(message);

if (sanitized.length > 1000) {
return socket.emit('error', '訊息太長');
}

broadcast(sanitized);
});

Q5. WebSocket連線管理?

A:

// 1. 自動重連 (Socket.io)
const socket = io({
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});

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

socket.on('reconnect', () => {
console.log('重連成功');
});

// 2. 心跳 (連線檢查)
// 伺服器
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 無回應時關閉
}

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

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

// 3. 記憶體管理
const MAX_CONNECTIONS = 1000;

wss.on('connection', (ws) => {
if (wss.clients.size > MAX_CONNECTIONS) {
ws.close(1008, '伺服器已滿');
return;
}

// 處理連線...
});

// 4. 優雅關閉
process.on('SIGTERM', () => {
console.log('伺服器正在關閉...');

// 向所有連線發送關閉通知
wss.clients.forEach((ws) => {
ws.send(JSON.stringify({ type: 'server-shutdown' }));
ws.close();
});

wss.close(() => {
console.log('WebSocket伺服器已關閉');
process.exit(0);
});
});

🎓 下一步

如果你理解了WebSocket,接下來可以學習:

  1. 什麼是Node.js? - 建構WebSocket伺服器
  2. 什麼是React? - 實現即時UI
  3. 什麼是Docker? (待建立文件) - 部署WebSocket伺服器

實踐練習

# 建立Socket.io聊天應用

# 1. 專案初始化
mkdir chat-app
cd chat-app
npm init -y

# 2. 安裝套件
npm install express socket.io

# 3. 建立伺服器 (參考上面的例子)
# server.js

# 4. 執行
node server.js

# 5. 存取 http://localhost:3000

🎬 總結

WebSocket是即時Web的核心技術:

  • 雙向通訊: 伺服器 ↔ 客戶端
  • 持久連線: 一次連線,持續使用
  • 即時: 即時資料傳輸
  • 高效: 比輪詢高效得多

如果需要即時功能,就使用WebSocket! 🔌✨