WebRTC 實戰 (4) —— ICE Candidate 與 NAT 穿越
如果你跟著前三篇實作,你可能會發現:在同一個 Wi-Fi 下連線沒問題,但一旦一台用 4G,一台用公司 Wi-Fi,連線就建立不起來。
這是因為我們還沒解決網路世界中最頑固的障礙:NAT (網路位址轉換) 與 防火牆。
一、 根本問題:為什麼 P2P 很難?
在現代網路中,大部分的設備都沒有公網 IP。你的設備可能處於一個私有網路中 (如 192.168.x.x)。當 A 想連線給 B 時,A 只知道 B 的私有 IP,但這在公網上是無法路由的。
二、 救援三劍客:STUN, TURN, ICE
1. STUN (Session Traversal Utilities for NAT)
功能:協助 ICE Agent 取得 Server Reflexive Candidate,反映 NAT 對外映射的位址與連接埠。媒體流量通常不經過 STUN 伺服器。
2. TURN (Traversal Using Relays around NAT)
功能:當直接路徑無法建立時,提供 Relay Candidate 中繼媒體流量。TURN 能顯著提高連通率,但仍可能受企業防火牆、傳輸協定、憑證或伺服器狀態影響,不能宣稱保證 100% 連通。
3. ICE (Interactive Connectivity Establishment)
功能:負責收集所有的連線可能性(稱為 ICE Candidates),並挑選出一條最優的路徑。
三、 前端實作:處理 ICE 事件
你需要在 RTCPeerConnection 建立後,立即監聽 onicecandidate 事件:
/* 前端代碼 */
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "turn:your-turn-server.com", username: "u", credential: "p" },
],
});
pc.onicecandidate = (event) => {
if (event.candidate) {
signaling.send({
type: "candidate",
from: "User_A",
to: "Target_User",
payload: event.candidate.toJSON(),
});
}
};
const pendingCandidates = [];
signaling.onMessage = async (msg) => {
if (msg.type === "candidate") {
if (pc.remoteDescription) {
await pc.addIceCandidate(msg.payload);
} else {
pendingCandidates.push(msg.payload);
}
}
};
async function flushPendingCandidates() {
while (pendingCandidates.length > 0) {
await pc.addIceCandidate(pendingCandidates.shift());
}
}ICE Candidate 有可能比 Offer/Answer 更早抵達。應先暫存 Candidate,等 setRemoteDescription() 成功後再呼叫 flushPendingCandidates(),避免 addIceCandidate() 因尚未存在遠端描述而失敗。
WARNING
範例中的 TURN 帳密只用於說明格式。正式環境不應把長期憑證寫死在前端,應由後端簽發短效 TURN 憑證,並提供 UDP、TCP 與 TLS 等可用路徑。
總結
今天我們解開了 WebRTC 連線最痛苦的一環:NAT 穿透與 ICE Candidate 交換邏輯。至此,你已經完整掌握了底層原理實戰的所有連線流程。
下一篇,我們將介紹一個常被忽略的強大武器:Data Channel。
進階挑戰
打開 Chrome 瀏覽器並輸入 chrome://webrtc-internals/。在進行 WebRTC 連線時觀察 IceCandidatePair 的狀態變遷。你能分辨出目前正在使用的連線路徑是 host (內網)、srflx (STUN/公網) 還是 relay (TURN/中繼) 嗎?
延伸閱讀與資源
- MDN: WebRTC connectivity
- Testing Tool: Trickle ICE