04-4. WebRTC:點對點即時通訊

深入理解 WebRTC 的 P2P 連線、媒體串流與實戰應用

📹 WebRTC:點對點即時通訊

⏱️ 閱讀時間: 18 分鐘 🎯 難度: ⭐⭐⭐⭐ (困難)


🏗️ WebRTC 在網路模型中的位置

OSI 7 層模型

┌──────────────────────────────┬─────────────────────────┐
│ 7. Application Layer (應用層) │  WebRTC API             │ ← WebRTC 在這裡
├──────────────────────────────┼─────────────────────────┤
│ 6. Presentation Layer (表示層)│  編碼/解碼 (VP8/H.264)   │
├──────────────────────────────┼─────────────────────────┤
│ 5. Session Layer (會話層)     │  信令 (SDP/SIP)         │
├──────────────────────────────┼─────────────────────────┤
│ 4. Transport Layer (傳輸層)   │  UDP (SRTP/SCTP)        │ ← 使用 UDP
├──────────────────────────────┼─────────────────────────┤
│ 3. Network Layer (網路層)     │  IP (ICE/STUN/TURN)     │
├──────────────────────────────┼─────────────────────────┤
│ 2. Data Link Layer (資料鏈結層)│  Ethernet               │
├──────────────────────────────┼─────────────────────────┤
│ 1. Physical Layer (實體層)    │  網路線、光纖            │
└──────────────────────────────┴─────────────────────────┘

TCP/IP 4 層模型

┌─────────────────────────────┬─────────────────────────┐
│ 4. Application Layer (應用層) │  WebRTC API             │ ← WebRTC 在這裡
│                             │  信令協定 (SDP)          │
├─────────────────────────────┼─────────────────────────┤
│ 3. Transport Layer (傳輸層)  │  UDP (主要)             │ ← 使用 UDP!
│                             │  SRTP (媒體)            │
│                             │  SCTP (資料)            │
├─────────────────────────────┼─────────────────────────┤
│ 2. Internet Layer (網際網路層)│  IP                     │
├─────────────────────────────┼─────────────────────────┤
│ 1. Network Access (網路存取層)│  Ethernet               │
└─────────────────────────────┴─────────────────────────┘

重點:

  • WebRTC 主要使用 UDP(低延遲優先)
  • 媒體用 SRTP(加密的 RTP)
  • 資料通道用 SCTP over UDP
  • 信令協定可自由選擇(WebSocket、HTTP 等)

🎯 什麼是 WebRTC?

一句話解釋

WebRTC = Web Real-Time Communication
       = 網頁即時通訊

💡 讓瀏覽器可以直接進行點對點(P2P)的
   視訊、語音、資料傳輸,無需插件!

生活化比喻

傳統視訊通話(需要伺服器轉發):
Alice → 伺服器 → Bob
就像透過郵局寄信,需要中轉

WebRTC(點對點):
Alice ↔ Bob
就像面對面交談,直接溝通

優勢:
✅ 延遲更低(無中轉)
✅ 頻寬成本更低(不經過伺服器)
✅ 隱私更好(資料不經過第三方)

🌟 WebRTC 的核心特性

1. 點對點連線 (P2P)

傳統架構:客戶端-伺服器
┌────────┐          ┌────────┐
│ Alice  │          │  Bob   │
└───┬────┘          └───┬────┘
    │                   │
    └───────┬───────────┘
            │
       ┌────▼────┐
       │ Server  │
       └─────────┘

問題:
❌ 伺服器頻寬成本高
❌ 延遲高(雙倍往返)
❌ 伺服器故障 = 服務中斷

WebRTC 架構:點對點
┌────────┐          ┌────────┐
│ Alice  │◄────────►│  Bob   │
└────────┘          └────────┘

優勢:
✅ 低延遲(直接連線)
✅ 低成本(無需中繼)
✅ 高隱私(端到端加密)

2. 無需插件

傳統方案:
- Flash(已淘汰)
- Java Applet(已淘汰)
- 桌面應用(需要安裝)

WebRTC:
- 純瀏覽器原生支援
- JavaScript API
- 跨平台(Windows、Mac、Linux、iOS、Android)

支援度:
Chrome:  ✅ 完整支援
Firefox: ✅ 完整支援
Safari:  ✅ 完整支援
Edge:    ✅ 完整支援

3. 內建安全性

WebRTC 強制加密:
┌─────────────────────────────┐
│ 媒體串流:SRTP (強制)         │
│ - 加密音訊/視訊              │
│ - 完整性檢查                 │
│                             │
│ 資料通道:DTLS (強制)         │
│ - 加密資料傳輸               │
│ - 憑證驗證                   │
│                             │
│ 信令:HTTPS (建議)           │
│ - 交換 SDP                  │
│ - ICE 候選者                │
└─────────────────────────────┘

無法禁用加密!

4. 自動適應網路狀況

WebRTC 內建:
✅ 自動調整位元率
✅ 自動調整解析度
✅ 丟包重傳 (NACK)
✅ 前向錯誤更正 (FEC)
✅ 抖動緩衝

網路變差時:
┌────────────────────────────┐
│ 1080p → 720p → 480p → 音訊  │
│ 自動降級保持通話            │
└────────────────────────────┘

🔧 WebRTC 架構

三大核心 API

┌────────────────────────────────────────┐
│           WebRTC JavaScript API        │
├────────────────────────────────────────┤
│                                        │
│  1. MediaStream (getUserMedia)         │
│     └─ 取得麥克風/攝影機               │
│                                        │
│  2. RTCPeerConnection                  │
│     └─ 建立 P2P 連線,傳輸媒體         │
│                                        │
│  3. RTCDataChannel                     │
│     └─ 傳輸任意資料(文字、檔案等)     │
│                                        │
└────────────────────────────────────────┘

📡 WebRTC 連線流程 ⭐⭐⭐

完整連線步驟

Alice                         信令伺服器                    Bob
  │                               │                          │
1.│ 取得本地媒體流                 │                          │
  │ (getUserMedia)                │                          │
  │                               │                          │
2.│ 創建 RTCPeerConnection        │                          │
  │                               │                          │
3.│ 創建 Offer (SDP)              │                          │
  ├─ Offer ─────────────────────>│                          │
  │                               ├─ Offer ──────────────────>│
  │                               │                          │
4.│                               │              創建 Answer │
  │                               │<──────────── Answer ─────┤
  │<──────────────────── Answer ──┤                          │
  │                               │                          │
5.│ 收集 ICE 候選者                │         收集 ICE 候選者   │
  ├─ ICE Candidate ──────────────>│                          │
  │                               ├─ ICE Candidate ──────────>│
  │                               │<──────── ICE Candidate ───┤
  │<────────── ICE Candidate ─────┤                          │
  │                               │                          │
6.│ ICE 協商(尋找最佳路徑)        │                          │
  │◄─────────────────────────────────────────────────────────►│
  │                  直接 P2P 連線!                           │
  │                               │                          │
7.│ 開始傳輸媒體/資料              │                          │
  │◄═════════════════════════════════════════════════════════►│
  │              SRTP (音訊/視訊)                              │
  │              SCTP (資料通道)                               │

詳細步驟說明

步驟 1:取得媒體流 (getUserMedia)

// 請求存取攝影機和麥克風
const localStream = await navigator.mediaDevices.getUserMedia({
    video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 }
    },
    audio: {
        echoCancellation: true,  // 回音消除
        noiseSuppression: true,  // 噪音抑制
        autoGainControl: true    // 自動增益
    }
});

// 顯示在本地 video 元素
document.getElementById('localVideo').srcObject = localStream;

步驟 2:創建 RTCPeerConnection

// ICE 伺服器設定
const configuration = {
    iceServers: [
        {
            urls: 'stun:stun.l.google.com:19302'  // Google STUN 伺服器
        },
        {
            urls: 'turn:turn.example.com:3478',   // TURN 伺服器
            username: 'user',
            credential: 'pass'
        }
    ]
};

// 創建連線
const peerConnection = new RTCPeerConnection(configuration);

// 加入本地媒體流
localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});

// 監聽遠端媒體流
peerConnection.ontrack = (event) => {
    document.getElementById('remoteVideo').srcObject = event.streams[0];
};

步驟 3-4:Offer/Answer 交換 (SDP)

// Alice (發起方) 創建 Offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

// 透過信令伺服器發送給 Bob
signalingChannel.send({
    type: 'offer',
    sdp: offer.sdp
});

// Bob (接收方) 收到 Offer
signalingChannel.on('offer', async (offer) => {
    await peerConnection.setRemoteDescription(offer);

    // 創建 Answer
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    // 發送回 Alice
    signalingChannel.send({
        type: 'answer',
        sdp: answer.sdp
    });
});

// Alice 收到 Answer
signalingChannel.on('answer', async (answer) => {
    await peerConnection.setRemoteDescription(answer);
});

步驟 5:ICE 候選者交換

// 監聽本地 ICE 候選者
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        // 發送給對方
        signalingChannel.send({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    }
};

// 接收遠端 ICE 候選者
signalingChannel.on('ice-candidate', async (candidate) => {
    await peerConnection.addIceCandidate(candidate);
});

🌐 NAT 穿透:ICE、STUN、TURN ⭐⭐⭐

問題:為什麼需要 NAT 穿透?

問題場景:

Alice 在家裡                          Bob 在公司
內網 IP: 192.168.1.100               內網 IP: 10.0.0.50
公網 IP: 203.0.113.10                公網 IP: 198.51.100.20
         (NAT 路由器)                        (公司防火牆)

Alice 無法直接連到 Bob 的內網 IP
Bob 也無法直接連到 Alice 的內網 IP

怎麼辦?

解決方案:ICE 框架

ICE (Interactive Connectivity Establishment)

ICE 框架整合三種技術:
┌─────────────────────────────────┐
│ 1. 直接連線 (最佳)               │
│    └─ 兩者在同一網路             │
│                                 │
│ 2. STUN (次佳)                  │
│    └─ 發現公網 IP,直接 P2P      │
│                                 │
│ 3. TURN (最後手段)               │
│    └─ 透過中繼伺服器轉發         │
└─────────────────────────────────┘

STUN:發現公網 IP

STUN (Session Traversal Utilities for NAT)

運作原理:

1. Alice 向 STUN 伺服器發送請求
   ┌────────┐          ┌──────────┐
   │ Alice  │─────────►│   STUN   │
   │ 內網IP │          │  Server  │
   └────────┘          └──────────┘
   192.168.1.100

2. STUN 回應 Alice 的公網 IP
   ┌────────┐          ┌──────────┐
   │ Alice  │◄─────────│   STUN   │
   └────────┘          └──────────┘
   「你的公網 IP 是 203.0.113.10:54321」

3. Alice 和 Bob 交換公網 IP

4. Alice 和 Bob 嘗試直接連線
   ┌────────┐          ┌────────┐
   │ Alice  │◄────────►│  Bob   │
   └────────┘          └────────┘
   203.0.113.10       198.51.100.20

成功率:

✅ 適用於:大多數家用路由器 (80-85%)
❌ 失敗於:對稱型 NAT、嚴格防火牆

免費 STUN 伺服器:

stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
stun:stun.stunprotocol.org:3478

TURN:中繼伺服器

TURN (Traversal Using Relays around NAT)

運作原理:
當 STUN 失敗時,透過 TURN 伺服器中繼

┌────────┐          ┌──────────┐          ┌────────┐
│ Alice  │─────────►│   TURN   │◄─────────│  Bob   │
└────────┘          │  Server  │          └────────┘
                    └──────────┘
                        中繼

流程:
1. Alice 連到 TURN 伺服器
2. Bob 連到 TURN 伺服器
3. TURN 轉發 Alice ↔ Bob 的所有資料

缺點:

❌ 頻寬成本(所有資料經過伺服器)
❌ 延遲增加
❌ 需要自己架設或付費使用

成本估算:
1000 同時通話
平均 1 Mbps 視訊
= 1000 Mbps = 1 Gbps 頻寬

何時需要 TURN?

✅ 企業防火牆(15-20% 情況)
✅ 對稱型 NAT
✅ 嚴格的網路限制

ICE 候選者類型

ICE 會收集三種類型的候選者:

1. Host Candidate (主機候選者)
   └─ 本機 IP:192.168.1.100
   └─ 最佳選項(同網路)

2. Server Reflexive Candidate (伺服器反射候選者)
   └─ 公網 IP:203.0.113.10:54321
   └─ 透過 STUN 發現
   └─ 次佳選項(STUN P2P)

3. Relay Candidate (中繼候選者)
   └─ TURN IP:198.51.100.50:3478
   └─ 最後手段(TURN 中繼)

ICE 選擇優先順序:
Host > Server Reflexive > Relay
(直接連線 > STUN P2P > TURN 中繼)

📊 SDP:會話描述協定

什麼是 SDP?

SDP = Session Description Protocol
    = 會話描述協定

用途:
- 描述媒體格式(編碼、解析度、位元率)
- 交換網路資訊(IP、Port)
- 協商能力(雙方都支援的格式)

SDP 範例

v=0
o=- 4611731400430051336 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS local_stream

m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 203.0.113.10
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:Fk8P
a=ice-pwd:kOlFOGRnxRLwZzK8Nr9k
a=ice-options:trickle
a=fingerprint:sha-256 A1:B2:C3:...
a=setup:actpass
a=mid:0
a=sendrecv
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1

m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102
c=IN IP4 203.0.113.10
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:Fk8P
a=ice-pwd:kOlFOGRnxRLwZzK8Nr9k
a=fingerprint:sha-256 A1:B2:C3:...
a=mid:1
a=sendrecv
a=rtcp-mux
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
a=fmtp:97 profile-level-id=42e01f

SDP 關鍵欄位解釋

v=0                    版本
o=                     發起者資訊
s=-                    會話名稱
t=0 0                  時間(0 = 永久)

m=audio 9 ...          媒體描述(音訊)
m=video 9 ...          媒體描述(視訊)

a=ice-ufrag:           ICE 使用者名稱
a=ice-pwd:             ICE 密碼
a=fingerprint:         DTLS 憑證指紋
a=rtpmap:111 opus      音訊編碼:Opus
a=rtpmap:96 VP8        視訊編碼:VP8

🎬 完整實作:視訊通話

信令伺服器 (Node.js + WebSocket)

// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Map();

wss.on('connection', (ws) => {
    let clientId;

    ws.on('message', (message) => {
        const data = JSON.parse(message);

        switch (data.type) {
            case 'register':
                // 註冊客戶端
                clientId = data.id;
                clients.set(clientId, ws);
                console.log(`Client ${clientId} registered`);
                break;

            case 'offer':
            case 'answer':
            case 'ice-candidate':
                // 轉發信令給目標客戶端
                const targetClient = clients.get(data.target);
                if (targetClient && targetClient.readyState === WebSocket.OPEN) {
                    targetClient.send(JSON.stringify({
                        type: data.type,
                        from: clientId,
                        payload: data.payload
                    }));
                }
                break;
        }
    });

    ws.on('close', () => {
        if (clientId) {
            clients.delete(clientId);
            console.log(`Client ${clientId} disconnected`);
        }
    });
});

console.log('Signaling server running on ws://localhost:8080');

客戶端 (HTML + JavaScript)

<!DOCTYPE html>
<html>
<head>
    <title>WebRTC Video Call</title>
    <style>
        video {
            width: 400px;
            height: 300px;
            border: 1px solid #ccc;
            margin: 10px;
        }
        #controls {
            margin: 20px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            font-size: 16px;
        }
    </style>
</head>
<body>
    <h1>WebRTC Video Call</h1>

    <div id="controls">
        <input type="text" id="clientId" placeholder="Your ID" />
        <input type="text" id="targetId" placeholder="Target ID" />
        <button onclick="register()">Register</button>
        <button onclick="startCall()">Start Call</button>
        <button onclick="hangUp()">Hang Up</button>
    </div>

    <div>
        <video id="localVideo" autoplay muted></video>
        <video id="remoteVideo" autoplay></video>
    </div>

    <script>
        let localStream;
        let peerConnection;
        let ws;
        let myId;
        let targetId;

        const configuration = {
            iceServers: [
                { urls: 'stun:stun.l.google.com:19302' },
                { urls: 'stun:stun1.l.google.com:19302' }
            ]
        };

        // 註冊到信令伺服器
        function register() {
            myId = document.getElementById('clientId').value;

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

            ws.onopen = () => {
                ws.send(JSON.stringify({
                    type: 'register',
                    id: myId
                }));
                console.log('Registered as:', myId);
            };

            ws.onmessage = async (event) => {
                const data = JSON.parse(event.data);
                await handleSignalingData(data);
            };
        }

        // 處理信令訊息
        async function handleSignalingData(data) {
            switch (data.type) {
                case 'offer':
                    await handleOffer(data.payload);
                    break;
                case 'answer':
                    await handleAnswer(data.payload);
                    break;
                case 'ice-candidate':
                    await handleIceCandidate(data.payload);
                    break;
            }
        }

        // 開始通話
        async function startCall() {
            targetId = document.getElementById('targetId').value;

            // 取得本地媒體流
            try {
                localStream = await navigator.mediaDevices.getUserMedia({
                    video: {
                        width: { ideal: 1280 },
                        height: { ideal: 720 }
                    },
                    audio: {
                        echoCancellation: true,
                        noiseSuppression: true
                    }
                });

                document.getElementById('localVideo').srcObject = localStream;

                // 創建 PeerConnection
                createPeerConnection();

                // 加入本地媒體流
                localStream.getTracks().forEach(track => {
                    peerConnection.addTrack(track, localStream);
                });

                // 創建並發送 Offer
                const offer = await peerConnection.createOffer();
                await peerConnection.setLocalDescription(offer);

                ws.send(JSON.stringify({
                    type: 'offer',
                    target: targetId,
                    payload: offer
                }));

                console.log('Offer sent to:', targetId);
            } catch (error) {
                console.error('Error starting call:', error);
            }
        }

        // 創建 PeerConnection
        function createPeerConnection() {
            peerConnection = new RTCPeerConnection(configuration);

            // ICE 候選者
            peerConnection.onicecandidate = (event) => {
                if (event.candidate) {
                    ws.send(JSON.stringify({
                        type: 'ice-candidate',
                        target: targetId,
                        payload: event.candidate
                    }));
                }
            };

            // 遠端媒體流
            peerConnection.ontrack = (event) => {
                document.getElementById('remoteVideo').srcObject = event.streams[0];
                console.log('Remote stream received');
            };

            // 連線狀態變化
            peerConnection.onconnectionstatechange = () => {
                console.log('Connection state:', peerConnection.connectionState);
            };

            // ICE 連線狀態
            peerConnection.oniceconnectionstatechange = () => {
                console.log('ICE state:', peerConnection.iceConnectionState);
            };
        }

        // 處理收到的 Offer
        async function handleOffer(offer) {
            console.log('Offer received');

            // 取得本地媒體流
            if (!localStream) {
                localStream = await navigator.mediaDevices.getUserMedia({
                    video: true,
                    audio: true
                });
                document.getElementById('localVideo').srcObject = localStream;
            }

            // 創建 PeerConnection
            createPeerConnection();

            // 加入本地媒體流
            localStream.getTracks().forEach(track => {
                peerConnection.addTrack(track, localStream);
            });

            // 設定遠端描述
            await peerConnection.setRemoteDescription(offer);

            // 創建並發送 Answer
            const answer = await peerConnection.createAnswer();
            await peerConnection.setLocalDescription(answer);

            ws.send(JSON.stringify({
                type: 'answer',
                target: targetId || offer.from,
                payload: answer
            }));

            console.log('Answer sent');
        }

        // 處理收到的 Answer
        async function handleAnswer(answer) {
            console.log('Answer received');
            await peerConnection.setRemoteDescription(answer);
        }

        // 處理 ICE 候選者
        async function handleIceCandidate(candidate) {
            console.log('ICE candidate received');
            await peerConnection.addIceCandidate(candidate);
        }

        // 掛斷
        function hangUp() {
            if (peerConnection) {
                peerConnection.close();
                peerConnection = null;
            }

            if (localStream) {
                localStream.getTracks().forEach(track => track.stop());
                localStream = null;
            }

            document.getElementById('localVideo').srcObject = null;
            document.getElementById('remoteVideo').srcObject = null;

            console.log('Call ended');
        }
    </script>
</body>
</html>

📤 資料通道 (Data Channel)

什麼是 Data Channel?

Data Channel = WebRTC 的 P2P 資料傳輸
             = 不只是音訊/視訊,還可以傳任意資料!

用途:
✅ 聊天訊息
✅ 檔案傳輸
✅ 遊戲狀態同步
✅ 協作編輯
✅ 螢幕分享的控制指令

Data Channel 特性

傳輸方式:
- 基於 SCTP over UDP
- 可靠或不可靠(可選)
- 有序或無序(可選)

效能:
- 低延遲(P2P 直連)
- 高頻寬(不經過伺服器)
- 雙向通訊

實作範例:檔案傳輸

// 創建 Data Channel
const dataChannel = peerConnection.createDataChannel('fileTransfer', {
    ordered: true,      // 保證順序
    maxRetransmits: 3   // 最多重傳 3 次
});

// 監聽 Data Channel
peerConnection.ondatachannel = (event) => {
    const channel = event.channel;

    channel.onmessage = (event) => {
        // 接收資料
        console.log('Received:', event.data);
    };

    channel.onopen = () => {
        console.log('Data channel opened');
    };

    channel.onclose = () => {
        console.log('Data channel closed');
    };
};

// 發送文字訊息
dataChannel.onopen = () => {
    dataChannel.send('Hello from WebRTC!');
};

// 發送檔案
async function sendFile(file) {
    const chunkSize = 16384; // 16KB chunks
    let offset = 0;

    // 先發送檔案資訊
    dataChannel.send(JSON.stringify({
        type: 'file-info',
        name: file.name,
        size: file.size,
        type: file.type
    }));

    // 分塊發送
    while (offset < file.size) {
        const chunk = file.slice(offset, offset + chunkSize);
        const arrayBuffer = await chunk.arrayBuffer();

        dataChannel.send(arrayBuffer);
        offset += chunkSize;

        // 進度更新
        const progress = (offset / file.size) * 100;
        console.log(`Progress: ${progress.toFixed(2)}%`);
    }

    // 發送完成訊號
    dataChannel.send(JSON.stringify({
        type: 'file-complete'
    }));
}

// 接收檔案
let receivedChunks = [];
let fileInfo = null;

dataChannel.onmessage = (event) => {
    if (typeof event.data === 'string') {
        // JSON 訊息
        const message = JSON.parse(event.data);

        if (message.type === 'file-info') {
            fileInfo = message;
            receivedChunks = [];
        } else if (message.type === 'file-complete') {
            // 組合檔案
            const blob = new Blob(receivedChunks, { type: fileInfo.type });
            const url = URL.createObjectURL(blob);

            // 下載檔案
            const a = document.createElement('a');
            a.href = url;
            a.download = fileInfo.name;
            a.click();
        }
    } else {
        // Binary data (檔案塊)
        receivedChunks.push(event.data);
    }
};

🎮 實戰應用場景

1. 視訊會議

應用:Zoom、Google Meet、Microsoft Teams

技術要點:
✅ 多人連線(SFU 架構)
✅ 螢幕分享
✅ 虛擬背景
✅ 自動降噪

架構:
┌────────┐     ┌────────┐     ┌────────┐
│ User 1 │────►│  SFU   │◄────│ User 2 │
└────────┘     │ Server │     └────────┘
               │        │
               │        │◄────┌────────┐
               └────────┘     │ User 3 │
                              └────────┘

SFU (Selective Forwarding Unit):
- 不混合音訊/視訊
- 只轉發串流
- 比 MCU 更高效

2. 線上遊戲

應用:多人即時對戰遊戲

優勢:
✅ 低延遲(<50ms)
✅ P2P 直連
✅ 減少伺服器負擔

使用 Data Channel:
- 遊戲狀態同步
- 玩家位置更新
- 遊戲事件

配置:
const dataChannel = peerConnection.createDataChannel('game', {
    ordered: false,        // 不保證順序(最新的最重要)
    maxRetransmits: 0      // 不重傳(丟包就算了)
});

3. 遠端協作

應用:Figma、Google Docs 即時協作

功能:
✅ 游標位置同步
✅ 即時編輯
✅ 語音/視訊討論
✅ 畫面分享

技術組合:
- WebRTC Data Channel: 游標、編輯操作
- WebRTC Media: 語音/視訊
- WebSocket: 持久化、多人同步

4. 檔案分享

應用:Firefox Send、ShareDrop

優勢:
✅ P2P 傳輸(不經伺服器)
✅ 高速(直連頻寬)
✅ 隱私(端到端加密)

實作要點:
- 大檔案分塊傳輸
- 進度顯示
- 斷點續傳(困難)

🎓 常見面試題

Q1:WebRTC 和 WebSocket 有什麼不同?

答案:

核心差異:

特性WebSocketWebRTC
連線方式客戶端 ↔ 伺服器點對點 (P2P)
傳輸層TCPUDP (主要)
用途雙向通訊即時音訊/視訊/資料
延遲較高(經過伺服器)極低(直連)
頻寬成本伺服器承擔P2P 分散
建立連線簡單複雜(ICE、STUN、TURN)
加密可選 (WSS)強制 (SRTP/DTLS)
瀏覽器支援99%+95%+

使用場景對比:

WebSocket 適合:
✅ 聊天室(訊息傳遞)
✅ 即時通知
✅ 股票行情
✅ 多人遊戲(非即時對戰)
✅ 協作編輯(文件同步)

範例:
Client 1 ───► Server ───► Client 2
         訊息     訊息

WebRTC 適合:
✅ 視訊通話
✅ 語音通話
✅ 螢幕分享
✅ 檔案傳輸(P2P)
✅ 即時遊戲(對戰)

範例:
Client 1 ◄───────────► Client 2
      直接 P2P 連線

組合使用:

實務上常組合使用:

視訊會議系統:
┌──────────────────────────────────┐
│ WebSocket: 信令 (Offer/Answer)    │
│ WebRTC: 音訊/視訊傳輸             │
└──────────────────────────────────┘

線上遊戲:
┌──────────────────────────────────┐
│ WebSocket: 遊戲邏輯、狀態同步     │
│ WebRTC: 語音聊天                  │
└──────────────────────────────────┘

記憶技巧:

WebSocket = 你 ↔ 郵局 ↔ 朋友
           (伺服器中轉)

WebRTC    = 你 ↔ 朋友
           (直接對話)

Q2:WebRTC 如何穿透 NAT?

答案:

NAT 穿透三步驟:

1. 收集候選者 (ICE Candidates)

本地收集三種 IP:

a) Host Candidate (本機 IP)
   192.168.1.100:54321
   └─ 適用:同一網路

b) Server Reflexive (公網 IP via STUN)
   203.0.113.10:54321
   └─ 適用:不同網路,NAT 支援

c) Relay (TURN 中繼)
   198.51.100.50:3478
   └─ 適用:所有情況(最後手段)

2. ICE 協商流程

Alice                    Bob
  │                       │
  ├─ 收集候選者            ├─ 收集候選者
  │  • Host               │  • Host
  │  • STUN               │  • STUN
  │  • TURN               │  • TURN
  │                       │
  ├─ 交換候選者 ─────────► │
  │ ◄───────── 交換候選者 ─┤
  │                       │
  ├─ 連線測試 ────────────► │
  │  (所有組合)            │
  │                       │
  └─ 選擇最佳路徑 ◄───────► │
     (優先級:Host > STUN > TURN)

3. 四種 NAT 類型

1. Full Cone NAT (完全圓錐)
   └─ 最寬鬆
   └─ STUN 成功率:100%

2. Restricted Cone NAT (限制圓錐)
   └─ 需要雙向通訊
   └─ STUN 成功率:90%

3. Port Restricted Cone NAT (端口限制圓錐)
   └─ 需要相同端口
   └─ STUN 成功率:80%

4. Symmetric NAT (對稱)
   └─ 最嚴格
   └─ STUN 成功率:0%
   └─ 必須用 TURN ❌

實際連線過程:

場景:Alice (對稱 NAT) ↔ Bob (普通 NAT)

步驟 1:Alice 和 Bob 都連到 STUN
┌─────┐      ┌──────┐      ┌─────┐
│Alice│─────►│ STUN │◄─────│ Bob │
└─────┘      └──────┘      └─────┘
  │              │             │
  │  「你的公網   │  「你的公網  │
  │   IP 是      │   IP 是     │
  │   203.0.1」  │   198.51.1」│

步驟 2:交換公網 IP (透過信令伺服器)
Alice ─► 信令伺服器 ─► Bob

步驟 3:嘗試直接連線
Alice ─X─► Bob  (失敗,Alice 是對稱 NAT)

步驟 4:降級到 TURN
┌─────┐      ┌──────┐      ┌─────┐
│Alice│◄────►│ TURN │◄────►│ Bob │
└─────┘      └──────┘      └─────┘
         中繼所有資料

成功率統計:

直接連線 (Host):     10%
STUN P2P:           70%
TURN 中繼:          20%
───────────────────────
總成功率:           100% ✅

Q3:WebRTC 的延遲有多低?

答案:

延遲組成:

總延遲 = 編碼 + 網路傳輸 + 解碼 + 抖動緩衝

1. 編碼延遲
   └─ 音訊 (Opus): 2.5-60ms
   └─ 視訊 (VP8/H.264): 10-50ms

2. 網路傳輸
   └─ P2P 直連: 10-100ms
   └─ TURN 中繼: 50-200ms

3. 解碼延遲
   └─ 音訊: 2.5-20ms
   └─ 視訊: 10-30ms

4. 抖動緩衝
   └─ 音訊: 20-150ms
   └─ 視訊: 0-100ms

典型端到端延遲:
音訊: 50-150ms
視訊: 100-300ms

與其他方案對比:

WebRTC (P2P):        100-300ms  ✅ 最低
WebRTC (TURN):       200-500ms
傳統 RTMP:          2000-5000ms
HLS:                10000-30000ms (10-30秒) ❌ 最高

實測範例:

場景 1:同城,光纖,P2P
總延遲:~100ms
├─ 網路: 30ms
├─ 編碼: 30ms
├─ 解碼: 20ms
└─ 緩衝: 20ms

場景 2:跨國,4G,TURN
總延遲:~500ms
├─ 網路: 250ms
├─ 編碼: 50ms
├─ 解碼: 50ms
└─ 緩衝: 150ms

場景 3:惡劣網路
總延遲:~1000ms
├─ 網路: 500ms (高延遲)
├─ 編碼: 100ms
├─ 解碼: 100ms
└─ 緩衝: 300ms (大量抖動)

如何優化延遲?

// 1. 使用低延遲編碼器
const sender = peerConnection.getSenders()[0];
const parameters = sender.getParameters();
parameters.encodings[0].maxBitrate = 500000; // 限制位元率
sender.setParameters(parameters);

// 2. 禁用抖動緩衝(極端情況)
const receiver = peerConnection.getReceivers()[0];
receiver.playoutDelayHint = 0; // 最小緩衝

// 3. 使用 Opus 音訊低延遲模式
const constraints = {
    audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
        latency: 0.01 // 10ms 延遲
    }
};

Q4:如何處理網路不穩定?

答案:

WebRTC 內建機制:

1. 自動位元率調整 (ABR)

網路狀況監測:
┌──────────────────────────────┐
│ RTT (往返時間)                │
│ 丟包率                        │
│ 抖動                          │
│ 可用頻寬                      │
└──────────────────────────────┘
         │
         ▼
    自動調整位元率
         │
         ▼
┌──────────────────────────────┐
│ 好:1080p @ 2Mbps            │
│ 中:720p @ 1Mbps             │
│ 差:480p @ 500Kbps           │
│ 極差:音訊 only              │
└──────────────────────────────┘

2. 前向錯誤更正 (FEC)

發送方:
原始封包: [1][2][3][4]
         ↓
加入 FEC: [1][2][3][4][FEC(1-4)]
         ↓
發送出去

接收方:
收到: [1][X][3][4][FEC(1-4)]
      ↓
用 FEC 恢復封包 2 ✅

3. NACK (重傳請求)

接收方:
收到: [1][2][X][4][5]
      ↓
發送 NACK: 「請重傳封包 3」
      ↓
收到重傳: [3]
      ↓
完整: [1][2][3][4][5] ✅

4. 抖動緩衝 (Jitter Buffer)

網路抖動:
┌──┬─────┬──┬───┬────┬──┐
│  │     │  │   │    │  │  不穩定到達
└──┴─────┴──┴───┴────┴──┘

經過抖動緩衝:
┌──┬──┬──┬──┬──┬──┐
│  │  │  │  │  │  │  均勻播放
└──┴──┴──┴──┴──┴──┘

監控網路狀況:

// 獲取連線統計
async function getStats() {
    const stats = await peerConnection.getStats();

    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            console.log('Bitrate:', report.bytesReceived);
            console.log('Packet Loss:', report.packetsLost);
            console.log('Jitter:', report.jitter);
        }

        if (report.type === 'candidate-pair' && report.state === 'succeeded') {
            console.log('RTT:', report.currentRoundTripTime);
            console.log('Available Bandwidth:', report.availableOutgoingBitrate);
        }
    });
}

// 每秒更新一次
setInterval(getStats, 1000);

手動處理策略:

// 監聽網路品質
let qualityLevel = 'high';

peerConnection.getStats().then(stats => {
    stats.forEach(report => {
        if (report.type === 'inbound-rtp') {
            const packetLoss = report.packetsLost / report.packetsReceived;

            if (packetLoss > 0.1) {
                qualityLevel = 'low';
                // 降低解析度
                adjustVideoQuality('low');
            } else if (packetLoss > 0.05) {
                qualityLevel = 'medium';
                adjustVideoQuality('medium');
            } else {
                qualityLevel = 'high';
                adjustVideoQuality('high');
            }
        }
    });
});

function adjustVideoQuality(quality) {
    const sender = peerConnection.getSenders()
        .find(s => s.track && s.track.kind === 'video');

    const parameters = sender.getParameters();

    switch (quality) {
        case 'high':
            parameters.encodings[0].maxBitrate = 2000000; // 2Mbps
            break;
        case 'medium':
            parameters.encodings[0].maxBitrate = 1000000; // 1Mbps
            break;
        case 'low':
            parameters.encodings[0].maxBitrate = 500000;  // 500Kbps
            break;
    }

    sender.setParameters(parameters);
}

Q5:WebRTC 的安全性如何?

答案:

強制加密:

WebRTC 的三層加密:

1. 媒體加密:SRTP (Secure RTP)
   ┌──────────────────────────┐
   │ 音訊/視訊串流             │
   │ AES 加密                 │
   │ 完整性檢查 (HMAC)        │
   └──────────────────────────┘

2. 資料通道:DTLS (Datagram TLS)
   ┌──────────────────────────┐
   │ Data Channel 資料        │
   │ TLS 1.2+ 加密            │
   │ 憑證驗證                 │
   └──────────────────────────┘

3. 信令:HTTPS/WSS (建議)
   ┌──────────────────────────┐
   │ Offer/Answer/ICE         │
   │ TLS 加密                 │
   └──────────────────────────┘

無法禁用加密:

WebRTC 設計原則:
❌ 沒有「禁用加密」選項
✅ 所有連線都是加密的
✅ 使用最新加密標準

DTLS 握手:

Alice                          Bob
  │                             │
  ├─ DTLS ClientHello ─────────►│
  │◄───────────── ServerHello ──┤
  │◄────────────── Certificate ─┤
  │◄────────── ServerHelloDone ─┤
  │                             │
  ├─ ClientKeyExchange ────────►│
  ├─ ChangeCipherSpec ─────────►│
  │◄──────────── ChangeCipherSpec│
  │                             │
  └─ 加密連線建立 ✅            │

指紋驗證:

SDP 中包含憑證指紋:
a=fingerprint:sha-256 A1:B2:C3:D4:...

作用:
1. 防止中間人攻擊
2. 驗證對方身份
3. 確保連線到正確的端點

流程:
Alice 生成憑證
  ↓
計算 SHA-256 指紋
  ↓
透過信令發送給 Bob
  ↓
Bob 驗證 DTLS 憑證指紋
  ↓
指紋匹配 = 連線成功 ✅
指紋不符 = 拒絕連線 ❌

潛在風險:

✅ P2P 連線已加密
✅ 媒體無法被竊聽

⚠️ 但是:
❌ IP 位址暴露(ICE 候選者)
   └─ 解決:使用 TURN only 模式

❌ 信令可能不安全
   └─ 解決:強制 HTTPS/WSS

❌ 中間人攻擊(如果信令不安全)
   └─ 解決:端到端驗證指紋

最佳實踐:

// 1. 使用 HTTPS 信令
const ws = new WebSocket('wss://secure-server.com');

// 2. 驗證指紋
const offer = await peerConnection.createOffer();
const fingerprint = extractFingerprint(offer.sdp);

// 透過其他安全通道驗證(如 QR code、簡訊)
if (userConfirmFingerprint(fingerprint)) {
    await peerConnection.setLocalDescription(offer);
}

// 3. 限制 IP 暴露(隱私模式)
const config = {
    iceServers: [
        { urls: 'turn:turn.example.com', // 只用 TURN
          username: 'user',
          credential: 'pass'
        }
    ],
    iceTransportPolicy: 'relay' // 強制使用中繼
};

// 4. 監控安全狀態
peerConnection.onstatechange = () => {
    const state = peerConnection.connectionState;
    if (state === 'connected') {
        console.log('✅ 安全連線已建立');
    } else if (state === 'failed') {
        console.log('❌ 連線失敗,可能有安全問題');
    }
};

📝 總結

WebRTC 核心要點:

基本概念:

  • WebRTC:Web Real-Time Communication(網頁即時通訊)
  • P2P:點對點直接連線(無需伺服器中轉)
  • 用途:視訊、音訊、資料傳輸

三大 API:

1. getUserMedia: 取得攝影機/麥克風
2. RTCPeerConnection: P2P 連線
3. RTCDataChannel: 傳輸任意資料

連線流程:

1. 取得媒體流
2. 創建 PeerConnection
3. Offer/Answer 交換 (SDP)
4. ICE 候選者交換
5. P2P 連線建立
6. 媒體/資料傳輸

NAT 穿透:

ICE = STUN + TURN
- STUN: 發現公網 IP (80% 成功)
- TURN: 中繼伺服器 (20% 需要)

效能特點:

延遲: 100-300ms (極低)
傳輸: UDP (低延遲優先)
加密: 強制 SRTP/DTLS
品質: 自動調整

記憶口訣:

WebRTC = Web + Real-Time + Communication
       = 瀏覽器 + 即時 + 通訊

核心:
P2P(點對點)
低延遲(<300ms)
強制加密(SRTP/DTLS)
無需插件(原生 API)

🔗 延伸閱讀

  • 上一篇:04-3. WebSocket 實戰應用
  • 下一篇:05-1. 資料庫協定
  • WebRTC 官方:https://webrtc.org
  • MDN WebRTC API:https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
  • WebRTC Samples:https://webrtc.github.io/samples/
  • STUN/TURN 伺服器列表:https://gist.github.com/mondain/b0ec1cf5f60ae726202e
  • RFC 8825 (WebRTC Overview):https://datatracker.ietf.org/doc/html/rfc8825
0%