戰(zhàn)避坑指南)
1. 從一次線上事故說(shuō)起為什么 candidate 屬性值得單獨(dú)拎出來(lái)講去年冬天團(tuán)隊(duì)里一個(gè)剛接手 ICE 流程的同學(xué)在聯(lián)調(diào)時(shí)遇到一個(gè)詭異現(xiàn)象兩端明明都拿到了對(duì)方的地址連接卻始終卡在 checking 狀態(tài)日志里看不到任何報(bào)錯(cuò)。排查了大半天最后發(fā)現(xiàn)問(wèn)題出在 candidate 的 foundation 字段上——兩臺(tái)設(shè)備因?yàn)榫W(wǎng)絡(luò)環(huán)境相似生成的 foundation 值撞車(chē)了導(dǎo)致 ICE 引擎誤判為同一組候選地址直接跳過(guò)了本該發(fā)起的連通性檢查。這件事讓我意識(shí)到很多人對(duì) ICE 的理解停留在收集候選地址、配對(duì)、檢查連通性這個(gè)粗線條的流程上但真正決定 ICE 能不能跑通、跑得快、跑得穩(wěn)的恰恰是 candidate 屬性這一層的細(xì)節(jié)。標(biāo)題里說(shuō)的ICE 流程1我理解是把 candidate 屬性作為整個(gè) ICE 流程拆解的第一站這個(gè)切入點(diǎn)選得很準(zhǔn)因?yàn)楹竺嫠械呐鋵?duì)、優(yōu)先級(jí)排序、連通性檢查全部建立在 candidate 屬性之上。這篇文章面向的是正在做實(shí)時(shí)音視頻、實(shí)時(shí)通信、P2P 傳輸相關(guān)開(kāi)發(fā)的工程師也適合對(duì)網(wǎng)絡(luò)穿透機(jī)制感興趣、想搞清楚底層原理的技術(shù)人。我會(huì)把 candidate 的每一個(gè)屬性字段拆開(kāi)講清楚它是什么、為什么需要它、實(shí)際生成時(shí)怎么算、踩過(guò)哪些坑。讀完之后你應(yīng)該能獨(dú)立排查大部分跟候選地址相關(guān)的連接問(wèn)題而不是對(duì)著日志干瞪眼。2. candidate 屬性到底是什么先建立整體認(rèn)知2.1 候選地址在 ICE 流程中的位置要理解 candidate 屬性得先知道 candidate 在 ICE 流程里扮演什么角色。ICE 的全稱(chēng)是 Interactive Connectivity Establishment直譯過(guò)來(lái)就是交互式連接建立。它的核心任務(wù)很簡(jiǎn)單讓兩個(gè)位于不同網(wǎng)絡(luò)環(huán)境下的端點(diǎn)找到一條能互相發(fā)送數(shù)據(jù)的路徑。這個(gè)過(guò)程分幾個(gè)階段。第一階段是收集候選地址每個(gè)端點(diǎn)會(huì)從自己的網(wǎng)卡、系統(tǒng)接口、中繼服務(wù)等地方收集所有可能用于通信的地址每一個(gè)地址就是一個(gè) candidate。第二階段是交換候選地址兩端通過(guò)信令通道把各自的候選列表發(fā)給對(duì)方。第三階段是配對(duì)與連通性檢查把本地的候選和遠(yuǎn)端的候選兩兩組合成 candidate pair逐個(gè)發(fā)送檢查包看哪一對(duì)能通。第四階段是提名與選定從能通的配對(duì)里選出一對(duì)最優(yōu)的作為正式傳輸通道。candidate 屬性貫穿了第一階段到第四階段。收集階段決定了屬性的初始值交換階段把這些屬性原樣傳遞配對(duì)和檢查階段則完全依賴(lài)這些屬性來(lái)做優(yōu)先級(jí)排序和去重判斷。可以說(shuō)candidate 屬性就是 ICE 流程的身份證每一個(gè)字段都在后續(xù)環(huán)節(jié)里被反復(fù)讀取和使用。2.2 一個(gè) candidate 的完整結(jié)構(gòu)長(zhǎng)什么樣先看一個(gè)典型的 candidate 描述長(zhǎng)什么樣這是從實(shí)際日志里抓出來(lái)的candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host candidate:2 1 UDP 1694498815 203.0.113.45 12345 typ srflx raddr 192.168.1.100 rport 54321 candidate:3 1 UDP 16777215 198.51.100.20 3478 typ relay raddr 203.0.113.45 rport 12345這三行分別代表了三種典型候選主機(jī)候選、服務(wù)器自反候選、中繼候選。每一行里除了開(kāi)頭的candidate:標(biāo)簽后面跟著一串用空格分隔的字段。這些字段就是我們要重點(diǎn)拆解的 candidate 屬性。從結(jié)構(gòu)上看一個(gè) candidate 由基礎(chǔ)字段和擴(kuò)展字段兩部分組成?;A(chǔ)字段是每個(gè) candidate 都必須有的包括 foundation、component-id、transport、priority、connection-address、port、candidate-type。擴(kuò)展字段則是可選的根據(jù) candidate 類(lèi)型不同而不同比如 raddr、rport、tcptype 等。理解這個(gè)結(jié)構(gòu)很重要因?yàn)樵趯?shí)際開(kāi)發(fā)中你經(jīng)常需要手動(dòng)構(gòu)造 candidate 字符串或者在日志里解析這些字段。字段順序是固定的不能隨意調(diào)換否則解析會(huì)出錯(cuò)。我見(jiàn)過(guò)有同學(xué)在拼接 candidate 時(shí)把 priority 和 port 的位置寫(xiě)反了結(jié)果對(duì)端解析出來(lái)的地址完全錯(cuò)亂排查了很久才發(fā)現(xiàn)是格式問(wèn)題。2.3 為什么這些屬性缺一不可有人可能會(huì)想不就是個(gè) IP 加端口嗎為什么要搞這么多字段這個(gè)問(wèn)題問(wèn)得好答案在于 ICE 要解決的是一個(gè)非常復(fù)雜的工程問(wèn)題在成千上萬(wàn)種網(wǎng)絡(luò)組合下快速找到一條可用的路徑。如果只有 IP 和端口你無(wú)法判斷兩個(gè)候選是不是來(lái)自同一個(gè)網(wǎng)卡去重問(wèn)題無(wú)法決定先檢查哪一對(duì)優(yōu)先級(jí)問(wèn)題無(wú)法知道這個(gè)地址是怎么來(lái)的類(lèi)型問(wèn)題也無(wú)法處理多網(wǎng)卡、多協(xié)議、多中繼的復(fù)雜場(chǎng)景。每一個(gè)屬性字段都是為了解決一個(gè)具體的工程問(wèn)題而存在的。舉個(gè)例子foundation 字段看起來(lái)只是個(gè)簡(jiǎn)單的字符串但它的作用是讓 ICE 引擎快速識(shí)別這兩個(gè)候選來(lái)自同一個(gè)基礎(chǔ)地址從而避免重復(fù)檢查。如果沒(méi)有 foundationICE 就得逐個(gè)比對(duì) IP、端口、類(lèi)型效率會(huì)低很多。再比如 priority 字段它是一個(gè) 32 位整數(shù)編碼了候選類(lèi)型、本地優(yōu)先級(jí)、組件 ID 等多層信息讓排序可以在一個(gè)整數(shù)比較里完成而不是多字段逐一比較。3. 逐個(gè)拆解candidate 核心屬性詳解3.1 foundation候選地址的指紋foundation 是一個(gè)字符串通常是一個(gè)短整數(shù)或者字母數(shù)字組合。它的核心作用是標(biāo)識(shí)一組共享同一基礎(chǔ)地址的候選。什么叫共享同一基礎(chǔ)地址簡(jiǎn)單說(shuō)如果兩個(gè)候選是從同一個(gè)網(wǎng)卡的同一個(gè) IP 上收集來(lái)的只是端口或協(xié)議不同它們的 foundation 就應(yīng)該相同。為什么需要這個(gè)因?yàn)?ICE 在連通性檢查時(shí)如果已經(jīng)檢查過(guò)某個(gè) foundation 的候選對(duì)就不需要再檢查同 foundation 的其他候選對(duì)了。這能大幅減少檢查次數(shù)。想象一下一臺(tái)機(jī)器有 3 個(gè)網(wǎng)卡每個(gè)網(wǎng)卡收集了 UDP 和 TCP 兩種候選那就是 6 個(gè)候選。如果沒(méi)有 foundation 去重兩兩配對(duì)就是 36 對(duì)有了 foundation可能只需要檢查 9 對(duì)。foundation 的生成規(guī)則在不同實(shí)現(xiàn)里略有差異但核心邏輯是一致的把基礎(chǔ) IP、傳輸協(xié)議、候選類(lèi)型這幾個(gè)因素組合起來(lái)做一個(gè)哈?;蛘咧苯悠唇?。RFC 5245 里建議的算法是把基礎(chǔ)地址、協(xié)議、候選類(lèi)型、以及相關(guān)的擴(kuò)展屬性比如 relay 的服務(wù)器地址拼成一個(gè)字符串然后取一個(gè)穩(wěn)定的哈希值。這里有個(gè)坑要注意foundation 必須在整個(gè) ICE 會(huì)話期間保持穩(wěn)定。也就是說(shuō)同一個(gè)候選在多次信令交換中foundation 不能變。我見(jiàn)過(guò)有實(shí)現(xiàn)每次生成 foundation 時(shí)用了隨機(jī)數(shù)導(dǎo)致對(duì)端每次收到的 foundation 都不一樣去重邏輯完全失效檢查次數(shù)暴增連接建立時(shí)間從幾百毫秒漲到了好幾秒。提示如果你在排查連接慢的問(wèn)題先看日志里 foundation 的分布。如果同一個(gè) IP 上的候選 foundation 各不相同大概率是生成邏輯有問(wèn)題。3.2 component-id區(qū)分?jǐn)?shù)據(jù)流通道component-id 是一個(gè)正整數(shù)從 1 開(kāi)始。它用來(lái)標(biāo)識(shí)同一個(gè)候選地址上的不同數(shù)據(jù)流通道。在最早的 ICE 設(shè)計(jì)里component-id 主要服務(wù)于 RTP 和 RTCP 的區(qū)分component-id 為 1 的候選用于傳輸 RTP 媒體流component-id 為 2 的候選用于傳輸 RTCP 控制流。雖然現(xiàn)在很多實(shí)現(xiàn)把 RTP 和 RTCP 復(fù)用在一個(gè)通道上也就是只用一個(gè) component-id但 component-id 這個(gè)字段依然保留著因?yàn)樗诙嗤ǖ缊?chǎng)景下仍然是必要的。比如某些實(shí)時(shí)通信場(chǎng)景需要同時(shí)傳輸音視頻和數(shù)據(jù)可能就會(huì)用到多個(gè) component。component-id 的關(guān)鍵特性是同一個(gè) foundation 下不同 component-id 的候選必須使用不同的端口。這是 ICE 的基本約束因?yàn)橥粋€(gè) IP 加同一個(gè)端口不可能同時(shí)承載兩個(gè)獨(dú)立的通道。如果你在構(gòu)造候選時(shí)違反了這條規(guī)則對(duì)端在配對(duì)時(shí)會(huì)直接丟棄這些候選。實(shí)際開(kāi)發(fā)中component-id 最常見(jiàn)的坑是兩端不一致。比如一端只支持 component-id 1另一端卻發(fā)了 component-id 1 和 2 的候選配對(duì)時(shí) component-id 2 的候選找不到匹配白白浪費(fèi)了檢查資源。所以在信令交換階段最好先協(xié)商好 component 的數(shù)量。3.3 transport傳輸協(xié)議的選擇transport 字段指定了候選使用的傳輸層協(xié)議常見(jiàn)的有 UDP 和 TCP。在實(shí)時(shí)音視頻場(chǎng)景里UDP 是絕對(duì)主力因?yàn)樗难舆t低、抖動(dòng)小適合媒體流傳輸。TCP 通常作為備選用在 UDP 被完全阻斷的網(wǎng)絡(luò)環(huán)境里。UDP 和 TCP 在 ICE 里的行為差異很大。UDP 的連通性檢查就是發(fā)一個(gè)綁定請(qǐng)求收到響應(yīng)就算通。TCP 則復(fù)雜得多因?yàn)?TCP 是面向連接的檢查過(guò)程涉及到握手、連接建立、連接保持等一系列狀態(tài)。而且 TCP 候選還分主動(dòng)和被動(dòng)兩種模式通過(guò) tcptype 擴(kuò)展字段來(lái)標(biāo)識(shí)。tcptype 有三個(gè)取值active、passive、so。active 表示這個(gè)候選會(huì)主動(dòng)發(fā)起 TCP 連接passive 表示它會(huì)等待對(duì)方來(lái)連so 表示可以同時(shí)作為主動(dòng)和被動(dòng)。配對(duì)時(shí)active 必須和 passive 配對(duì)或者 so 和任意一方配對(duì)兩個(gè) active 或兩個(gè) passive 是配不上的。這里有個(gè)實(shí)操經(jīng)驗(yàn)如果你的場(chǎng)景需要 TCP 候選盡量讓一端發(fā) active、另一端發(fā) passive而不是兩端都發(fā) so。因?yàn)?so 雖然靈活但在某些 NAT 環(huán)境下同時(shí)嘗試雙向連接會(huì)導(dǎo)致?tīng)顟B(tài)混亂反而降低成功率。3.4 priority決定檢查順序的核心字段priority 是整個(gè) candidate 屬性里最復(fù)雜、也最關(guān)鍵的字段。它是一個(gè) 32 位無(wú)符號(hào)整數(shù)取值范圍從 0 到 4294967295。ICE 引擎會(huì)按照 priority 從高到低排序候選對(duì)優(yōu)先檢查高優(yōu)先級(jí)的配對(duì)。priority 的計(jì)算公式在 RFC 5245 里有明確定義priority (2^24) * type_pref (2^8) * local_pref (2^0) * (256 - component_id)這個(gè)公式把三種信息編碼進(jìn)了一個(gè)整數(shù)。type_pref 是候選類(lèi)型的偏好值host 類(lèi)型最高srflx 次之relay 最低。local_pref 是本地優(yōu)先級(jí)通常根據(jù)網(wǎng)卡的重要程度來(lái)設(shè)置比如有線網(wǎng)卡比無(wú)線網(wǎng)卡高。component_id 則通過(guò)256 - component_id的方式反向編碼讓 component-id 小的候選優(yōu)先級(jí)略高。具體數(shù)值上host 的 type_pref 通常是 126srflx 是 100relay 是 0。這樣算下來(lái)host 候選的 priority 大約在 2130706431 左右srflx 在 1694498815 左右relay 在 16777215 左右。這個(gè)差距非常大確保了 host 候選永遠(yuǎn)優(yōu)先于 srflxsrflx 永遠(yuǎn)優(yōu)先于 relay。為什么這么設(shè)計(jì)因?yàn)?host 候選是直連延遲最低、帶寬最大、最可靠srflx 需要經(jīng)過(guò) NAT 映射多了一跳relay 要經(jīng)過(guò)中繼服務(wù)器延遲最高、成本也最高。ICE 的設(shè)計(jì)哲學(xué)就是能直連就直連直連不行再中繼priority 就是這個(gè)哲學(xué)的數(shù)值化體現(xiàn)。注意priority 是本地生成的但交換后對(duì)端會(huì)原樣使用。也就是說(shuō)你生成的 priority 會(huì)影響對(duì)端的檢查順序。所以不要隨意設(shè)置 priority要按照標(biāo)準(zhǔn)公式來(lái)算否則可能導(dǎo)致連接建立變慢。3.5 connection-address 和 port實(shí)際的通信端點(diǎn)connection-address 就是候選的 IP 地址port 就是端口號(hào)。這兩個(gè)字段看起來(lái)最簡(jiǎn)單但實(shí)際踩坑最多。先說(shuō) IP 地址。在 IPv4 環(huán)境下就是點(diǎn)分十進(jìn)制的形式比如 192.168.1.100。在 IPv6 環(huán)境下是冒號(hào)分隔的十六進(jìn)制形式。這里有個(gè)細(xì)節(jié)IPv6 地址在 candidate 字符串里通常不帶方括號(hào)直接寫(xiě)地址本身。但如果在其他上下文里引用可能需要加方括號(hào)來(lái)區(qū)分端口。再說(shuō)端口。端口號(hào)的范圍是 1 到 65535但實(shí)際可用的端口受操作系統(tǒng)限制。在 Linux 上1024 以下的端口需要 root 權(quán)限所以 ICE 候選通常使用 1024 以上的端口。另外很多企業(yè)防火墻會(huì)限制某些端口范圍如果你的候選端口恰好落在被封鎖的范圍里連通性檢查就會(huì)失敗。這里有個(gè)實(shí)操技巧在收集候選時(shí)盡量多收集幾個(gè)端口。因?yàn)槟承?NAT 設(shè)備會(huì)對(duì)不同端口做不同的映射策略多試幾個(gè)端口能提高穿透成功率。我見(jiàn)過(guò)一個(gè)案例某運(yùn)營(yíng)商的 NAT 對(duì) 50000 以上的端口映射很穩(wěn)定但對(duì) 30000 以下的端口映射經(jīng)常失敗多收集端口后成功率從 60% 提升到了 95%。3.6 candidate-type候選的來(lái)源分類(lèi)candidate-type 標(biāo)識(shí)了候選的來(lái)源常見(jiàn)的有四種host、srflx、prflx、relay。host 是主機(jī)候選直接從本地網(wǎng)卡上收集到的地址。這是最原始的候選沒(méi)有經(jīng)過(guò)任何 NAT 映射或中繼。在局域網(wǎng)內(nèi)host 候選通常能直接連通。srflx 是服務(wù)器自反候選通過(guò) STUN 服務(wù)器獲取。端向 STUN 服務(wù)器發(fā)送請(qǐng)求服務(wù)器返回它看到的源地址這個(gè)地址就是端在 NAT 上的公網(wǎng)映射地址。srflx 候選能穿透大部分 NAT是公網(wǎng)通信的主力。prflx 是對(duì)等自反候選不是在收集階段生成的而是在連通性檢查過(guò)程中發(fā)現(xiàn)的。當(dāng)端收到一個(gè)來(lái)自未知地址的檢查請(qǐng)求時(shí)會(huì)把那個(gè)地址作為一個(gè) prflx 候選加入列表。prflx 主要用于處理 NAT 映射變化的情況比如對(duì)稱(chēng) NAT 下不同目標(biāo)的映射端口不同prflx 能捕捉到這種變化。relay 是中繼候選通過(guò) TURN 服務(wù)器分配。端向 TURN 服務(wù)器申請(qǐng)一個(gè)中繼地址所有數(shù)據(jù)都通過(guò)這個(gè)地址轉(zhuǎn)發(fā)。relay 的延遲最高、成本最大但穿透能力最強(qiáng)幾乎能應(yīng)對(duì)所有網(wǎng)絡(luò)環(huán)境。ICE 的原則是最后才用 relay所以 relay 的 priority 最低。這四種類(lèi)型的優(yōu)先級(jí)順序是host srflx prflx relay。實(shí)際實(shí)現(xiàn)里prflx 的優(yōu)先級(jí)通常和 srflx 相當(dāng)或略低因?yàn)樗彩枪W(wǎng)地址只是發(fā)現(xiàn)方式不同。3.7 擴(kuò)展字段raddr、rport 與 tcptype除了基礎(chǔ)字段candidate 還可以帶一些擴(kuò)展字段用來(lái)補(bǔ)充描述候選的來(lái)源信息。raddr 是 related address表示這個(gè)候選的相關(guān)地址。對(duì)于 srflx 候選raddr 是本地的主機(jī)地址也就是 NAT 映射前的地址。對(duì)于 relay 候選raddr 是中繼服務(wù)器看到的端的地址。rport 則是相關(guān)的端口。這兩個(gè)字段的主要作用是診斷和優(yōu)化。比如在排查 NAT 類(lèi)型時(shí)對(duì)比 raddr 和 connection-address 就能看出 NAT 做了什么樣的映射。如果 raddr 相同但 connection-address 不同說(shuō)明是對(duì)稱(chēng) NAT如果 raddr 和 connection-address 的端口也相同說(shuō)明是錐形 NAT。tcptype 是 TCP 候選特有的擴(kuò)展字段取值 active、passive、so。前面講 transport 時(shí)已經(jīng)提過(guò)這里補(bǔ)充一點(diǎn)tcptype 只在 transport 為 TCP 時(shí)出現(xiàn)UDP 候選沒(méi)有這個(gè)字段。如果你在解析 candidate 時(shí)看到 UDP 候選帶了 tcptype那肯定是格式錯(cuò)誤。4. 實(shí)操生成、解析與調(diào)試 candidate4.1 手動(dòng)構(gòu)造一個(gè)合法的 candidate 字符串理解了各個(gè)字段的含義現(xiàn)在來(lái)實(shí)際構(gòu)造一個(gè) candidate。假設(shè)我們要構(gòu)造一個(gè) host 類(lèi)型的 UDP 候選IP 是 192.168.1.100端口 54321component-id 為 1。先算 priority。type_pref 取 126local_pref 取 65535最大值component_id 為 1priority (2^24) * 126 (2^8) * 65535 (256 - 1) 16777216 * 126 256 * 65535 255 2113929216 16776960 255 2130706431然后 foundation 可以取一個(gè)基于 IP 和類(lèi)型的哈希值這里簡(jiǎn)化為 1。最終字符串是candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host再構(gòu)造一個(gè) srflx 候選公網(wǎng)地址是 203.0.113.45端口 12345相關(guān)地址是 192.168.1.100:54321candidate:2 1 UDP 1694498815 203.0.113.45 12345 typ srflx raddr 192.168.1.100 rport 54321注意字段順序foundation、component-id、transport、priority、connection-address、port、typ、candidate-type然后是擴(kuò)展字段。這個(gè)順序不能亂解析器是按位置讀取的。4.2 從日志里解析 candidate 的實(shí)用方法實(shí)際調(diào)試時(shí)你拿到的往往是一大串 candidate 字符串需要快速提取關(guān)鍵信息。用正則表達(dá)式是最方便的方式。下面這個(gè) Python 片段可以解析標(biāo)準(zhǔn)格式的 candidateimport re def parse_candidate(line): pattern rcandidate:(\S) (\d) (\S) (\d) (\S) (\d) typ (\S)(.*) match re.match(pattern, line.strip()) if not match: return None foundation, component_id, transport, priority, addr, port, ctype, ext match.groups() result { foundation: foundation, component_id: int(component_id), transport: transport, priority: int(priority), address: addr, port: int(port), type: ctype, } # 解析擴(kuò)展字段 for kv in ext.strip().split(): if kv in (raddr, rport, tcptype): result[kv] True ext_parts ext.strip().split() for i in range(0, len(ext_parts) - 1, 2): result[ext_parts[i]] ext_parts[i 1] return result這個(gè)解析器能處理大部分標(biāo)準(zhǔn)格式的 candidate。實(shí)際使用時(shí)你可能需要根據(jù)具體實(shí)現(xiàn)的格式做微調(diào)比如有些實(shí)現(xiàn)會(huì)在 candidate 前面加a前綴這是 SDP 格式解析前先去掉即可。4.3 用 priority 排序驗(yàn)證候選質(zhì)量拿到候選列表后第一件事是按 priority 排序看看高優(yōu)先級(jí)的候選是不是符合預(yù)期。正常情況下host 候選應(yīng)該排在最前面然后是 srflx最后是 relay。如果發(fā)現(xiàn) relay 候選的 priority 比 srflx 還高那說(shuō)明 priority 計(jì)算有問(wèn)題。常見(jiàn)原因是 type_pref 設(shè)置錯(cuò)了比如把 relay 的 type_pref 設(shè)成了 100 而不是 0。這種錯(cuò)誤會(huì)導(dǎo)致 ICE 優(yōu)先嘗試中繼路徑連接延遲大幅增加。另一個(gè)檢查點(diǎn)是同一類(lèi)型候選之間的排序。比如有兩個(gè) host 候選一個(gè)來(lái)自有線網(wǎng)卡一個(gè)來(lái)自無(wú)線網(wǎng)卡有線網(wǎng)卡的 local_pref 應(yīng)該更高priority 也應(yīng)該更高。如果排序反了ICE 會(huì)優(yōu)先嘗試無(wú)線網(wǎng)卡可能帶來(lái)不必要的延遲和抖動(dòng)。4.4 連通性檢查失敗的排查思路連通性檢查失敗是最常見(jiàn)的問(wèn)題表現(xiàn)是 candidate pair 一直處于 waiting 或 in-progress 狀態(tài)最終超時(shí)。排查時(shí)按以下順序檢查第一看候選是否真的交換成功了。有時(shí)候信令通道有問(wèn)題對(duì)端根本沒(méi)收到候選列表或者收到的列表是空的。檢查信令日志確認(rèn)雙方都發(fā)送和接收了 candidate。第二看 foundation 是否有重復(fù)。如果兩個(gè)不同的候選 foundation 相同ICE 會(huì)認(rèn)為它們是同一組可能跳過(guò)必要的檢查。檢查 foundation 的生成邏輯確保不同基礎(chǔ)地址的候選 foundation 不同。第三看端口是否被防火墻阻斷。用 telnet 或 nc 命令測(cè)試候選的 IP 和端口是否可達(dá)。如果不可達(dá)說(shuō)明中間有防火墻或 NAT 設(shè)備阻斷了流量。第四看 transport 是否匹配。如果一端只發(fā) UDP 候選另一端只發(fā) TCP 候選那永遠(yuǎn)配不上對(duì)。檢查雙方的 transport 配置確保至少有一種共同的協(xié)議。第五看 component-id 是否一致。前面提過(guò)component-id 不匹配的候選無(wú)法配對(duì)。檢查雙方的 component 配置。5. 常見(jiàn)問(wèn)題速查與避坑經(jīng)驗(yàn)5.1 candidate 屬性相關(guān)的高頻問(wèn)題下面這張表整理了實(shí)際開(kāi)發(fā)中最常遇到的 candidate 屬性問(wèn)題以及對(duì)應(yīng)的排查方向問(wèn)題現(xiàn)象可能原因排查方向連接卡在 checkingfoundation 重復(fù)導(dǎo)致去重錯(cuò)誤檢查 foundation 生成邏輯連接建立慢priority 排序錯(cuò)誤先檢查了低優(yōu)先級(jí)候選驗(yàn)證 priority 計(jì)算公式部分候選被丟棄component-id 或 transport 不匹配檢查雙方配置一致性srflx 候選缺失STUN 服務(wù)器不可達(dá)或返回錯(cuò)誤測(cè)試 STUN 服務(wù)器連通性relay 候選無(wú)法使用TURN 服務(wù)器認(rèn)證失敗或配額不足檢查 TURN 憑證和配額IPv6 候選解析失敗地址格式不正確確認(rèn) IPv6 地址不帶方括號(hào)TCP 候選配不上對(duì)tcptype 組合不合法確保 active 配 passive5.2 三個(gè)我踩過(guò)的坑第一個(gè)坑foundation 用了隨機(jī)數(shù)。前面提過(guò)某次實(shí)現(xiàn)里 foundation 每次生成都不同導(dǎo)致對(duì)端每次收到的 foundation 都不一樣去重完全失效。表現(xiàn)是連接建立時(shí)間從 300 毫秒漲到了 5 秒以上。修復(fù)方法很簡(jiǎn)單把 foundation 改成基于基礎(chǔ)地址的確定性哈希。第二個(gè)坑priority 的 local_pref 設(shè)成了 0。有一次為了簡(jiǎn)化把 local_pref 統(tǒng)一設(shè)成了 0結(jié)果所有同類(lèi)型候選的 priority 都一樣ICE 無(wú)法區(qū)分有線網(wǎng)卡和無(wú)線網(wǎng)卡隨機(jī)選擇路徑。表現(xiàn)是連接質(zhì)量不穩(wěn)定有時(shí)延遲很低有時(shí)抖動(dòng)很大。修復(fù)方法是根據(jù)網(wǎng)卡類(lèi)型設(shè)置不同的 local_pref。第三個(gè)坑raddr 和 rport 填反了。在構(gòu)造 srflx 候選時(shí)把 raddr 和 rport 的位置寫(xiě)反了導(dǎo)致對(duì)端解析出來(lái)的相關(guān)地址完全錯(cuò)誤。雖然不影響連通性檢查因?yàn)闄z查用的是 connection-address 和 port但在診斷 NAT 類(lèi)型時(shí)得到了錯(cuò)誤結(jié)論。修復(fù)方法是嚴(yán)格按照字段順序拼接。5.3 一個(gè)容易被忽略的細(xì)節(jié)候選的生存時(shí)間candidate 不是永久有效的。每個(gè)候選都有一個(gè)生存時(shí)間超過(guò)這個(gè)時(shí)間后候選會(huì)被標(biāo)記為失效不再參與連通性檢查。生存時(shí)間的長(zhǎng)度取決于候選類(lèi)型host 候選通常很長(zhǎng)因?yàn)楸镜氐刂凡粫?huì)變srflx 候選取決于 NAT 映射的保持時(shí)間通常幾十秒到幾分鐘relay 候選取決于 TURN 服務(wù)器的分配策略通常幾分鐘到幾十分鐘。實(shí)際開(kāi)發(fā)中如果 ICE 會(huì)話持續(xù)時(shí)間很長(zhǎng)需要定期刷新候選否則可能出現(xiàn)連接突然中斷的問(wèn)題。刷新方式是重新收集候選并交換或者通過(guò) STUN 綁定請(qǐng)求保持 NAT 映射。我見(jiàn)過(guò)一個(gè)案例某應(yīng)用在通話 10 分鐘后突然斷線排查發(fā)現(xiàn)是 srflx 候選的 NAT 映射過(guò)期了而應(yīng)用沒(méi)有及時(shí)刷新。提示如果你的應(yīng)用需要長(zhǎng)時(shí)間保持連接建議在 ICE 配置里設(shè)置候選刷新間隔通常取 NAT 映射保持時(shí)間的一半比較穩(wěn)妥。6. 從 candidate 屬性看 ICE 的設(shè)計(jì)哲學(xué)把 candidate 屬性全部拆完回頭再看 ICE 的整個(gè)設(shè)計(jì)會(huì)發(fā)現(xiàn)一個(gè)很清晰的思路用豐富的元數(shù)據(jù)換取決策效率。每一個(gè)屬性字段都是為了在某個(gè)環(huán)節(jié)減少計(jì)算量或提高準(zhǔn)確性。foundation 用于快速去重priority 用于快速排序candidate-type 用于快速分類(lèi)raddr 用于快速診斷。這些字段在收集階段一次性生成之后在整個(gè) ICE 流程里被反復(fù)使用避免了重復(fù)計(jì)算。這種設(shè)計(jì)思路在實(shí)際工程里很有借鑒意義。很多系統(tǒng)在設(shè)計(jì)時(shí)喜歡按需計(jì)算用到什么算什么但 ICE 選擇了提前計(jì)算、冗余存儲(chǔ)的路子。代價(jià)是候選字符串變長(zhǎng)了交換的數(shù)據(jù)量變大了但換來(lái)的是檢查階段的高效和穩(wěn)定。在實(shí)時(shí)通信這種對(duì)延遲極度敏感的場(chǎng)景里這個(gè)取舍是值得的。另一個(gè)值得注意的點(diǎn)是 ICE 對(duì)漸進(jìn)式降級(jí)的支持。host 不行用 srflxsrflx 不行用 relay每一層都有對(duì)應(yīng)的候選類(lèi)型和優(yōu)先級(jí)。這種設(shè)計(jì)讓 ICE 能在各種網(wǎng)絡(luò)環(huán)境下工作從最簡(jiǎn)單的局域網(wǎng)到最復(fù)雜的多層 NAT都能找到一條可用的路徑。candidate 屬性里的 type_pref 和 priority 就是這個(gè)降級(jí)策略的數(shù)值化表達(dá)。我在實(shí)際項(xiàng)目里體會(huì)最深的一點(diǎn)是不要試圖繞過(guò)標(biāo)準(zhǔn)去優(yōu)化 candidate 屬性。曾經(jīng)有團(tuán)隊(duì)為了減少候選數(shù)量手動(dòng)過(guò)濾掉了所有 relay 候選結(jié)果在部分嚴(yán)格 NAT 環(huán)境下完全連不上。也有團(tuán)隊(duì)為了提高優(yōu)先級(jí)把 relay 的 type_pref 調(diào)高結(jié)果正常網(wǎng)絡(luò)下也走了中繼延遲翻倍。標(biāo)準(zhǔn)里的每一個(gè)數(shù)值都是經(jīng)過(guò)大量實(shí)踐驗(yàn)證的除非你有非常明確的理由和充分的測(cè)試否則老老實(shí)實(shí)按標(biāo)準(zhǔn)來(lái)。最后分享一個(gè)小技巧在調(diào)試 ICE 問(wèn)題時(shí)把雙方的 candidate 列表按 priority 排序后并排打印出來(lái)一眼就能看出配對(duì)的可能性。高優(yōu)先級(jí)的 host 對(duì) host 如果能配上連接就是最優(yōu)的如果只能靠 relay 對(duì) relay那就要考慮網(wǎng)絡(luò)環(huán)境是不是太苛刻了。這個(gè)簡(jiǎn)單的對(duì)比方法幫我快速定位過(guò)很多次連接質(zhì)量問(wèn)題。