04-2. WebSocket vs HTTP 深度比較

理解何時該用 WebSocket,何時該用 HTTP

🔀 WebSocket vs HTTP 深度比較

🎯 核心差異

💡 比喻:

HTTP = 郵寄信件
- 你寄一封信(請求)
- 郵局回信(回應)
- 一問一答,不能主動通知

WebSocket = 電話通話
- 撥通後保持連線
- 雙方隨時可以說話
- 即時雙向溝通

連線模式對比

HTTP(請求-回應):

客戶端                      伺服器
  │                           │
  ├─ GET /data ──────────────>│
  │                           │
  │<─ 200 OK ──────────────────┤
  │  (data)                    │
  │                           │
  ├─ GET /data ──────────────>│  (需要新資料,再次請求)
  │                           │
  │<─ 200 OK ──────────────────┤
  │  (new data)                │

WebSocket(雙向通訊):

客戶端                      伺服器
  │                           │
  ├─ WebSocket Handshake ────>│  (一次連線)
  │<─ 101 Switching Protocols ┤
  │                           │
  │<══════ 保持連線 ═══════════>│
  │                           │
  ├─ Message ────────────────>│  (隨時發送)
  │<─ Message ──────────────────┤  (伺服器主動推送)
  ├─ Message ────────────────>│
  │<─ Message ──────────────────┤

📊 詳細比較表

特性HTTPWebSocket
連線短連線(一次性)長連線(持續)
方向單向(客戶端發起)雙向(任一方發起)
開銷每次請求都有 Header(~200+ bytes)握手後僅需 2-6 bytes
延遲高(需建立連線)低(連線已建立)
即時性需輪詢即時推送
瀏覽器支援✅ 所有瀏覽器✅ 現代瀏覽器
快取✅ 支援❌ 不支援
CDN✅ 支援❌ 不支援
防火牆✅ 友善⚠️ 可能被阻擋

🔄 HTTP 實現即時通訊的方法

1️⃣ 短輪詢(Short Polling)

💡 比喻:每隔幾秒問一次「有新訊息嗎?」
// 客戶端:每 3 秒請求一次
function shortPolling() {
    setInterval(async () => {
        const response = await fetch('/api/messages');
        const messages = await response.json();

        if (messages.length > 0) {
            displayMessages(messages);
        }
    }, 3000);  // 每 3 秒
}

shortPolling();

缺點:

  • ❌ 大量無用請求(沒有新資料也要請求)
  • ❌ 延遲高(最多 3 秒才知道有新訊息)
  • ❌ 伺服器負擔大

適用場景:

  • 更新頻率低(每分鐘)
  • 對即時性要求不高

2️⃣ 長輪詢(Long Polling)

💡 比喻:打電話問「有新訊息就告訴我」,沒有的話就一直等
// 客戶端
async function longPolling() {
    while (true) {
        try {
            const response = await fetch('/api/messages/poll');
            const messages = await response.json();

            displayMessages(messages);

            // 立即發起下一次輪詢
        } catch (error) {
            await new Promise(resolve => setTimeout(resolve, 3000));
        }
    }
}

longPolling();
# 伺服器(Flask)
from flask import Flask, jsonify
import queue
import time

app = Flask(__name__)
message_queues = {}

@app.route('/api/messages/poll')
def poll_messages():
    user_id = request.args.get('user_id')

    # 等待新訊息(最多 30 秒)
    timeout = 30
    start_time = time.time()

    while time.time() - start_time < timeout:
        if user_id in message_queues and not message_queues[user_id].empty():
            messages = []
            while not message_queues[user_id].empty():
                messages.append(message_queues[user_id].get())
            return jsonify(messages)

        time.sleep(0.5)  # 避免忙等

    # 超時,回應空陣列
    return jsonify([])

優點:

  • ✅ 比短輪詢即時
  • ✅ 減少無用請求

缺點:

  • ❌ 伺服器需維持大量連線
  • ❌ 斷線需重新連線

3️⃣ Server-Sent Events (SSE)

💡 比喻:訂閱電台廣播,伺服器持續推送
// 客戶端
const eventSource = new EventSource('/api/stream');

eventSource.onmessage = (event) => {
    const message = JSON.parse(event.data);
    displayMessage(message);
};

eventSource.onerror = (error) => {
    console.error('SSE error:', error);
    eventSource.close();
};
# 伺服器(Flask)
from flask import Flask, Response
import time
import json

app = Flask(__name__)

@app.route('/api/stream')
def stream():
    def generate():
        while True:
            # 產生事件
            data = {
                'message': 'Hello',
                'timestamp': time.time()
            }

            yield f"data: {json.dumps(data)}\n\n"
            time.sleep(1)

    return Response(generate(), mimetype='text/event-stream')

優點:

  • ✅ 伺服器主動推送
  • ✅ 自動重連
  • ✅ 簡單易用

缺點:

  • ❌ 單向(只能伺服器→客戶端)
  • ❌ 僅支援文字
  • ❌ 連線數限制(HTTP/1.1 每域名 6 個)

🆚 HTTP vs WebSocket 性能比較

頻寬消耗

HTTP(每次請求):

GET /api/data HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
Cookie: session=abc123
...
(Header 約 200-500 bytes)

資料:{"temp": 25}(13 bytes)
總共:213-513 bytes

WebSocket(握手後):

Frame Header: 2-6 bytes
資料:{"temp": 25}(13 bytes)
總共:15-19 bytes

節省:92-96% 的頻寬!

延遲比較

# 模擬測試
import time
import requests
from websocket import create_connection

# HTTP 輪詢
start = time.time()
for i in range(100):
    response = requests.get('http://example.com/api/data')
http_time = time.time() - start
print(f"HTTP 100 次請求:{http_time:.2f} 秒")

# WebSocket
ws = create_connection('ws://example.com/ws')
start = time.time()
for i in range(100):
    ws.send('{"action": "get_data"}')
    result = ws.recv()
ws_time = time.time() - start
print(f"WebSocket 100 次訊息:{ws_time:.2f} 秒")

# 結果範例:
# HTTP 100 次請求:5.23 秒
# WebSocket 100 次訊息:0.15 秒
# WebSocket 快 ~35 倍!

📋 選擇指南

使用 HTTP 的場景

✅ 適合 HTTP:

1. RESTful API(CRUD 操作):

// 取得使用者資料
GET /api/users/123

// 建立文章
POST /api/articles
{
  "title": "New Article",
  "content": "..."
}

// 更新設定
PUT /api/settings
{
  "theme": "dark"
}

2. 需要快取的資源:

// 圖片、CSS、JS 等靜態資源
GET /images/logo.png
Cache-Control: max-age=31536000

// API 資料快取
GET /api/products?category=electronics
Cache-Control: max-age=300

3. 無需即時性的資料:

// 文章列表(不需即時更新)
fetch('/api/articles')

// 使用者個人檔案
fetch('/api/profile')

// 歷史記錄
fetch('/api/orders/history')

4. SEO 需求:

<!-- 搜尋引擎可以爬取 HTTP 頁面 -->
<meta property="og:title" content="Article Title" />

使用 WebSocket 的場景

✅ 適合 WebSocket:

1. 即時聊天:

// 即時訊息推送
ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    displayMessage(message);
};

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

2. 即時通知:

// 訂單狀態更新
ws.onmessage = (event) => {
    const notification = JSON.parse(event.data);

    if (notification.type === 'order_shipped') {
        showNotification('您的訂單已出貨!');
    }
};

3. 多人協作:

// Google Docs 風格的協作編輯
ws.onmessage = (event) => {
    const update = JSON.parse(event.data);

    if (update.type === 'text_change') {
        applyTextChange(update.position, update.text);
    }

    if (update.type === 'cursor_move') {
        showOtherUserCursor(update.userId, update.position);
    }
};

4. 即時數據(股票、遊戲):

// 股票價格即時更新
ws.onmessage = (event) => {
    const priceUpdate = JSON.parse(event.data);
    updateStockPrice(priceUpdate.symbol, priceUpdate.price);
};

// 遊戲狀態同步
ws.onmessage = (event) => {
    const gameState = JSON.parse(event.data);
    updateGameState(gameState);
};

5. IoT 監控:

// 感測器數據即時顯示
ws.onmessage = (event) => {
    const sensorData = JSON.parse(event.data);
    updateDashboard(sensorData);
};

🔄 混合使用策略

很多應用會同時使用 HTTP 和 WebSocket:

class HybridApp {
    constructor() {
        this.ws = null;
        this.setupWebSocket();
    }

    // WebSocket:即時通訊
    setupWebSocket() {
        this.ws = new WebSocket('ws://example.com/ws');

        this.ws.onmessage = (event) => {
            const data = JSON.parse(event.data);

            if (data.type === 'notification') {
                this.handleNotification(data);
            } else if (data.type === 'chat') {
                this.handleChatMessage(data);
            }
        };

        this.ws.onerror = () => {
            // WebSocket 失敗,降級到 Long Polling
            this.fallbackToPolling();
        };
    }

    // HTTP:API 請求
    async loadUserProfile() {
        const response = await fetch('/api/user/profile');
        const profile = await response.json();
        this.displayProfile(profile);
    }

    async uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);

        await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });
    }

    // HTTP:靜態資源
    async loadArticles() {
        const response = await fetch('/api/articles');
        const articles = await response.json();
        this.renderArticles(articles);
    }

    // WebSocket:即時發送訊息
    sendChatMessage(message) {
        this.ws.send(JSON.stringify({
            type: 'chat',
            content: message
        }));
    }

    // 降級策略
    fallbackToPolling() {
        setInterval(async () => {
            const response = await fetch('/api/messages/poll');
            const messages = await response.json();
            messages.forEach(msg => this.handleChatMessage(msg));
        }, 3000);
    }
}

// 使用
const app = new HybridApp();

// HTTP:載入初始資料
app.loadUserProfile();
app.loadArticles();

// WebSocket:即時通訊
app.sendChatMessage('Hello!');

🎓 常見面試題

Q1:為什麼 WebSocket 比 HTTP 更適合即時通訊?

答案:

核心原因:

  1. 雙向通訊:
HTTP:
客戶端 → 伺服器(請求)
客戶端 ← 伺服器(回應)
伺服器「無法」主動推送

WebSocket:
客戶端 ↔ 伺服器
任一方都可以「主動」發送訊息
  1. 低延遲:
HTTP 輪詢:
- 每次都要建立 TCP 連線(三次握手)
- 發送完整 HTTP Header(200+ bytes)
- 延遲:~100-500ms

WebSocket:
- 連線已建立(無需握手)
- Header 僅 2-6 bytes
- 延遲:~1-10ms
  1. 低頻寬:
發送 100 次 "hello"(5 bytes)

HTTP:
每次請求:Header ~200 bytes + Data 5 bytes = 205 bytes
100 次:20,500 bytes

WebSocket:
握手:~200 bytes(一次)
每次訊息:Header 2 bytes + Data 5 bytes = 7 bytes
100 次:200 + 700 = 900 bytes

節省:95.6% 的頻寬!
  1. 伺服器負擔:
HTTP 輪詢(1000 個用戶,每 3 秒一次):
每秒請求數:1000 / 3 ≈ 333 req/s

WebSocket(1000 個用戶):
1000 個長連線(持續保持)
只有在有訊息時才處理

記憶口訣:「雙低低低」

  • 雙(雙向)
  • 低延遲
  • 低頻寬
  • 低負擔

Q2:WebSocket 有什麼缺點?

答案:

主要缺點:

1. 無法快取:

HTTP:
瀏覽器自動快取:GET /api/products
CDN 快取:圖片、CSS、JS

WebSocket:
訊息無法快取
每次都是「新鮮」資料

2. 負載平衡困難:

HTTP(無狀態):
請求 1 → Server A
請求 2 → Server B
請求 3 → Server A
✅ 簡單(隨機分配)

WebSocket(有狀態):
連線 1 → Server A(必須保持在 Server A)
如果 Server A 掛掉?
需要:Sticky Session 或狀態同步

3. 防火牆/代理問題:

企業防火牆可能阻擋 WebSocket
某些 Proxy 不支援 Upgrade 請求

解決:降級到 Long Polling

4. 連線管理複雜:

// 需要處理:
ws.onerror = () => {};    // 錯誤處理
ws.onclose = () => {};    // 斷線重連
setInterval(() => {       // 心跳檢測
    ws.send('ping');
}, 30000);

5. 除錯困難:

HTTP:
- Chrome DevTools 清楚顯示
- Postman 可以測試
- curl 可以模擬

WebSocket:
- 訊息是動態的
- 需要專門工具(wscat)
- 較難重現問題

6. SEO 不友善:

搜尋引擎爬蟲無法執行 JavaScript WebSocket
動態內容無法被索引

Q3:什麼時候應該使用 Long Polling 而不是 WebSocket?

答案:

使用 Long Polling 的場景:

1. 舊瀏覽器支援:

IE 9 及以下不支援 WebSocket
使用 Long Polling 作為後備方案

2. 防火牆限制:

某些企業防火牆阻擋 WebSocket
Long Polling 使用標準 HTTP,較不易被阻擋

3. 更新頻率低:

如果訊息每分鐘才有一次:
Long Polling 的開銷可以接受

如果每秒數百次更新:
WebSocket 更合適

4. 無需雙向通訊:

只需要伺服器→客戶端推送
不需要客戶端→伺服器即時發送

範例:新聞推送、股票報價
→ 可以用 Long Polling 或 SSE

5. 短期連線:

只需要監聽幾分鐘
建立 WebSocket 的成本不值得

範例:付款頁面等待交易完成

降級策略:

class RealTimeCommunication {
    constructor(url) {
        this.url = url;
        this.preferWebSocket = true;
        this.connect();
    }

    connect() {
        if (this.preferWebSocket && 'WebSocket' in window) {
            this.useWebSocket();
        } else {
            this.useLongPolling();
        }
    }

    useWebSocket() {
        this.ws = new WebSocket(this.url);

        this.ws.onerror = () => {
            console.log('WebSocket 失敗,降級到 Long Polling');
            this.useLongPolling();
        };

        this.ws.onmessage = (event) => {
            this.handleMessage(JSON.parse(event.data));
        };
    }

    async useLongPolling() {
        while (true) {
            try {
                const response = await fetch(this.url + '/poll', {
                    method: 'GET',
                    signal: AbortSignal.timeout(30000)  // 30 秒超時
                });

                const data = await response.json();
                if (data.messages) {
                    data.messages.forEach(msg => this.handleMessage(msg));
                }

            } catch (error) {
                await new Promise(resolve => setTimeout(resolve, 3000));
            }
        }
    }

    handleMessage(message) {
        console.log('收到訊息:', message);
    }
}

Q4:如何選擇 WebSocket 或 SSE?

答案:

SSE (Server-Sent Events) = 單向推送
WebSocket = 雙向通訊

比較:

特性SSEWebSocket
方向單向(伺服器→客戶端)雙向
資料格式文字(UTF-8)文字+二進位
協定HTTPWebSocket (ws://)
自動重連✅ 內建❌ 需手動實作
瀏覽器支援✅ 現代瀏覽器✅ 現代瀏覽器
複雜度簡單複雜

使用 SSE:

// 客戶端
const eventSource = new EventSource('/api/notifications');

eventSource.onmessage = (event) => {
    const notification = JSON.parse(event.data);
    showNotification(notification);
};

// 自動重連(內建)
eventSource.onerror = () => {
    console.log('連線中斷,自動重連...');
};
# 伺服器(Flask)
@app.route('/api/notifications')
def notifications():
    def generate():
        while True:
            # 等待新通知
            notification = get_next_notification()
            yield f"data: {json.dumps(notification)}\n\n"

    return Response(generate(), mimetype='text/event-stream')

選擇建議:

使用 SSE:
✅ 只需伺服器推送(通知、新聞、股票)
✅ 需要自動重連
✅ 簡單易用

使用 WebSocket:
✅ 需要雙向通訊(聊天、遊戲)
✅ 需要二進位資料(圖片、音訊)
✅ 低延遲要求高

Q5:HTTP/2 和 HTTP/3 對即時通訊有幫助嗎?

答案:

HTTP/2 改進:

1. 多路復用(Multiplexing):
HTTP/1.1:每個請求需要新連線(最多 6 個)
HTTP/2:單一連線可以同時處理多個請求

→ Long Polling 效率提升

2. Server Push:
伺服器可以主動推送資源
但不適合即時訊息(需要預知資源)
// HTTP/2 Server Push(不適合即時訊息)
// 只適合靜態資源預載
response.push('/style.css');
response.push('/script.js');

HTTP/3(QUIC)改進:

1. 基於 UDP:
TCP 的 Head-of-Line Blocking 問題解決
封包遺失不會阻塞整個連線

2. 更快的連線建立:
0-RTT(Zero Round Trip Time)
首次連線後,之後可以立即發送資料

3. 更好的行動網路支援:
網路切換(WiFi ↔ 4G)不會斷線

結論:

HTTP/2 和 HTTP/3 改進了 HTTP 的效能
但對於「真正即時」的應用,WebSocket 仍是首選

HTTP/2 SSE > HTTP/1.1 Long Polling
但
WebSocket > HTTP/2 SSE(對即時性要求極高的場景)

選擇總結:

即時性要求:
極高(<10ms):WebSocket
高(<100ms):WebSocket 或 HTTP/2 SSE
中(<1s):HTTP/2 SSE 或 Long Polling
低(>1s):HTTP 輪詢

📝 總結

核心要點:

  • HTTP:請求-回應,適合一般 API、靜態資源
  • WebSocket:雙向長連線,適合即時通訊

選擇原則:

需要即時性、雙向通訊?→ WebSocket
需要快取、SEO、簡單?→ HTTP
只需單向推送?→ SSE
瀏覽器相容性?→ Long Polling

最佳實踐:

  • 混合使用(HTTP 載入資料 + WebSocket 即時通訊)
  • 提供降級方案(WebSocket → Long Polling)
  • 根據場景選擇最適合的技術

記憶口訣:「雙即快,HTTP 簡穩廣」

  • WebSocket:雙(雙向)、即(即時)、快(快速)
  • HTTP:簡(簡單)、穩(穩定)、廣(廣泛支援)

🔗 延伸閱讀

0%