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:3478TURN:中繼伺服器
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=42e01fSDP 關鍵欄位解釋
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 有什麼不同?
答案:
核心差異:
| 特性 | WebSocket | WebRTC |
|---|---|---|
| 連線方式 | 客戶端 ↔ 伺服器 | 點對點 (P2P) |
| 傳輸層 | TCP | UDP (主要) |
| 用途 | 雙向通訊 | 即時音訊/視訊/資料 |
| 延遲 | 較高(經過伺服器) | 極低(直連) |
| 頻寬成本 | 伺服器承擔 | 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