WebRTC點對點通訊架構設計

NO IMAGE

這是我在公司內部的一次分享,想要讓小夥伴對WebRTC都有所瞭解,並且可以上手去做一個基於webrtc的應用。雖然幾乎所有人都知道,webrtc是一個瀏覽器端內置的點對點接口,甚至是準標準了。但是,到底怎麼利用這一個已經不是新特性,但是很不幸的是,不少人對這東西還是隻停留在聽說過,怎麼才能使用它呢?怎麼利用webrtc作出一個我們想要的p2p應用呢?這篇文章結合我的分享,再加一些補充,把關於webrtc入門的東西講清楚。

什麼是WebRTC?

到底什麼是WebRTC?其實這個問題並沒有三兩句那麼清除,要解釋很多詞。我總結起來,只能用一些側面的,但是容易理解的內容進行解釋:

  • 全稱為Web Real-Time Communications,即web實時通訊
  • Peer-to-peer,點對點
  • to capture and optionally stream audio and/or video media, as well as to exchange arbitrary data between browsers 抓取用戶的視頻或音頻流,也可以傳輸任意數據類型,在瀏覽器之間(between是兩個之間的意思,所以是說webrtc僅是兩個之間的事,沒法整3個之間的事)。另外,還要注意“瀏覽器”這個點,webrtc是瀏覽器內置的,跟很多其他瀏覽器自帶的接口,例如websql一樣。但是實際上,webrtc的接口完全獨立出來,所以也不一定非得在瀏覽器環境下,目前node、react-native都有對應的包使得它們支持使用webrtc。
  • without requiring an intermediary 不需要中間就可以傳輸(忽悠吧)
  • without requiring that the user install plug-ins or any other third-party software 不需要插件或第三方軟件

這是從MDN上面抄來的解釋,這裡面有個坑,就是,webrtc的初衷,是為了解決點對點的媒體傳輸問題,從這個點考慮,視頻通話這樣的場景是最適合的,沒有之一。但是,我們還想把這個事情整深入一點。不過,在這之前,我們必須瞭解,作為開發者,怎麼一行一行,把這些接口都使用起來。

歷史和現狀

作為一篇完整的文章,還是需要有一些廢話把webrtc的前世今生講一下。講到點對點媒體通訊技術,不得不講到一家公司。2011年的時候,Google 收購了GIPS,它是一個為RTC開發出許多組件的一個公司,例如編解碼和回聲消除技術。Google收購了它才一年,就在2012年開源了GIPS開發的技術,開源的時候,就以WebRTC作為開源技術的名稱,並開始積極與相關機構IET和W3C制定行業標準。

GIPS早就被很多公司購買使用,例如QQ(如圖)

WebRTC點對點通訊架構設計

可以說這家公司開發了從早期開始,點對點媒體通訊領域最可靠的技術,被全球各家公司使用。因此,它們的技術不可言喻的屬於頂級。谷歌拿到它們的技術之後,就把它們開源了,可以說對於其他開發廠商而言真的是福音中的福音。而這個被開源出來的東西,就叫webrtc。

目前,webrtc已經得到了多個瀏覽器的支持,主要是chrome、firefox、opera,但是ie和safari還不完全支持。如果想要做一款基於webrtc的應用,就必須在你的客戶端裡面去使用支持webrtc的瀏覽器內核。不過幸運的是,node和react-native都已經有人做了包,可以實現將webrtc集成到應用中,這樣,基於electron和react-native的點對點應用就顯得非常容易了。

WebRTC的API

webrtc給了我們三個主要的api接口,我們可以利用這三個接口創建完整的媒體傳輸,甚至是任意數據的傳輸通道。

  1. MediaStream (getUserMedia)
  2. RTCPeerConnection
  3. RTCDataChannel

MediaStream(getUserMedia)

MediaStream是獲取用戶媒體輸入信息的接口,比如設備的攝像頭輸入、麥克風輸入等,將來可能還支持其他類型的設備輸入,不過目前而言,主要就是這兩個。在獲取到這些輸入之後,它以“流”(stream)的形式返回給程序代碼使用,而“流”又由“軌”組成,比如音軌和視軌。獲得這些流之後,直接把它塞到html裡面的一個video或audio標籤上,就可以看到或聽到輸入的內容了。傳送給peer連接的另外一端時,也是要把流傳過去,不過現在已經改成了傳軌。

我們用代碼來實現:

navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
}).then((stream) => {
let video = document.querySelector('#video')
video.srcObject = stream
video.onloadedmetadata = () => video.play()
})

在html裡面放一個video#video,就可以把攝像頭和麥克風的stream塞給它,看到自己的影像了。

RTCPeerConnection

這個接口主要用於創建一個peer實例,得到這個實例之後,利用這個實例的各種方法,創建出真實的peer to peer連接。這個過程裡面需要了解STUN、TURN協議,ICE框架,Signaling服務,SDP等知識,這我會在下文講。

創建peer實例

let peer = new RTCPeerConnection(servers)

聽上去,p2p挺方便的,但是並不是一個簡單的創建過程。要建立一個peer-to-peer連接,可沒想的那麼容易,用一個new就可以建立?不可能的。要建立一個真正的peer connection,需要用實例化出來的peer的方法進行一系列的操作。

交換身份

peer.addIceCandidate(candidate)

這個用來把要建立連接的對方的網絡信息加入自己的本地。什麼是對方的網絡信息呢?就是它的網絡唯一識別地址,如果是普通的網絡環境,我們用ip地址就可以標記它。

但是,實際上的網絡環境往往會是,client會隱藏在NAT網絡背後。因此,要有一種方法,從這種複雜的網絡環境下,得到對方peer的識別信息。怎麼整呢?這個時候,就要用到STUN協議,這個協議的作用,就是要從NAT網絡中,找出另一端在網絡中可以被正常訪問到的網絡路徑。

可是,在一些極端情況下,STUN也無法搞定,某些網絡設備屏蔽了STUN的識別能力。在這種情況下,只能採用另外一種辦法來解決兩個peer之間的數據傳輸了,就是採用TURN協議,實現一個媒體中轉服務。所謂媒體中轉,其實就是先把視頻發送到服務器上面,再由另外一個peer把它下載下來。

上面這套方案被webrtc內置了,它採用ICE框架來實現這套方案,作為開發者,要做的是,告訴程序,你的STUN服務器信息和TURN服務器地址和認證信息。也就是說,作為產品級架構,需要自己搭建STUN和TURN服務器。

怎麼把這些服務器信息傳給webrtc呢?就是在new RTCPeerConnection的時候,作為參數傳進去。

let peer = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
{
url: 'turn:[email protected]<turn_server_ip_address>',
credential: 'my_password',
},
],
})

這樣就可以讓程序使用你自己的stun & turn服務器了。

但是,怎麼最終把candidate身份信息傳給對方呢?單憑webrtc是無法做到的,我們需要藉助一個服務器來實現,這個就是signaling服務器。這個signaling服務器的作用,就是在利用peer的傳輸能力之前,建立連接階段,傳輸各自的身份信息、描述信息(下面會講)的。

不是說peer to peer是點對點通訊嗎?怎麼還要一個服務器呢?這也是沒辦法的,webrtc實現的時候,完全放開了上述信息交換的協議,因此開發者需要自己實現這塊。一般我們會使用一個websocket來實現這個signaling服務,當一個peer需要發送一個signaling的時候,發送一個socket消息到服務器,再由服務器發送一個socket消息給另外一個peer,這樣,它們就可以交換信息。

peer.onicecandidate = function(e) {
socket.emit('icecandidate', e.candidate) // socket是假設的一個websocket實例
}
socket.on('icecandidate', function(e) {
let data = e.message
peer.addIceCandidate(data)
})

在onicecandidate的回調函數裡面使用socket發送candidate,在另外一個peer裡面,通過soket的事件接收candidate,並且把candidate加入到自己本地。

交換描述信息

什麼是描述信息呢?大概就是一個設備的信息,當一個peer打算把自己的設備stream發送給另外一個peer使用的時候,需要將設備信息告訴給對方,比如視頻的編碼之類的。怎麼交換呢?peer需要通過一個offer和answer操作來實現。

let desc = await peer.createOffer()
await peer.setLocalDescription(desc)
socket.emit('offer', desc)
socket.on('offer', async function(e) {
let data = e.message
await peer.setRemoteDescription(data)
let desc = await peer.createAnswer()
peer.setLocalDescription(desc)
socket.emit('answer', desc)
})
socket.on('answer', async function(e) {
let data = e.message
await peer.setRemoteDescription(data)
})

上面這段代碼,實際上會在不同的情況下運行不同的部分。當它作為首先發出消息的一方時,它要發送一個offer給遠端的peer。而發送的內容,就是通過createOffer創建的description。這個description叫做Session Description Protocol,即很多文檔裡面說的SDP。當遠端peer發送了SDP之後,遠端的peer通過websocket接收到之後,用setRemoteDescription把信息塞到本地。

WebRTC點對點通訊架構設計

WebRTC signaling SDP

上圖裡面還反映了一個問題,那就是onicecandidate被觸發的時間。我原本以為,這個事件會在new完成之後,但事實上,它會在createOffer或者createAnswer的時候發生。

總之,完成上面的offer和answer之後,兩個peer就建立了連接,之後才能傳輸stream或者其他數據。

發送媒體流

通過前面的getUserMedia接口,我們已經可以拿到當前用戶的攝像頭、麥克風輸入了。怎麼把這些輸入發送給遠端的peer呢?這個時候就不需要藉助signaling服務器了,當上面的連接創建之後,只需要調用peer的對應方法,就可以做到了,這裡才是真正的點對點數據傳輸了。

function sendStream(stream) {
let tracks = stream.getTracks()
tracks.forEach((track) => {
peer.addTrack(track, stream)
})
}
peer.ontrack = function(e) {
let stream = e.streams[0]
// 把stream塞到video上
}

這樣就可以做到將自己的視頻流信息發送給遠端的peer,並觸發遠端peer的ontrack。當然,反過來也是一樣,對方也可以把自己的stream發送給自己,自己執行ontrack裡面的操作。

RTCDataChannel

在使用RTCPeerConnection創建了peer實例,並且創建了連接之後,就可以使用RTCDataChannel接口創建出一個數據傳輸通道,用來傳輸任意數據的信息。雖說官方給出的解釋是“任意數據”,但是在實際編碼中,傳輸的是字符串……

它的使用就簡單的多了:

let channel = peer.createDateChannel('a channel')

通過peer的createDataChannel方法創建一個channel,然後它擁有:

channel.onopen = function() {}
channel.onmessage = function(e) {
let data = e.data
}
channel.onerror = function() {}
channel.onclose = function() {}
channel.send(data)
channel.close()

這些方法,一看就知道是幹嘛用的,就不贅述了。

其實,使用RTCDataChannel接口的大多數場景,都是為了實現文件傳輸,特別是一些大文件傳輸。當兩臺處於同一個網絡裡面的電腦使用webrtc進行文件傳輸的時候,由於不用經過服務器,所以可以實現更高效的文件傳輸。但是,由於datachannel其實並不能直接發送二進制流,而是隻能發送文本(Firefox除外),所以沒辦法,我們還必須利用html5的特性把文件轉換為可轉碼文本,再進行分片,通過啟用多個peer(下文會解釋)把文件發送給另外一個客戶端,再由另外一個客戶端組裝文件。

不過在firefox裡面,就方便的多,注意,下面的代碼僅適用於Firefox:

document.querySelector('input[type=file]').onchange = function () {
var file = this.files[0];
dataChannel.send(file);
};
dataChannel.onmessage = function (event) {
var blob = event.data; // Firefox allows us send blobs directly
var reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onload = function (event) {
var fileDataURL = event.target.result; // it is Data URL...can be saved to disk
SaveToDisk(fileDataURL, 'fake fileName');
};
};

上面的代碼出自這裡

關於如何分片傳輸文件的方法,可以自己谷歌搜一下,方案也挺多的,選擇自己喜歡的一種即可。

基於WebRTC的P2P網絡

上一部分我們已經瞭解到了,如何用代碼去實現創建一個peer to peer的通訊。現在的問題是,我們如何利用webrtc技術,實現一套應用解決方案,真正把這套技術用到自己的產品裡面。要了解這套知識,我把它分為四個層面:

  • Level 1: Peer Instance
  • Level 2: Client (node)
  • Level 3: Network
  • Level 4: Complete Service

第一層:peer實例層面

這其實就是前面關於webrtc api的一整套知識。如何利用api接口,創建實例,並且使得兩個實例能夠創建連接,實現視頻、音頻甚至是任意類型數據的交換。

但是有一個點不知道你有沒有發現?

webrtc頂多在兩個peer之間建立連接,不能有第三個peer插足進來。我們看peer的方法就會發現,setLocalDescription, setRemoteDescription等方法,都僅是為了把peer分為local和remote兩個角色。這也就是說,peer to peer是指兩個peer實例之間的故事,而不是我們平時裡說的點對點(node to node),也可能是因為我們平日裡對“點對點”這個概念有所誤解。

WebRTC點對點通訊架構設計

webrtc peer to peer

既然一個連接僅能在兩個peer之間通信,那怎麼可能讓很多用戶使用這項技術來實現點對點傳輸呢?

第二層:client層面

我們平時說的“點對點”其實是指“節點對節點”,一個節點(node)是一個客戶端的架構設計,對於一個應用客戶端而言,你需要把它想象為一個容器。這個容器會與網絡中的其他節點進行p2p連接。但是前面已經說了,一個peer to peer只會包含一對peer實例,那麼怎麼構建多人網絡呢?那就是要在容器中放置多個peer實例,每一個實例與另外一個節點容器中的某個peer實例建立連接。

WebRTC點對點通訊架構設計

webrtc client

就像圖裡面顯示的一樣,一個客戶端,想和其他的客戶端建立連接,就new一個新的peer出來,用這個peer和對方建立連接。一個client裡面有多少個peer,取決於它想和多少個客戶端建立連接。

第三層:network層面

當一個一個的節點連接在一起,所有能夠相互通信的節點的集合,就是一個network。而對於一個應用而言,可能會出現多個network。這理解起來非常簡單,我們以聊天室為例子,一個聊天室裡面的所有人,都是一個節點,而整個聊天室就是一個網絡。但是,假如我們有兩個聊天室,那就會有兩個網絡。但是很顯然,這兩個聊天室可能存在相同的一個用戶(客戶端),而他之所以能在兩個不同的聊天室聊天,是因為他的客戶端起來了n個peer實例,每個實例跟不同的遠端peer連接。

WebRTC點對點通訊架構設計

webrtc network

簡單的說,一個client可能同時屬於多個network。

WebRTC點對點通訊架構設計

同一個client屬於不同的network

第四層:service層面

如何保證應用給用戶提供完整的可靠的點對點服務呢?比如迅雷下載、微信聊天等等。在上面3層我們已經做好了對peer的管理,也就是在client中創建多個peer,每一個peer完成自己的使命。但是,如何管理好用戶在不同network之間的連接和內容傳輸,需要客戶端、服務器通過嚴格的邏輯進行分發。這裡包括用戶的認證、權限的分配、組別劃分等等。因此,說一個webrtc應用無法離開服務端,也是沒有錯的。

WebRTC點對點通訊架構設計

webrtc service

通過更為複雜的網絡架構,可以提高你不同地域、不同網絡間的性能或者實現特別的功能。總之,在基於前面的技術基礎上,你可以在任何一個環節進行變化,以適應實際的需求。

然而,你有沒有發現一個更嚴重的問題?如果P2P網絡依賴於服務端,那麼倘若服務端發生故障,也就會導致整個網絡癱瘓。有沒有一種可能使得提供服務的能力,也通過p2p網絡來實現呢?其實,我們有一種方案,就是將stun、turn、signaling服務內置於客戶端內部,當用戶打開客戶端的時候,也起一個本地的服務器。這樣,只要當兩個客戶端可以相互訪問時,就可以不在依靠一箇中心化的服務器了。

WebRTC點對點通訊架構設計

去中心化的webrtc應用架構示意圖

不過這裡面有一點需要注意,就是兩個客戶端可以相互訪問對方起的服務器。做到這一點其實不難,對於同一局域網下面客戶端,一般都是可以相互訪問的,但是,即使處於局域網下面的客戶端不能被訪問,整個網絡中,只要節點數量足夠多,也一定存在於公網,能夠被任何客戶端訪問的節點,這樣它就可以作為一個對其他可以訪問他的節點的signaling服務器了。當然,如果要作為turn服務器,感覺還是不是很好,一方面是安全性受到質疑,另一方面是消耗的資源比較多,如果幾千個節點同時連到它,那它估計馬上就掛了。

但是無論如何,這都給了我們想象的空間。這種架構下面,利用webrtc做一款區塊鏈應用也是非常容易的。怎麼做到呢?我們可以使用electron來做,它既提供了web的能力,又提供了node的能力,因此是非常好的選擇。

小結

至此,有關如何利用webrtc這項技術來開發一個應用的知識就介紹完了。這篇文章僅僅介紹了技術層面,如何把基於webrtc的通信搭起來,但是有關webrtc的東西其實還有挺多可以探討,例如:

  • 如何實現視頻通話
  • 如何創建多人視頻會議
  • 如何實現文件傳輸與分享
  • 如何實現一個區塊鏈
  • 用戶認證的細節是什麼

另外,也有一些遺留問題有待深入探討,例如:

  • 如何保證安全問題
  • 性能如何,最大支持創建多少個peer 實例
  • 網絡差的情況下,如何保證連接

這些問題都有待你深入瞭解,如果你對webrtc感興趣,或者對本文的一些闡述有自己的看法,可以在下方的留言框給我留言,一起探討。

文章發佈在我的博客 www.tangshuang.net/5493.html 如果有疑問或不足,請移步反饋。

相關文章

TySheMo前端數據管理模型

連續同源異步操作隊列

JS超級對象Objext:響應式、版本控制、數據鎖

HelloType:JS運行時數據類型檢查工具