一、WebRTC與架構(gòu)
簡(jiǎn)單來(lái)說(shuō),WebRTC 是一個(gè)可以在 Web 應(yīng)用程序中實(shí)現(xiàn)音頻,視頻和數(shù)據(jù)的實(shí)時(shí)通信的開(kāi)源項(xiàng)目霞丧。在實(shí)時(shí)通信中和泌,音視頻的采集和處理是一個(gè)很復(fù)雜的過(guò)程荧呐。比如音視頻流的編解碼、降噪和回聲消除等擅憔,但是在 WebRTC 中鸵闪,這一切都交由瀏覽器的底層封裝來(lái)完成。我們可以直接拿到優(yōu)化后的媒體流暑诸,然后將其輸出到本地屏幕和揚(yáng)聲器蚌讼,或者轉(zhuǎn)發(fā)給其對(duì)等端。
- Web API層:面向開(kāi)發(fā)者提供標(biāo)準(zhǔn)API(javascirpt)个榕,前端應(yīng)用通過(guò)這一層接入使用WebRTC能力篡石。
- C++ API層:面向?yàn)g覽器開(kāi)發(fā)者,使瀏覽器制造商能夠輕松地實(shí)現(xiàn)Web API方案西采。
- 音頻引擎(VoiceEngine):音頻引擎是一系列音頻多媒體處理的框架凰萨,包括從視頻采集卡到網(wǎng)絡(luò)傳輸端等整個(gè)解決方案。
- iSAC/iLBC/Opus等編解碼
- NetEQ語(yǔ)音信號(hào)處理
- 回聲消除和降噪
- 視頻引擎(VideoEngine): 是一系列視頻處理的整體框架,從攝像頭采集視頻胖眷、視頻信息網(wǎng)絡(luò)傳輸?shù)揭曨l顯示整個(gè)完整過(guò)程的解決方案武通。
- VP8編解碼
- jitter buffer:動(dòng)態(tài)抖動(dòng)緩沖
- Image enhancements:圖像增益
- 傳輸(Transport):傳輸 / 會(huì)話層,會(huì)話協(xié)商 + NAT穿透組件
- RTP 實(shí)時(shí)協(xié)議
- P2P傳輸 STUN+TRUN+ICE實(shí)現(xiàn)的網(wǎng)絡(luò)穿越
- 硬件模塊:音視頻的硬件捕獲以及NetWork IO相關(guān)
雖然瀏覽器給我們解決了大部分音視頻處理問(wèn)題珊搀,但是從瀏覽器請(qǐng)求音頻和視頻時(shí)厅须,我們還是需要特別注意流的大小和質(zhì)量。因?yàn)榧幢阌布軌虿东@高清質(zhì)量流食棕,CPU 和帶寬也不一定可以跟上朗和,這也是我們?cè)诮⒍鄠€(gè)對(duì)等連接時(shí),不得不考慮的問(wèn)題簿晓。
二眶拉、WebRTC的重要的類(lèi)和API
1.MediaStream(媒體流)和 MediaStreamTrack(媒體軌道)
這個(gè)類(lèi)并不完全屬于WebRTC的范疇,但是在本地媒體流獲取憔儿,及遠(yuǎn)端流傳到vedio標(biāo)簽播放都與WebRTC相關(guān)忆植。 MS 由兩部分構(gòu)成: MediaStreamTrack 和 MediaStream。
- MediaStreamTrack 媒體軌谒臼,代表一種單類(lèi)型數(shù)據(jù)流朝刊,可以是音頻軌或者視頻軌。
-
MediaStream 是一個(gè)完整的音視頻流蜈缤。它可以包含 >=0 個(gè) MediaStreamTrack拾氓。它主要的作用就是確保幾個(gè)媒體軌道是同步播放。
2.Constraints 媒體約束
關(guān)于MediaStream底哥,還有一個(gè)重要的概念叫做: Constraints(約束)咙鞍。它是用來(lái)規(guī)范當(dāng)前采集的數(shù)據(jù)是否符合需要,并可以通過(guò)參數(shù)來(lái)設(shè)置趾徽。
// 基本
const constraint1 = {
"audio": true, // 是否捕獲音頻
"video": true // 是否捕獲視頻
}
// 詳細(xì)
const constraint2 = {
"audio": {
"sampleSize": 8,
"echoCancellation": true //回聲消除
},
"video": { // 視頻相關(guān)設(shè)置
"width": {
"min": "381", // 當(dāng)前視頻的最小寬度
"max": "640"
},
"height": {
"min": "200", // 最小高度
"max": "480"
},
"frameRate": {
"min": "28", // 最小幀率
"max": "10"
}
}
}
// 獲取指定寬高续滋,這里需要注意:在改變視頻流的寬高時(shí),
// 如果寬高比和采集到的不一樣孵奶,會(huì)直接截掉某部分
{ audio: false,
video: { width: 1280, height: 720 }
}
// 設(shè)定理想值疲酌、最大值、最小值
{
audio: true,
video: {
width: { min: 1024, ideal: 1280, max: 1920 },
height: { min: 776, ideal: 720, max: 1080 }
}
}
對(duì)于移動(dòng)設(shè)備來(lái)說(shuō)了袁,還可以指定獲取前攝像頭朗恳,或者后置攝像頭:
{ audio: true, video: { facingMode: "user" } } // 前置
{ audio: true, video: { facingMode: { exact: "environment" } } } // 后置
// 也可以指定設(shè)備 id,
// 通過(guò) navigator.mediaDevices.enumerateDevices() 可以獲取到支持的設(shè)備
{ video: { deviceId: myCameraDeviceId } }
還有一個(gè)比較有意思的就是設(shè)置視頻源為屏幕早像,但是目前只有火狐支持了這個(gè)屬性僻肖。
{ audio: true, video: {mediaSource: 'screen'} }
3.獲取設(shè)備本地音視頻
其中本地媒體流獲取用到的是navigator.getUserMedia(),它提供了訪問(wèn)用戶本地相機(jī)/麥克風(fēng)媒體流的手段。
var video = document.querySelector('video');
navigator.getUserMedia({
audio : true,
video : true
}, function (stream) {
//拿到本地媒體流
video.src = window.URL.creatObjectURL(stream);
}, function (error) {
console.log(error);
});
getUserMedia的第一個(gè)參數(shù)就是Constraint卢鹦,第二個(gè)參數(shù)傳入回調(diào)函數(shù)拿到視頻流。當(dāng)然你可以使用如下Promise的寫(xiě)法:
navigator.mediaDevices.getUserMedia(constraints).
then(successCallback).catch(errorCallback);
4.RTCPeerConnection
RTCPeerConnection,用于實(shí)現(xiàn)peer跟peer之間的NAT穿透冀自,繼而無(wú)需服務(wù)器就能傳輸音視頻數(shù)據(jù)流的連接通道揉稚。
這么說(shuō)過(guò)于抽象,為了幫助理解熬粗,可以用一個(gè)不太恰當(dāng)?shù)兄诶斫獾谋扔鳎篟TCPeerConnection就是一個(gè)高級(jí)且功能強(qiáng)大的用于傳輸音視頻數(shù)據(jù)而建立類(lèi)似Websocket鏈接通道搀玖,只不過(guò)它可以用來(lái)建立瀏覽器
之所以說(shuō)是高級(jí)且強(qiáng)大,是因?yàn)樗鳛閃ebRTC web層核心API驻呐,讓你無(wú)須關(guān)注數(shù)據(jù)傳輸延遲抖動(dòng)灌诅、音視頻編解碼,音畫(huà)同步等問(wèn)題含末。直接使用PeerConnection 就能用上這些瀏覽器提供的底層封裝好的能力猜拾。
5.Peer-to-peer Data API
RTCDataChannel可以建立瀏覽器之間的點(diǎn)對(duì)點(diǎn)通訊。常用的通訊方式有websocket, ajax和等方式佣盒。websocket雖然是雙向通訊挎袜,但是無(wú)論是websocket還是ajax都是客戶端和服務(wù)器之間的通訊,你必須配置服務(wù)器才可以進(jìn)行通訊肥惭。
而由于RTCDATAChannel借助RTCPeerConnection無(wú)需經(jīng)過(guò)服務(wù)器盯仪,就可以提供點(diǎn)對(duì)點(diǎn)之間的通訊,無(wú)需/(避免)服務(wù)器了這個(gè)中間件蜜葱。
var pc = new RTCPeerConnection();
var dc = pc.createDataChannel("my channel");
dc.onmessage = function (event) {
console.log("received: " + event.data);
};
dc.onopen = function () {
console.log("datachannel open");
};
dc.onclose = function () {
console.log("datachannel close");
};
6.信令Signaling
我們說(shuō)WebRTC的RTCPeerConnection是可以做到瀏覽器間(無(wú)服務(wù))的通信全景。
但這里有個(gè)問(wèn)題,當(dāng)兩個(gè)瀏覽器不通過(guò)服務(wù)器建立PeerConnection時(shí)牵囤,它們?cè)趺粗辣舜说拇嬖谀仳窖啵窟M(jìn)一步講,它們?cè)撛趺粗缹?duì)方的網(wǎng)絡(luò)連接位置(IP/端口等)呢奔浅?支持何種編解碼器馆纳?甚至于什么時(shí)候開(kāi)始媒體流傳輸、又該什么時(shí)候結(jié)束呢汹桦?
因此在建立WebRTC的RTCPeerConnection前鲁驶,必須建立?另一條通道來(lái)交這些協(xié)商信息,這些也被稱(chēng)為信令舞骆,這條通道成為信令通道(Signaling Channel)钥弯。
兩個(gè)客戶端瀏覽器交換的信令具有以下功能:
- 協(xié)商媒體功能和設(shè)置
- 標(biāo)識(shí)和驗(yàn)證會(huì)話參與者的身份(交換SDP對(duì)象中的信息:媒體類(lèi)型、編解碼器督禽、帶寬等元數(shù)據(jù))
- 控制媒體會(huì)話脆霎、指示進(jìn)度、更改會(huì)話狈惫、終止會(huì)話等
- 其中主要涉及SDP(offer睛蛛、answer)會(huì)話描述協(xié)議,以及ICE candidate的交換。
這里需要注意的一點(diǎn):WebRTC標(biāo)準(zhǔn)本身沒(méi)有規(guī)定信令交換的通訊方式忆肾,信令服務(wù)根據(jù)自身的情況實(shí)現(xiàn)荸频。一般會(huì)使用websocket通道來(lái)做信令通道,比如可以基于http://socket.io來(lái)搭建信令服務(wù)客冈。當(dāng)然業(yè)界也有很多開(kāi)源且穩(wěn)定成熟的信令服務(wù)方案可供選擇旭从。
顯而易見(jiàn),在上述連接的過(guò)程中:呼叫端(在這里都是指代瀏覽器)需要給接收端發(fā)送一條名為offer的信息场仲。 接收端在接收到請(qǐng)求后和悦,則返回一條 answer 信息給呼叫端。
這便是上述任務(wù)之一 渠缕,SDP 格式的本地媒體元數(shù)據(jù)的交換鸽素。sdp 信息一般長(zhǎng)這樣:
v=0
o=- 1837933589686018726 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS yvKeJMUSZzvJlAJHn4unfj6q9DMqmb6CrCOT
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
...
但是任務(wù)不僅僅是交換,還需要分別保存自己和對(duì)方的信息褐健,所以我們?cè)偌狱c(diǎn)料:
- 呼叫端 創(chuàng)建 offer 信息后付鹿,先調(diào)用 setLocalDescription 存儲(chǔ)本地 offer 描述,再將其發(fā)送給 接收端蚜迅。
- 接收端 收到 offer 后舵匾,先調(diào)用 setRemoteDescription 存儲(chǔ)遠(yuǎn)端 offer 描述;然后又創(chuàng)建 answer 信息谁不,同樣需要調(diào)用 setLocalDescription 存儲(chǔ)本地 answer 描述坐梯,再返回給 接收端
- 呼叫端 拿到 answer 后,再次調(diào)用 setRemoteDescription 設(shè)置遠(yuǎn)端 answer 描述刹帕。
到這里點(diǎn)對(duì)點(diǎn)連接還缺一步吵血,也就是網(wǎng)絡(luò)信息 ICE 候選交換。不過(guò)這一步和 offer偷溺、answer 信息的交換并沒(méi)有先后順序蹋辅,流程也是一樣的。即:在呼叫端和接收端的 ICE 候選信息準(zhǔn)備完成后挫掏,進(jìn)行交換侦另,并互相保存對(duì)方的信息,這樣就完成了一次連接尉共。
這張圖是我認(rèn)為比較完善的了褒傅,詳細(xì)的描述了整個(gè)連接的過(guò)程。正好我們?cè)賮?lái)小結(jié)一下:
- 基礎(chǔ)設(shè)施:必要的信令服務(wù)和 NAT 穿越服務(wù)
- clientA 和 clientB 分別創(chuàng)建 RTCPeerConnection 并為輸出端添加本地媒體流袄友。如果是視頻通話類(lèi)型殿托,則意味著,兩端都需要添加媒體流進(jìn)行輸出剧蚣。
- 本地 ICE 候選信息采集完成后支竹,通過(guò)信令服務(wù)進(jìn)行交換旋廷。
- 呼叫端(好比 A 給 B 打視頻電話,A 為呼叫端)發(fā)起 offer 信息唾戚,接收端接收并返回一個(gè) answer 信息柳洋,呼叫端保存待诅,完成連接叹坦。
三、WebRTC建立連接的關(guān)鍵-ICE連接
在交換SDP后,webrtc就開(kāi)始真正的連接來(lái)傳輸音視頻數(shù)據(jù)卑雁。這個(gè)建立連接的過(guò)程相當(dāng)復(fù)雜募书,原因是webrtc既要保證高效的傳輸性,又要保證穩(wěn)定的連通性测蹲。
由于瀏覽器客戶端之間所處的位置往往是相當(dāng)復(fù)雜的莹捡,可能處于同一個(gè)內(nèi)網(wǎng)段內(nèi),也可能處于兩個(gè)不同的位置扣甲,所處的NAT網(wǎng)關(guān)也可能很復(fù)雜篮赢。因此需要一種機(jī)制找到一條傳輸質(zhì)量最優(yōu)的道路,而WebRTC正具備這種能力琉挖。
首先簡(jiǎn)單了解以下三個(gè)概念启泣。
- ICE Canidate(ICE 候選者):包含遠(yuǎn)端通信時(shí)使用的協(xié)議、IP 地址和端口示辈、候選者類(lèi)型等信息寥茫。
- STUN/TURN:STUN實(shí)現(xiàn)P2P型連接,TRUN實(shí)現(xiàn)中繼型連接矾麻。兩者實(shí)現(xiàn)均有標(biāo)準(zhǔn)協(xié)議纱耻。(參考下圖)
- NAT穿越:NAT即網(wǎng)絡(luò)地址轉(zhuǎn)換,由于客戶端并不能分配到公網(wǎng)IP险耀,需要內(nèi)網(wǎng)IP與公網(wǎng)IP端口做映射才能與外網(wǎng)通信弄喘。而NAT穿越就是位于層層Nat網(wǎng)關(guān)背后的客戶端之間發(fā)現(xiàn)對(duì)方并建立連接。
ICE連接大致的原理及步驟如下:
- 發(fā)起收集ICE Canidate任務(wù)甩牺。
- 本機(jī)能收集host類(lèi)型(內(nèi)網(wǎng)IP端口)的candidate蘑志。
- 通過(guò)STUN服務(wù)器收集srflx類(lèi)型(NAT映射到外網(wǎng)的IP端口)的candiate。
- 通過(guò)TURN服務(wù)器收集relay類(lèi)型的(中繼服務(wù)器的 IP 和端口)的candidate柴灯。
- 開(kāi)始嘗試NAT穿越卖漫,按照host類(lèi)型、srflx類(lèi)型赠群、relay類(lèi)型的優(yōu)先級(jí)去連接羊始。
以上,WebRTC便能找到一條傳輸質(zhì)量最優(yōu)的連接道路查描。 當(dāng)然實(shí)際情況并不是這么簡(jiǎn)單突委,整個(gè)過(guò)程包含著更復(fù)雜的底層細(xì)節(jié)柏卤。
以下參考
ICE協(xié)議下NAT穿越的實(shí)現(xiàn)(STUN&TURN)
1.首先來(lái)簡(jiǎn)單講講什么是NAT?
原來(lái)這是因?yàn)镮PV4引起的匀油,我們上網(wǎng)很可能會(huì)處在一個(gè)NAT設(shè)備(無(wú)線路由器之類(lèi))之后缘缚。
NAT設(shè)備會(huì)在IP封包通過(guò)設(shè)備時(shí)修改源/目的IP地址. 對(duì)于家用路由器來(lái)說(shuō), 使用的是網(wǎng)絡(luò)地址端口轉(zhuǎn)換(NAPT), 它不僅改IP, 還修改TCP和UDP協(xié)議的端口號(hào), 這樣就能讓內(nèi)網(wǎng)中的設(shè)備共用同一個(gè)外網(wǎng)IP. 舉個(gè)例子, NAPT維護(hù)一個(gè)類(lèi)似下表的NAT表:
NAT設(shè)備會(huì)根據(jù)NAT表對(duì)出去和進(jìn)來(lái)的數(shù)據(jù)做修改, 比如將192.168.0.3:8888發(fā)出去的封包改成120.132.92.21:9202, 外部就認(rèn)為他們是在和120.132.92.21:9202通信. 同時(shí)NAT設(shè)備會(huì)將120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再發(fā)給內(nèi)網(wǎng)的主機(jī), 這樣內(nèi)部和外部就能雙向通信了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202這一映射因?yàn)槟承┰虮籒AT設(shè)備淘汰了, 那么外部設(shè)備就無(wú)法直接與192.168.0.3:8888通信了。
我們的設(shè)備經(jīng)常是處在NAT設(shè)備的后面, 比如在大學(xué)里的校園網(wǎng), 查一下自己分配到的IP, 其實(shí)是內(nèi)網(wǎng)IP, 表明我們?cè)贜AT設(shè)備后面, 如果我們?cè)趯嬍以俳觽€(gè)路由器, 那么我們發(fā)出的數(shù)據(jù)包會(huì)多經(jīng)過(guò)一次NAT.
2.NAT的副作用以及解決方案
國(guó)內(nèi)移動(dòng)無(wú)線網(wǎng)絡(luò)運(yùn)營(yíng)商在鏈路上一段時(shí)間內(nèi)沒(méi)有數(shù)據(jù)通訊后, 會(huì)淘汰NAT表中的對(duì)應(yīng)項(xiàng), 造成鏈路中斷敌蚜。這是NAT帶來(lái)的第一個(gè)副作用:NAT超時(shí)桥滨。而國(guó)內(nèi)的運(yùn)營(yíng)商一般NAT超時(shí)的時(shí)間為5分鐘,所以通常我們TCP長(zhǎng)連接的心跳設(shè)置的時(shí)間間隔為3-5分鐘弛车。
而第二個(gè)副作用就是:我們這邊文章要提到的NAT墻齐媒。NAT會(huì)有一個(gè)機(jī)制,所有外界對(duì)內(nèi)網(wǎng)的請(qǐng)求纷跛,到達(dá)NAT的時(shí)候喻括,都會(huì)被NAT所丟棄,這樣如果我們處于一個(gè)NAT設(shè)備后面贫奠,我們將無(wú)法得到任何外界的數(shù)據(jù)唬血。
但是這種機(jī)制有一個(gè)解決方案:就是如果我們A主動(dòng)往B發(fā)送一條信息,這樣A就在自己的NAT上打了一個(gè)B的洞唤崭。這樣A的這條消息到達(dá)B的NAT的時(shí)候拷恨,雖然被丟掉了,但是如果B這個(gè)時(shí)候在給A發(fā)信息浩姥,到達(dá)A的NAT的時(shí)候挑随,就可以從A之前打的那個(gè)洞中,發(fā)送給到A手上了勒叠。
簡(jiǎn)單來(lái)講兜挨,就是如果A和B要進(jìn)行通信,那么得事先A發(fā)一條信息給B眯分,B發(fā)一條信息給A拌汇。這樣提前在各自的NAT上打了對(duì)方的洞,這樣下一次A和B之間就可以進(jìn)行通信了弊决。
3.四種NAT類(lèi)型:
RFC3489 中將 NAT 的實(shí)現(xiàn)分為四大類(lèi):
- Full Cone NAT (完全錐形 NAT)
- Restricted Cone NAT (限制錐形 NAT 噪舀,可以理解為 IP 限制,Port不限制)
- Port Restricted Cone NAT (端口限制錐形 NAT飘诗,IP+Port 限制)
- Symmetric NAT (對(duì)稱(chēng) NAT)
其中完全最上層的完全錐形NAT的穿透性最好与倡,而最下層的對(duì)稱(chēng)形NAT的安全性最高。
簡(jiǎn)單來(lái)講講這4種類(lèi)型的NAT代表什么:
- 如果一個(gè)NAT是Full Cone NAT昆稿,那么無(wú)論什么IP地址訪問(wèn)纺座,都不會(huì)被NAT墻掉(這種基本很少)。
- Restricted Cone NAT溉潭,僅僅是經(jīng)過(guò)打洞的IP能穿越NAT净响,但是不限于Port少欺。
- Port Restricted Cone NAT,僅僅是經(jīng)過(guò)打洞的IP+端口號(hào)能穿越NAT馋贤。
- Symmetric NAT 這種也是僅僅是經(jīng)過(guò)打洞的IP+端口號(hào)能穿越NAT赞别,但是它有一個(gè)最大的和Cone類(lèi)型的NAT的區(qū)別,它對(duì)外的公網(wǎng)Port是不停的變化的:比如A是一個(gè)對(duì)稱(chēng)NAT配乓,那么A給B發(fā)信息仿滔,經(jīng)過(guò)NAT映射到一個(gè)Port:10000,A給C發(fā)信息扰付,經(jīng)過(guò)NAT映射到一個(gè)Port:10001堤撵,這樣會(huì)導(dǎo)致一個(gè)問(wèn)題仁讨,我們服務(wù)器根本無(wú)法協(xié)調(diào)進(jìn)行NAT打洞羽莺。
至于為什么無(wú)法協(xié)調(diào)打洞,下面我們會(huì)從STUN和TURN的工作原理來(lái)講洞豁。
4.STUN Server主要做了兩件事
- 接受客戶端的請(qǐng)求盐固,并且把客戶端的公網(wǎng)IP、Port封裝到ICE Candidate中丈挟。
- 通過(guò)一個(gè)復(fù)雜的機(jī)制刁卜,得到客戶端的NAT類(lèi)型。
完成了這些STUN Server就會(huì)這些基本信息發(fā)送回客戶端曙咽,然后根據(jù)NAT類(lèi)型蛔趴,來(lái)判斷是否需要TURN服務(wù)器協(xié)調(diào)進(jìn)行下一步工作。
我們來(lái)講講這兩步具體做了什么吧:
第一件事就不用說(shuō)了例朱,其實(shí)就是得到客戶端的請(qǐng)求孝情,把源IP和Port拿到,添加到ICE Candidate中洒嗤。
來(lái)講講第二件事箫荡,STUN是如何判斷NAT的類(lèi)型的:
假設(shè)B是客戶端,C是STUN服務(wù)器渔隶,C有兩個(gè)IP分別為IP1和IP2(至于為什么要兩個(gè)IP羔挡,接著往下看):
STEP1.判斷客戶端是否在NAT后:
B向C的IP1的pot1端口發(fā)送一個(gè)UDP 包。C收到這個(gè)包后间唉,會(huì)把它收到包的源IP和port寫(xiě)到UDP包中绞灼,然后把此包通過(guò)IP1和port1發(fā)還給B。這個(gè)IP和port也就是NAT的外網(wǎng) IP和port(如果你不理解呈野,那么請(qǐng)你去看我的BLOG里面的NAT的原理和分類(lèi))低矮,也就是說(shuō)你在STEP1中就得到了NAT的外網(wǎng)IP。
熟悉NAT工作原理的朋友可以知道际跪,C返回給B的這個(gè)UDP包B一定收到商佛。如果在你的應(yīng)用中喉钢,向一個(gè)STUN服務(wù)器發(fā)送數(shù)據(jù)包后,你沒(méi)有收到STUN的任何回應(yīng)包良姆,那只有兩種可能:1肠虽、STUN服務(wù)器不存在,或者你弄錯(cuò)了port玛追。2税课、你的NAT拒絕一切UDP包從外部向內(nèi)部通過(guò)。
當(dāng)B收到此UDP后痊剖,把此UDP中的IP和自己的IP做比較韩玩,如果是一樣的,就說(shuō)明自己是在公網(wǎng)陆馁,下步NAT將去探測(cè)防火墻類(lèi)型找颓,我不想多說(shuō)。如果不一樣叮贩,說(shuō)明有NAT的存在击狮,系統(tǒng)進(jìn)行STEP2的操作。
STEP2.判斷是否處于Full Cone Nat下:
B向C的IP1發(fā)送一個(gè)UDP包益老,請(qǐng)求C通過(guò)另外一個(gè)IP2和PORT(不同與SETP1的IP1)向B返回一個(gè)UDP數(shù)據(jù)包(現(xiàn)在知道為什么C要有兩個(gè)IP了吧彪蓬,雖然還不理解為什么,呵呵)捺萌。
我們來(lái)分析一下档冬,如果B收到了這個(gè)數(shù)據(jù)包,那說(shuō)明什么桃纯?說(shuō)明NAT來(lái)著不拒酷誓,不對(duì)數(shù)據(jù)包進(jìn)行任何過(guò)濾,這也就是STUN標(biāo)準(zhǔn)中的full cone NAT慈参。遺憾的是呛牲,F(xiàn)ull Cone Nat太少了,這也意味著你能收到這個(gè)數(shù)據(jù)包的可能性不大驮配。如果沒(méi)收到娘扩,那么系統(tǒng)進(jìn)行STEP3的操作。
STEP3.判斷是否處于對(duì)稱(chēng)NAT下:
B向C的IP2的port2發(fā)送一個(gè)數(shù)據(jù)包壮锻,C收到數(shù)據(jù)包后琐旁,把它收到包的源IP和port寫(xiě)到UDP包中,然后通過(guò)自己的IP2和port2把此包發(fā)還給B猜绣。
和step1一樣灰殴,B肯定能收到這個(gè)回應(yīng)UDP包。此包中的port是我們最關(guān)心的數(shù)據(jù)掰邢,下面我們來(lái)分析:
如果這個(gè)port和step1中的port一樣牺陶,那么可以肯定這個(gè)NAT是個(gè)CONE NAT伟阔,否則是對(duì)稱(chēng)NAT。道理很簡(jiǎn)單:根據(jù)對(duì)稱(chēng)NAT的規(guī)則掰伸,當(dāng)目的地址的IP和port有任何一個(gè)改變皱炉,那么NAT都會(huì)重新分配一個(gè)port使用,而在step3中狮鸭,和step1對(duì)應(yīng)合搅,我們改變了IP和port。因此歧蕉,如果是對(duì)稱(chēng)NAT,那這兩個(gè)port肯定是不同的灾部。
如果在你的應(yīng)用中,到此步的時(shí)候PORT是不同的惯退,那么這個(gè)它就是處在一個(gè)對(duì)稱(chēng)NAT下了赌髓。如果相同,那么只剩下了restrict cone 和port restrict cone蒸痹。系統(tǒng)用step4探測(cè)是是那一種春弥。
STEP4.判斷是處于Restrict Cone NAT還是Port Restrict NAT之下:
B向C的IP2的一個(gè)端口PD發(fā)送一個(gè)數(shù)據(jù)請(qǐng)求包,要求C用IP2和不同于PD的port返回一個(gè)數(shù)據(jù)包給B叠荠。
我們來(lái)分析結(jié)果:如果B收到了,那也就意味著只要IP相同扫责,即使port不同榛鼎,NAT也允許UDP包通過(guò)。顯然這是Restrict Cone NAT鳖孤。如果沒(méi)收到者娱,沒(méi)別的好說(shuō),Port Restrict NAT.
到這里STUN Server一共通過(guò)這4步苏揣,判斷出客戶端處于什么類(lèi)型的NAT下黄鳍,然后去做后續(xù)的處理:
這4步都會(huì)返回給客戶端它的公網(wǎng)IP、Port和NAT類(lèi)型平匈,除此之外:
(a)如果A處于公網(wǎng)或者Full Cone Nat下框沟,STUN不做其他的了,因?yàn)槠渌蛻舳丝梢灾苯雍虯進(jìn)行通信增炭。
(b)如果A處于Restrict Cone或者Port Restrict NAT下忍燥,STUN還會(huì)協(xié)調(diào)TURN進(jìn)行NAT打洞。
(c)如果A處于對(duì)稱(chēng)NAT下隙姿,那么點(diǎn)對(duì)點(diǎn)連接下梅垄,NAT是無(wú)法進(jìn)行打洞的。所以為了通信输玷,只能采取最后的手段了队丝,就是轉(zhuǎn)成C/S架構(gòu)了靡馁,STUN會(huì)協(xié)調(diào)TURN進(jìn)行消息轉(zhuǎn)發(fā)。
5.TURN Server也主要做了兩件事:
為NAT打洞:如果A和B要互相通信机久,那么TURN Server奈嘿,會(huì)命令A(yù)和B互相發(fā)一條信息,這樣各自的NAT就留下了對(duì)方的洞吞加,下次他們就可以之間進(jìn)行通信了裙犹。
為對(duì)稱(chēng)NAT提供消息轉(zhuǎn)發(fā):當(dāng)A或者B其中一方是對(duì)稱(chēng)NAT時(shí),那么給這一方發(fā)信息衔憨,就只能通過(guò)TURN Server來(lái)轉(zhuǎn)發(fā)了叶圃。
6.為什么對(duì)稱(chēng)NAT無(wú)法打洞:
假如A、B進(jìn)行通信践图,而B(niǎo)處于對(duì)稱(chēng)NAT之下掺冠,那么A與B通信,STUN拿到A码党,B的公網(wǎng)地址和端口號(hào)都為10000德崭,然后去協(xié)調(diào)TURN打洞,那么TURN去命令A(yù)發(fā)信息給B揖盘,則A就在NAT打了個(gè)B的洞眉厨,但是這個(gè)B的洞是端口號(hào)為10000的洞,但是下次B如果給A發(fā)信息兽狭,因?yàn)锽是對(duì)稱(chēng)NAT憾股,它給每個(gè)新的IP發(fā)送信息時(shí),都重新對(duì)應(yīng)一個(gè)公網(wǎng)端口箕慧,所以給A發(fā)送請(qǐng)求可能是公網(wǎng)10001端口服球,但是A只有B的10000端口被打洞過(guò),所以B的請(qǐng)求就被丟棄了颠焦。
顯然Server是無(wú)法協(xié)調(diào)客戶端打洞的斩熊,因?yàn)閰f(xié)調(diào)客戶端打得洞僅僅是上次對(duì)端為Server發(fā)送端口的洞,并不適用于另一個(gè)請(qǐng)求伐庭。
最后的最后再補(bǔ)充一點(diǎn)粉渠,就是NAT打的洞也是具有時(shí)效性的,如果NAT超時(shí)了似忧,那么還是需要重新打洞的渣叛。
五、墊片-兼容性
一開(kāi)始各個(gè)瀏覽器廠商盯捌,都會(huì)實(shí)現(xiàn)自己的一套API淳衙,諸如webkitRTCPeerConnection
和mozRTCPeerConnection
這樣的差異,對(duì)于前端開(kāi)發(fā)者當(dāng)然是苦不堪言。而adapter.js正是為了消除這種差異箫攀,幫助我們可以按照規(guī)范來(lái)寫(xiě)我們的WebRTC代碼肠牲。可以參考 https://github.com/webrtcHacks/adapter靴跛。