WebRTC點對點通信架構設計

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

什麼是WebRTC?

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

  • 全稱爲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的初衷,是爲了解決點對點的媒體傳輸問題,從這個點考慮,視頻通話這樣的場景是最適合的,沒有之一。可是,咱們還想把這個事情整深刻一點。不過,在這以前,咱們必須瞭解,做爲開發者,怎麼一行一行,把這些接口都使用起來。node

歷史和現狀

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

GIPS早就被不少公司購買使用,例如QQ(如圖)web


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

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

WebRTC的API

webrtc給了咱們三個主要的api接口,咱們能夠利用這三個接口建立完整的媒體傳輸,甚至是任意數據的傳輸通道。react-native

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

MediaStream(getUserMedia)

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

咱們用代碼來實現:瀏覽器

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:my_username@<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 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 peer to peer

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

第二層:client層面

咱們平時說的「點對點」實際上是指「節點對節點」,一個節點(node)是一個客戶端的架構設計,對於一個應用客戶端而言,你須要把它想象爲一個容器。這個容器會與網絡中的其餘節點進行p2p鏈接。可是前面已經說了,一個peer to peer只會包含一對peer實例,那麼怎麼構建多人網絡呢?那就是要在容器中放置多個peer實例,每個實例與另一個節點容器中的某個peer實例創建鏈接。


webrtc client

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

第三層:network層面

當一個一個的節點鏈接在一塊兒,全部可以相互通訊的節點的集合,就是一個network。而對於一個應用而言,可能會出現多個network。這理解起來很是簡單,咱們以聊天室爲例子,一個聊天室裏面的全部人,都是一個節點,而整個聊天室就是一個網絡。可是,假如咱們有兩個聊天室,那就會有兩個網絡。可是很顯然,這兩個聊天室可能存在相同的一個用戶(客戶端),而他之因此能在兩個不一樣的聊天室聊天,是由於他的客戶端起來了n個peer實例,每一個實例跟不一樣的遠端peer鏈接。


webrtc network

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


同一個client屬於不一樣的network

第四層:service層面

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


webrtc service

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

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


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

不過這裏面有一點須要注意,就是兩個客戶端能夠相互訪問對方起的服務器。作到這一點其實不難,對於同一局域網下面客戶端,通常都是能夠相互訪問的,可是,即便處於局域網下面的客戶端不能被訪問,整個網絡中,只要節點數量足夠多,也必定存在於公網,可以被任何客戶端訪問的節點,這樣它就能夠做爲一個對其餘能夠訪問他的節點的signaling服務器了。固然,若是要做爲turn服務器,感受仍是不是很好,一方面是安全性受到質疑,另外一方面是消耗的資源比較多,若是幾千個節點同時連到它,那它估計立刻就掛了。

可是不管如何,這都給了咱們想象的空間。這種架構下面,利用webrtc作一款區塊鏈應用也是很是容易的。怎麼作到呢?咱們可使用electron來作,它既提供了web的能力,又提供了node的能力,所以是很是好的選擇。

小結

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

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

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

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

這些問題都有待你深刻了解,若是你對webrtc感興趣,或者對本文的一些闡述有本身的見解,能夠在下方的留言框給我留言,一塊兒探討。


文章發佈在個人博客 www.tangshuang.net/5493.html 若是有疑問或不足,請移步反饋。

相關文章
相關標籤/搜索