這是我在公司內部的一次分享,想要讓小夥伴對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已經得到了多個瀏覽器的支援,主要是chrome、firefox、opera,但是ie和safari還不完全支援。如果想要做一款基於webrtc的應用,就必須在你的客戶端裡面去使用支援webrtc的瀏覽器核心。不過幸運的是,node和react-native都已經有人做了包,可以實現將webrtc整合到應用中,這樣,基於electron和react-native的點對點應用就顯得非常容易了。
WebRTC的API
webrtc給了我們三個主要的api介面,我們可以利用這三個介面建立完整的媒體傳輸,甚至是任意資料的傳輸通道。
- MediaStream (getUserMedia)
- RTCPeerConnection
- 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: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 如果有疑問或不足,請移步反饋。