本文 AI 產出,尚未審核

JavaScript 課程 – Fetch 與網路請求(Networking)

主題:WebSocket 與即時通訊


簡介

在現代 Web 應用程式中,即時性已成為使用者體驗的關鍵要素。聊天系統、即時遊戲、即時儀表板、協作編輯等功能,都需要在瀏覽器與伺服器之間保持持續、低延遲的雙向通訊。傳統的 HTTP 請求(如 fetchXMLHttpRequest)是 一次性 的「請求—回應」模式,無法滿足這類需求。

WebSocket 正是為了解決這個問題而設計的。它在單一 TCP 連線上提供 全雙工(full‑duplex) 通訊,讓瀏覽器與伺服器可以在任意時刻互相傳送資料,而不必每次都重新建立連線。本文將從概念、實作、常見陷阱與最佳實踐,帶你一步一步掌握 WebSocket,並透過實務範例了解如何在 JavaScript 中運用即時通訊。


核心概念

1. WebSocket 的工作原理

  1. 握手(Handshake)
    • 客戶端先以 HTTP/1.1 發送 Upgrade: websocket 的請求,伺服器回應 101 Switching Protocols,完成協議切換。
  2. 建立持久連線
    • 握手成功後,TCP 連線升級為 WebSocket,雙方可以開始互相傳送 frames(資料框)。
  3. 全雙工通訊
    • 客戶端與伺服器皆可隨時 send 訊息,且不需要等待對方的回應。
  4. 關閉連線
    • 任一方傳送 Close frame,並等待對方回應後正式斷線。

小提醒:WebSocket 仍然是基於 TCP,因而繼承了 TCP 的可靠性與順序性,但不具備 HTTP 的「快取」與「代理」機制。

2. 建立 WebSocket 連線的基本語法

// 建立 WebSocket 物件,指定伺服器端點 (URL 必須以 ws:// 或 wss:// 開頭)
const socket = new WebSocket('wss://example.com/chat');

// 監聽開啟連線事件
socket.addEventListener('open', () => {
  console.log('🔗 連線已建立');
});

// 監聽訊息事件
socket.addEventListener('message', event => {
  console.log('📨 收到訊息:', event.data);
});

// 監聽關閉事件
socket.addEventListener('close', event => {
  console.log('❌ 連線已關閉', event.code, event.reason);
});

// 監聽錯誤事件
socket.addEventListener('error', err => {
  console.error('⚠️ WebSocket 錯誤', err);
});

wss:// 為加密版(相當於 HTTPS),在正式環境中強烈建議使用。

3. 資料傳送與接收

  • 文字訊息socket.send('Hello') 會以 UTF‑8 編碼傳送字串。
  • 二進位資料socket.send(blob)socket.send(arrayBuffer),瀏覽器會自動以 binary frame 發送。
  • 自訂協議:常見做法是使用 JSON 包裝訊息,便於在前端與後端解析。
// 以 JSON 格式傳送聊天訊息
function sendChatMessage(content) {
  const payload = {
    type: 'chat',
    timestamp: Date.now(),
    data: content,
  };
  socket.send(JSON.stringify(payload));
}

4. 心跳機制(Ping/Pong)與連線維護

WebSocket 本身支援 ping/pong 控制幀,但瀏覽器端無法直接發送 ping。常見做法是自行在應用層實作心跳訊息

let heartbeatInterval;

// 每 30 秒發送一次心跳
function startHeartbeat() {
  heartbeatInterval = setInterval(() => {
    socket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
  }, 30000);
}

// 收到伺服器回應後可重置計時
socket.addEventListener('message', event => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'heartbeat-ack') {
    console.log('✅ 心跳回應 OK');
  }
});

5. 重連機制

網路斷線、伺服器重啟等情況會導致 close 事件。為了提升使用者體驗,需在客戶端實作自動重連

let reconnectAttempts = 0;
const MAX_RETRIES = 5;

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

  ws.addEventListener('open', () => {
    console.log('🔗 重新連線成功');
    reconnectAttempts = 0; // 重置計數
    startHeartbeat();
  });

  ws.addEventListener('close', () => {
    if (reconnectAttempts < MAX_RETRIES) {
      const delay = Math.pow(2, reconnectAttempts) * 1000; // 指數退避
      console.warn(`❌ 連線中斷,${delay / 1000}s 後嘗試重連`);
      setTimeout(() => {
        reconnectAttempts++;
        connect();
      }, delay);
    } else {
      console.error('🚨 超過最大重連次數,請手動刷新頁面');
    }
  });

  // 其他事件處理...
  return ws;
}

// 初始連線
let socket = connect();

程式碼範例

以下提供 五個實用範例,從最簡單的文字聊天到結合二進位檔案傳輸與 UI 更新的完整示範。

範例 1:最基礎的文字回音(Echo)伺服器

說明:此範例用於驗證本機或測試伺服器的 WebSocket 是否正常工作。伺服器會把收到的訊息原封不動回傳。

// client.js
const socket = new WebSocket('wss://echo.websocket.org'); // 公開測試服務

socket.addEventListener('open', () => {
  console.log('🔗 已連線至 Echo 伺服器');
  socket.send('Hello WebSocket!'); // 發送測試訊息
});

socket.addEventListener('message', event => {
  console.log('📨 伺服器回傳:', event.data);
});

提示echo.websocket.org 已於 2023 年停止服務,請自行搭建簡易 Node.js echo 伺服器或使用其他測試端點。

範例 2:聊天室 UI(簡易版)

說明:使用 HTML + CSS + JavaScript 建立即時聊天介面,訊息以 JSON 包裝,並支援自動滾動。

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>簡易聊天室</title>
  <style>
    body { font-family:Arial, sans-serif; margin:20px; }
    #log { height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; }
    .msg { margin:5px 0; }
    .self { color:#0066cc; }
    .peer { color:#333; }
  </style>
</head>
<body>
  <div id="log"></div>
  <input id="input" type="text" placeholder="輸入訊息…" />
  <button id="sendBtn">發送</button>

  <script>
    const socket = new WebSocket('wss://example.com/chat');
    const log = document.getElementById('log');
    const input = document.getElementById('input');
    const sendBtn = document.getElementById('sendBtn');

    // 接收訊息
    socket.addEventListener('message', e => {
      const { type, data, sender } = JSON.parse(e.data);
      if (type === 'chat') {
        const div = document.createElement('div');
        div.className = 'msg ' + (sender === 'me' ? 'self' : 'peer');
        div.textContent = `${sender}: ${data}`;
        log.appendChild(div);
        log.scrollTop = log.scrollHeight; // 自動滾動
      }
    });

    // 發送訊息
    function send() {
      const text = input.value.trim();
      if (!text) return;
      const payload = { type: 'chat', data: text, sender: 'me' };
      socket.send(JSON.stringify(payload));
      input.value = '';
    }

    sendBtn.addEventListener('click', send);
    input.addEventListener('keypress', e => e.key === 'Enter' && send());
  </script>
</body>
</html>

範例 3:二進位檔案傳輸(圖片上傳)

說明:將使用者選取的圖片轉成 Blob,直接透過 WebSocket 傳送給伺服器,伺服器回傳成功訊息後顯示預覽。

<input type="file" id="fileInput" accept="image/*" />
<img id="preview" style="max-width:200px; margin-top:10px;" />

<script>
  const socket = new WebSocket('wss://example.com/upload');
  const fileInput = document.getElementById('fileInput');
  const preview = document.getElementById('preview');

  socket.addEventListener('open', () => console.log('🔗 上傳通道已建立'));

  socket.addEventListener('message', e => {
    const msg = JSON.parse(e.data);
    if (msg.type === 'upload-success') {
      preview.src = URL.createObjectURL(fileInput.files[0]); // 本地預覽
      alert('圖片已上傳成功!');
    }
  });

  fileInput.addEventListener('change', () => {
    const file = fileInput.files[0];
    if (!file) return;
    // 直接傳送 Blob
    socket.send(file);
  });
</script>

注意:伺服器端必須根據 Content-Type 或二進位長度自行判斷檔案類型,並妥善儲存。

範例 4:心跳與自動重連(完整範例)

說明:結合前面提到的心跳與指數退避重連機制,確保長時間連線的穩定性。

const WS_URL = 'wss://example.com/realtime';
let ws;
let heartbeatTimer;
let reconnectAttempts = 0;
const MAX_RETRY = 8;

// 建立連線
function initWebSocket() {
  ws = new WebSocket(WS_URL);

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

  ws.addEventListener('message', e => {
    const msg = JSON.parse(e.data);
    if (msg.type === 'heartbeat-ack') {
      console.log('✅ 心跳回應');
    } else {
      // 其他業務處理
      console.log('📨 收到訊息', msg);
    }
  });

  ws.addEventListener('close', () => {
    console.warn('❌ 連線關閉,嘗試重連...');
    stopHeartbeat();
    scheduleReconnect();
  });

  ws.addEventListener('error', err => {
    console.error('⚠️ WebSocket 錯誤', err);
    ws.close(); // 觸發 close 以統一處理重連
  });
}

// 心跳:每 20 秒發一次
function startHeartbeat() {
  heartbeatTimer = setInterval(() => {
    ws.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
  }, 20000);
}
function stopHeartbeat() {
  clearInterval(heartbeatTimer);
}

// 重連:指數退避
function scheduleReconnect() {
  if (reconnectAttempts >= MAX_RETRY) {
    console.error('🚨 已達最大重連次數,請手動刷新');
    return;
  }
  const delay = Math.pow(2, reconnectAttempts) * 1000; // 1,2,4,8...
  setTimeout(() => {
    reconnectAttempts++;
    console.log(`🔁 第 ${reconnectAttempts} 次重連...`);
    initWebSocket();
  }, delay);
}

// 初始化
initWebSocket();

範例 5:使用 WebSocket 建立「多人協作白板」的基礎架構

說明:此範例展示如何把使用者的繪圖指令(座標、顏色)以 JSON 廣播給所有連線的客戶端,實現即時同步。

<canvas id="board" width="600" height="400" style="border:1px solid #ccc;"></canvas>
<script>
  const canvas = document.getElementById('board');
  const ctx = canvas.getContext('2d');
  const socket = new WebSocket('wss://example.com/whiteboard');

  let drawing = false;

  canvas.addEventListener('mousedown', e => {
    drawing = true;
    const { offsetX, offsetY } = e;
    ctx.beginPath();
    ctx.moveTo(offsetX, offsetY);
    sendDraw({ type: 'start', x: offsetX, y: offsetY });
  });

  canvas.addEventListener('mousemove', e => {
    if (!drawing) return;
    const { offsetX, offsetY } = e;
    ctx.lineTo(offsetX, offsetY);
    ctx.stroke();
    sendDraw({ type: 'draw', x: offsetX, y: offsetY });
  });

  canvas.addEventListener('mouseup', () => {
    drawing = false;
    ctx.closePath();
    sendDraw({ type: 'end' });
  });

  // 發送繪圖指令
  function sendDraw(data) {
    socket.send(JSON.stringify({ action: 'draw', payload: data }));
  }

  // 接收其他使用者的指令
  socket.addEventListener('message', e => {
    const msg = JSON.parse(e.data);
    if (msg.action !== 'draw') return;
    const p = msg.payload;
    if (p.type === 'start') {
      ctx.beginPath();
      ctx.moveTo(p.x, p.y);
    } else if (p.type === 'draw') {
      ctx.lineTo(p.x, p.y);
      ctx.stroke();
    } else if (p.type === 'end') {
      ctx.closePath();
    }
  });
</script>

延伸:實務上會加入 使用者 ID顏色選擇撤銷/重做 等功能,並在伺服器端使用 房間(room) 機制分流。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記處理 close 事件 斷線後 UI 仍顯示「已連線」狀態,使用者無法得知失聯。 close 事件中更新 UI、停止心跳、啟動自動重連。
直接在 onmessage 中做大量運算 會阻塞 UI,造成卡頓。 使用 Web Worker 處理繁重計算或把資料拆分為小塊。
未設定 binaryType 取得二進位資料時會得到 BlobArrayBuffer,若預期不一致會出錯。 明確設定 socket.binaryType = 'arraybuffer'(或 'blob')。
心跳頻率過高 造成不必要的流量與伺服器負擔。 依需求選擇 15~30 秒的間隔,並在伺服器端設定合理的逾時時間。
直接把使用者輸入的字串送出 可能遭受 XSSJSON 注入 JSON.stringify,或在伺服器端進行 白名單驗證
忘記使用 wss:// 在非加密環境下,瀏覽器會阻止不安全的 WebSocket 連線。 於正式環境一定使用 TLS(wss://),開發時可自行簽發憑證測試。
未限制同時連線數 Bot 或惡意攻擊可能導致資源耗盡。 在伺服器層面加入 Rate LimitingIP 黑名單連線上限

最佳實踐

  1. 抽象化 WebSocket 客戶端:封裝成類別或模組,統一管理事件、心跳與重連。
  2. 使用 Promiseasync/await 包裝請求-回應模式:雖然 WebSocket 本質是事件驅動,但可自行實作 request-response 的簡易協議,提升程式可讀性。
  3. 記錄連線狀態:利用 navigator.connection 等 API 判斷網路類型,適時調整心跳頻率。
  4. *伺服器端使用 ping/pong*:大多數 WebSocket 框架(如 wssocket.io)已內建心跳,確保雙方都能偵測斷線。
  5. 安全考量:驗證來源(Origin)與子協議(Sec-WebSocket-Protocol)以防止跨站 WebSocket 攻擊(CSWS)。

實際應用場景

場景 為什麼適合使用 WebSocket 典型實作
即時聊天 / 訊息推播 需要雙向即時傳遞文字、表情、檔案。 socket.iows + JSON 協議
多人線上遊戲 高頻率、低延遲的座標與狀態同步。 二進位 ArrayBuffer + 客製化壓縮
即時儀表板 / 金融行情 大量資料持續推送,避免輪詢。 Server‑Sent Events(SSE)可作備援;WebSocket 為首選
協作編輯(文件、白板) 多使用者同時編輯,需要即時合併與衝突解決。 OT(Operational Transformation)或 CRDT + WebSocket
IoT 裝置監控 裝置端常保持長時間連線,伺服器推送指令。 MQTT over WebSocket 或直接使用 WebSocket
遠端控制台 / 終端機 需要即時回傳指令執行結果。 xterm.js + WebSocket 形成互動式終端機

案例分享:某電商平台在促銷期間使用 WebSocket 實作「秒殺倒數」與「即時庫存更新」功能,將 HTTP 輪詢的平均延遲從 2.4 秒降至 120 毫秒,同時減少伺服器的 70% 請求量。


總結

WebSocket 為現代 Web 提供了 全雙工、低延遲 的即時通訊管道,彌補了傳統 HTTP 請求的不足。掌握以下核心要點,即可在前端開發中自信地使用 WebSocket:

  1. 握手與協議升級:了解 Upgrade: websocket 的流程。
  2. 全雙工資料傳送:文字與二進位皆可直接 send
  3. 心跳與重連:實作指數退避與心跳機制,提升連線穩定性。
  4. 安全與效能:使用 wss://、驗證 Origin、限制連線數。
  5. 實務範例:從簡易聊天、檔案上傳、多人白板到大型即時遊戲,皆可套用相同的概念。

只要把 WebSocket 客戶端 抽象成可重用的模組,配合適當的協議設計(如 JSON、Protobuf),你就能在各種即時應用場景中快速構建高效、可靠的解決方案。祝你開發順利,打造出更多令人驚豔的即時互動體驗! 🚀