WebRTC 生產實戰:信令架構、NAT穿透、SFU架構與10個常見陷阱
网络协议
那場掉線40%使用者的通話故障
「視訊通話全掛了。」儀表板顯示40%的連線失敗。4G使用者、企業防火牆後面的使用者、CGNAT地區的使用者——全部連不上。我們的單台TURN伺服器達到了瓶頸:10000個併發中繼連線,每個2 Mbps,頂著20 Gbps的流量。核心在悄悄丟棄UDP封包,因為接收緩衝區滿了。WebRTC的demo在localhost沒問題,但生產環境需要完全不同的認知。
我部署過四個WebRTC生產系統——從處理5000併發問診的遠距醫療平台,到支撐50000觀眾的直播服務。這是一份從零到一的實戰筆記。
核心概念速查
| 概念 | 角色 | 關鍵機制 | 生產影響 |
|---|---|---|---|
| 信令 | 在媒體傳輸前交換會話元資料 | WebSocket/SSE/HTTP長輪詢 | 重連、訊息順序、房間狀態 |
| ICE | 互動式連線建立 | STUN + TURN 候選位址 | 85%直連P2P,15%需要中繼 |
| STUN | 穿透NAT發現公網IP:Port | 查詢公網STUN伺服器 | 對對稱型NAT無效 |
| TURN | 直連失敗時的媒體中繼 | 專用中繼伺服器 | 頻寬消耗大,每流約500 kbps |
| SDP | 媒體能力協商 | 編解碼器、格式、ICE候選位址 | Offer/Answer模型 |
| SFU | 選擇性轉發單元 | 路由流而不解碼 | 多人通話頻寬高效 |
信令架構:看不見的骨架
信令是WebRTC最被低估的部分。規範故意不定義它,意味著每個團隊都自己實作——多數都搞砸了。
生產信令伺服器(Node.js + Socket.IO)
// server/signaling.ts
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { Redis } from "ioredis";
const io = new Server(3000, {
pingInterval: 10000,
pingTimeout: 5000,
connectTimeout: 10000,
});
// Redis 配接器支援水平擴展
const pubClient = new Redis({ host: "redis" });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
io.on("connection", (socket) => {
socket.on("join-room", async ({ roomId, userId }) => {
// 檢查房間容量
const count = await redis.scard(`room:${roomId}:participants`);
if (count >= 12) {
socket.emit("error", { message: "房間已滿" });
return;
}
socket.join(roomId);
await redis.sadd(`room:${roomId}:participants`, userId);
// 通知其他參與者
socket.to(roomId).emit("participant-joined", { userId });
// 發送現有參與者列表
const participants = await redis.smembers(`room:${roomId}:participants`);
socket.emit("room-state", { participants });
});
// 轉發信令訊息
socket.on("offer", ({ to, sdp }) => {
io.to(to).emit("offer", { from: socket.id, sdp });
});
socket.on("answer", ({ to, sdp }) => {
io.to(to).emit("answer", { from: socket.id, sdp });
});
socket.on("ice-candidate", ({ to, candidate }) => {
io.to(to).emit("ice-candidate", { from: socket.id, candidate });
});
socket.on("disconnect", async () => {
const rooms = Array.from(socket.rooms);
for (const roomId of rooms) {
if (roomId !== socket.id) {
await redis.srem(`room:${roomId}:participants`, socket.id);
socket.to(roomId).emit("participant-left", { userId: socket.id });
}
}
});
});
信令坑點
- 訊息順序很重要:如果ICE候選在SDP answer之前到達,瀏覽器會忽略它。需要實作緩衝。
- 重連:Socket.IO處理傳輸層重連,但你需要應用層的信令狀態恢復。把待處理操作存在Redis裡。
- 過期狀態:參與者異常斷連而不走正常離開流程,成員記錄會殘留。用TTL過期的Redis key管理房間成員。
NAT穿透:殘酷的現實
NAT為什麼阻斷P2P
沒NAT:
主機A (1.2.3.4:50000) ←──────────→ 主機B (5.6.7.8:60000)
直接UDP正常工作。
有NAT:
主機A (192.168.1.5:50000) ──NAT──▶ 公網 (1.2.3.4:12345)
主機B (10.0.0.3:60000) ──NAT──▶ 公網 (5.6.7.8:54321)
A發包到5.6.7.8:54321 → NAT B丟棄(沒有映射記錄)
NAT只允許來自內網主機最近聯繫過的位址的入站包。
ICE過程逐步解析
1. 收集候選位址
└─ Host: 192.168.1.5:50000(本地,基本不可用)
└─ SRFLX(STUN): 1.2.3.4:12345(反射位址)
└─ RELAY(TURN): turn.example.com:3478(中繼,永遠可用)
2. 按優先級排序並交換
3. 連通性檢查
└─ 逐對嘗試候選位址
└─ 第一個成功的對獲選
4. 提名 → 媒體開始傳輸
NAT類型與其影響
| NAT類型 | P2P可能? | 需要TURN? | 普遍性 |
|---|---|---|---|
| 全錐形 | 是 | 否 | 少見 |
| 受限錐形 | 是 | 否 | 常見 |
| 埠受限錐形 | 是 | 否 | 非常常見 |
| 對稱NAT | 否 | 是 | 企業、行動、CGNAT |
實踐中,10-15%的P2P連線需要TURN中繼。行動網路使用者跳到25-30%。
TURN伺服器設定(coturn)
# /etc/coturn/turnserver.conf
listening-port=3478
tls-listening-port=5349
relay-ip=10.0.1.5
external-ip=203.0.113.5/10.0.1.5
lt-cred-mech
user=myapp:myapp_turn_secret
realm=myapp.com
max-bps=2000000
no-multicast-peers
no-loopback-peers
cert=/etc/letsencrypt/live/turn.myapp.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.myapp.com/privkey.pem
prometheus
SFU架構:超越P2P
超過3個人,P2P網格就崩潰了:
P2P Mesh(4人間): 6條連線,每人上傳3倍碼率
P2P Mesh(8人間): 28條連線,每人上傳7倍碼率
SFU: 8人 × 1條連線 = 8條連線
每人上傳一次,SFU轉發給其他7人
上傳:1倍碼率。下載:7倍碼率
mediasoup SFU實作
// server/sfu.ts
import * as mediasoup from "mediasoup";
const worker = await mediasoup.createWorker({
logLevel: "warn",
rtcMinPort: 40000,
rtcMaxPort: 49999,
});
const router = await worker.createRouter({
mediaCodecs: [
{
kind: "video", mimeType: "video/VP8", clockRate: 90000,
parameters: { "x-google-start-bitrate": 1000 },
},
{
kind: "video", mimeType: "video/H264", clockRate: 90000,
parameters: { "packetization-mode": 1, "profile-level-id": "42e01f" },
},
{ kind: "audio", mimeType: "audio/opus", clockRate: 48000, channels: 2 },
],
});
async function handleNewParticipant(transport, roomId) {
const producer = await transport.produce({
kind: "video",
rtpParameters: clientRtpParameters,
encodings: [
{ rid: "h", maxBitrate: 1500000, scalabilityMode: "L1T3" },
{ rid: "m", maxBitrate: 500000, scalabilityMode: "L1T3" },
{ rid: "l", maxBitrate: 150000, scalabilityMode: "L1T3" },
],
});
}
Simulcast:一條串流三種畫質
const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
});
const transceiver = pc.addTransceiver(stream.getVideoTracks()[0], {
sendEncodings: [
{ rid: "h", maxBitrate: 1500_000, scaleResolutionDownBy: 1 },
{ rid: "m", maxBitrate: 500_000, scaleResolutionDownBy: 2 },
{ rid: "l", maxBitrate: 150_000, scaleResolutionDownBy: 4 },
],
});
監控WebRTC統計
setInterval(async () => {
const stats = await pc.getStats();
for (const [_, report] of stats) {
if (report.type === "remote-inbound-rtp" && report.kind === "video") {
console.log({
packetLoss: report.fractionLost * 100 + "%",
jitter: report.jitter * 1000 + "ms",
});
}
}
}, 2000);
10個常見生產陷阱
陷阱1:沒有TURN伺服器
10-15%的使用者會連不上。行動端25-30%。從第一天就為TURN頻寬做預算。
陷阱2:單台TURN — 單點故障。至少2台。
陷阱3:忽略ICE候選滴漏
每發現一個就發送,別等所有都就緒。
陷阱4:getUserMedia不加約束
記得指定合理的解析度、幀率。
陷阱5:不處理getUserMedia錯誤
權限拒絕、設備未找到等都需友善提示。
陷阱6:通話中網路切換 — 實作ICE重啟。
陷阱7:瀏覽器相容性
| 瀏覽器 | 問題 | 方案 |
|---|---|---|
| Chrome | VP9 simulcast丟層 | 用VP8/H264 |
| Firefox | H264不支援simulcast | 用VP8 |
| Safari | getUserMedia需使用者手勢 | 放在click裡 |
| Chrome Android | 部分設備限制720p | 顯式請求720p |
陷阱8:不監控統計
陷阱9:碼率過高 — 限制maxBitrate
陷阱10:忘記清理 — 正確關閉PeerConnection
監控儀表板指標
| 指標 | 健康範圍 | 告警閾值 |
|---|---|---|
| ICE連線時間 | 1-3秒 | > 10秒 |
| 丟包率 | 0-2% | > 5% |
| 抖動 | 0-30ms | > 50ms |
| RTT | 20-80ms | > 300ms |
| 幀率 | 25-30 | < 15 |
| TURN使用率 | 10-20% | > 30% |
總結:WebRTC生產部署是20%媒體和80%基礎設施。三條最致命的問題:不部署TURN、不處理網路切換、所有東西都跑單機。從第一天就建監控。給TURN頻寬做預算,在真實行動網路上測試。
線上工具
- JSON 格式化 — 除錯信令訊息載荷和SDP會話描述
- Base64 編解碼 — 處理base64編碼的ICE候選位址和憑證
- 雜湊計算器 — 為長期認證生成TURN憑證雜湊
本站提供瀏覽器本地工具,免註冊即可試用 →
#WebRTC#实时通信#STUN#TURN#SFU#音视频#2026