http://xiaol.me/2014/08/24/webrtc-stun-turn-signaling/javascript
原文:WebRTC in the real world: STUN, TURN and signaling By Sam Duttonphp
WebRTC 實現了網頁點對點交流。
可是…
WebRTC 仍然須要服務器來:css
本文將向你展現如何創建一個信令服務器,並使用STUN和TURN服務器來處理實際應用中出現的一些怪異的鏈接問題。也將解釋WebRTC應用是如何處理多方通信並與相似VoIP、PSTN的服務互動的。html
若是你沒有了解過WebRTC,我強烈建議你在看這篇文章以前先看看這篇文章 Getting Started With WebRTC
html5
信令即協調通信的過程。WebRTC應用要發起一個對話,客戶端就須要交換以下信息:java
這個信令過程須要客戶端之間能來回傳遞消息,可是WebRTC APIs並無提供這種機制的實現,你須要本身建立。下面將描述創建信令服務器的幾種方式。無論怎麼樣,先來點上下文吧…node
爲了不冗餘,以及作到與現有技術的最大兼容,信令方法和協議都不禁WebRTC標準來指定。這些都由JSEP(JavaScript Session Establishment Protocol)來概述.python
WebRTC呼叫創建背後的想法已是徹底指定和控制媒體連接,可是儘可能託管和應用間的信令鏈接。
由是不一樣的應用可能會喜歡用不一樣的協議,好比已存在的SIP、Jungle信令協議,或者也許爲了一些新奇的用例而作的特殊應用而自定義的協議。
這一節文字要傳達的關鍵信息點是多媒體會話的描述,這個描述指定了必要的傳輸和創建媒體連接所必要的媒體配置信息。git
JSEP的架構也避免了讓瀏覽器去保存狀態,那就是,像一個信令狀態機同樣工做。這裏也許會有一個問題,好比,當頁面被刷新時,信令數據會丟失。不過,也能夠把這些信令狀態存在服務器。github
JSEP須要offer和answer之間作出以前提到的媒體元數據的信息交換。offer和answer經過Session Description Protocol(SDP)格式來溝通,以下
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
v=0 o=- 7614219274584779017 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE audio video a=msid-semantic: WMS m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 c=IN IP4 0.0.0.0 a=rtcp:1 IN IP4 0.0.0.0 a=ice-ufrag:W2TGCZw2NZHuwlnf a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=mid:audio a=rtcp-mux a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe a=rtpmap:111 opus/48000/2 … |
想知道SDP的格式的全部明確含義,能夠看看這個IETF examples.
請記住WebRTC被設計使得offer或answer能夠在被擰在一塊兒以前經過編輯SDP文原本設置好本地或遠程描述。好比apprtc.appspot.com中的preferAudioCodec()
方法就被用於設置默認的編解碼器和比特率。SDP用JavaScript來操做是有點痛苦,因此如今有個討論是關於WebRTC的將來版本是否能夠用JSON格式來替代,不過這裏提到了一些堅持使用SDP的好處。
RTCPeerConnection接口被WebRTC應用用於建立各點之間的鏈接並交流視音頻信息。
要開始這個過程RTCPeerConnection須要先作兩個工做:
當本地信息被確認後,就會經過信令系統與遠程終端進行交換。
聯想下alice is trying to call Eve這幅漫畫,發起/響應機制在其中完整的展示出來:
Alice和Eve還須要交換網絡信息。’finding candidates’就是經過ICE框架找到網絡連接和端口的過程。
下面是一個簡略的信令過程W3C代碼示例。這片代碼假設已經存在一些信令機制,如SignalingChannel. 下面討論信令的一些詳細細節。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
var signalingChannel = new SignalingChannel(); var configuration = { 'iceServers': [{ 'url': 'stun:stun.example.org' }] }; var pc; // call start() to initiate function start() { pc = new RTCPeerConnection(configuration); // send any ice candidates to the other peer pc.onicecandidate = function (evt) { if (evt.candidate) signalingChannel.send(JSON.stringify({ 'candidate': evt.candidate })); }; // let the 'negotiationneeded' event trigger offer generation pc.onnegotiationneeded = function () { pc.createOffer(localDescCreated, logError); } // once remote stream arrives, show it in the remote video element pc.onaddstream = function (evt) { remoteView.src = URL.createObjectURL(evt.stream); }; // get a local stream, show it in a self-view and add it to be sent navigator.getUserMedia({ 'audio': true, 'video': true }, function (stream) { selfView.src = URL.createObjectURL(stream); pc.addStream(stream); }, logError); } function localDescCreated(desc) { pc.setLocalDescription(desc, function () { signalingChannel.send(JSON.stringify({ 'sdp': pc.localDescription })); }, logError); } signalingChannel.onmessage = function (evt) { if (!pc) start(); var message = JSON.parse(evt.data); if (message.sdp) pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () { // if we received an offer, we need to answer if (pc.remoteDescription.type == 'offer') pc.createAnswer(localDescCreated, logError); }, logError); else pc.addIceCandidate(new RTCIceCandidate(message.candidate)); }; function logError(error) { log(error.name + ': ' + error.message); } |
要知道這片代碼中offer/answer和candidate交換過程是如何運做的,能夠看看simpl.info/pc 中視頻聊天示例的控制檯記錄。若是你須要跟多細節,能夠下載完整的WebRTC信令轉儲,並經過Chrome的 chrome://webrtc-internals 或Opera的 opera://webrtc-internals 頁面來統計。
要說清楚’我怎麼才能找到某人來聊天’挺複雜的。
對於電話來講,咱們有電話號碼目錄。對於在線視頻聊天,咱們須要身份認證以及在線狀態管理系統,即用戶初始化會話。WebRTC應用須要一種方式來讓客戶端來互相標識他們是像建立一個聊天室仍是加入一個聊天。
WebRTC沒有提供終端目錄機制,因此咱們不會進入這一項。這個過程能夠簡單的經過郵件或信息分享一個URL,好比 talky.io、tawk.com 和 browsermeeting.com這些視頻聊天應用中,你邀請別人加入是經過跟他們分享你的自有連接。開發者Chris Ball建立了一個有趣的實驗serverless-webrtc讓WebRTC的參與者經過IM,email或者信鴿來交換元數據。
重申一下,信令協議及機制並不禁WebRTC標準定義。無論你選擇什麼,你都須要一箇中介服務器來交換客戶端之間的信令信息和應用數據。很惋惜,網頁應用並不能簡單的直接衝着英特網說’把我和個人朋友連起來!’.
還好信令信息很小,而且大多數只在一個呼叫的開始才須要交換.在對apprtc.appspot.com和samdutton-nodertc.jit.su的測試中咱們發現,一個視頻聊天會話中,信令服務器總共處理了30-45條消息,全部消息的總大小才10kb左右。
而且對帶寬的要求也較低,WebRTC信令服務器並不消耗太多cpu或內存,由於它們只須要作消息中轉,並保存少許的會話狀態數據(例如,有哪些客戶已經鏈接了)。
Tip!
信令機制能夠用來交換會話元數據,也能夠用來作應用數據通信。它就是一個消息服務器。
信令的消息服務須要是雙向的:客戶端發到服務器且服務器發到客戶端。雙向通信違反了HTTP協議的客戶/服務,請求/響應模型。不過一些hack,好比爲了將數據從服務器推送到網頁的長輪詢)已經出現不少年了。
最近,EventSource API已被普遍的應用了,他使得服務器經過HTTP發送數據到瀏覽器成爲可能。這裏有個簡單的demo。EventSource被設計成單向傳遞消息,可是它能夠和XHR結合構建成交換信令消息的服務器:一個從呼叫者開始傳遞消息,用XHR請求傳輸,經過EventSource推送到被呼叫者那去。
WebSocket是一個更天然的解決方案,被設計成全雙工的客戶端/服務器通信(消息能夠同時雙向傳輸)。一個將信令服務器用純WebSocket或服務器發送事件(EventSource)的型式構建的好處是後臺接口能夠由各類語言的通用框架公共託管包來實現,好比PHP,Python和Ruby。
大概四分之三的瀏覽器都支持WebSocket了,更重要的是,全部支持WebRTC的瀏覽器都支持WebSocket,無論是桌面端仍是移動端。全部鏈接都須要使用TLS,去保證不被截獲到未加密的信息,而且減小proxy traveral引發的問題。(須要更多WebSocket和proxy traversal相關的信息,能夠看看Ilya Grigorik的High Performance Browser Networking一書的WebRTC章節。Peter Lubber的WebSocket Cheat Sheet有更多關於WebSocket客戶端和服務器端的信息)。
apprtc.appspot.comWebRTC視頻聊天應用的信令是經過Google App Engine Channel API完成的,這個API用到了Comet)技術(長輪詢)去實現信令推送信息(這裏有一個App Engine爲支持WebSocket存在好久的bug,快去關注這個bug,給它投票別讓它沉了!)。這裏有一份這個應用的詳細代碼。
WebRTC客戶端經過ajax輪詢獲取服務器信息處理信令也是可行的,可是這致使太多冗餘的網絡請求,尤爲對於移動端客戶來講更是一個問題。甚至在一個會話創建以後,終端仍須要輪詢信令信息去查詢是否會話有變化或者會話是否被對方終止了。這個示例使用了該方法,但作了一些輪詢頻率的優化。
雖然信令服務器對於每一個客戶來講消耗的帶寬和CPU都較少,可是應用流行起來的話依然要處理不一樣地域的大量的數據,應對高併發。通訊量較高的WebRTC應用須要可以應對高負載。
這裏咱們不會討論細節,但仍有以下一些爲高容量,高性能信息能夠注意的點。
(開發者Phil Leggetter的Real-Time Web Technologies Guide提供了一個關於消息服務和代碼庫的總結性清單。)
如下的簡單網頁應用代碼使用到了基於Node上的Socket.io而創建的信令服務器。Socket.io的設計使創建信息交換服務器變得簡單,並且它尤爲適用於WebRTC信令服務器,由於它內置了’房間’的概念。這個例子不是爲產品級別的信令服務器設計的,可是它面向相對較小的用戶羣工做得很好。
Socket.io除了用WebSocket,還適配如下備用技術:Adobe Flash Socket, AJAX long polling, AJAX multipart streaming, Forever Iframe and JSONP polling. 它有多種後臺實現,可是它的Node版本應該是最著名的,咱們下面的例子就用的這個版本。
例子中沒有WebRTC,這裏只是展現網頁應用信令該如何設計。查看控制檯能夠看到客戶是如何加入一個房間且交換信息的。咱們的WebRTC codelab有如何將這個例子集成進完整的WebRTC視頻聊天應用的步驟。你能夠在codelab repo第五步下載這些代碼,也能夠在samdutton-nodertc.jit.su在線試試效果。
index.html的代碼以下:
1
2 3 4 5 6 7 8 9 10 |
<!DOCTYPE html> <html> <head> <title>WebRTC client</title> </head> <body> <script src='/socket.io/socket.io.js'></script> <script src='js/main.js'></script> </body> </html> |
html中引用的的javascript文件main.js代碼以下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var isInitiator; room = prompt('Enter room name:'); var socket = io.connect(); if (room !== '') { console.log('Joining room ' + room); socket.emit('create or join', room); } socket.on('full', function (room){ console.log('Room ' + room + ' is full'); }); socket.on('empty', function (room){ isInitiator = true; console.log('Room ' + room + ' is empty'); }); socket.on('join', function (room){ console.log('Making request to join room ' + room); console.log('You are the initiator!'); }); socket.on('log', function (array){ console.log.apply(console, array); }); |
完整的服務端應用代碼:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
var static = require('node-static'); var http = require('http'); var file = new(static.Server)(); var app = http.createServer(function (req, res) { file.serve(req, res); }).listen(2013); var io = require('socket.io').listen(app); io.sockets.on('connection', function (socket){ // convenience function to log server messages to the client function log(){ var array = ['>>> Message from server: ']; for (var i = 0; i < arguments.length; i++) { array.push(arguments[i]); } socket.emit('log', array); } socket.on('message', function (message) { log('Got message:', message); // for a real app, would be room only (not broadcast) socket.broadcast.emit('message', message); }); socket.on('create or join', function (room) { var numClients = io.sockets.clients(room).length; log('Room ' + room + ' has ' + numClients + ' client(s)'); log('Request to create or join room ' + room); if (numClients === 0){ socket.join(room); socket.emit('created', room); } else if (numClients === 1) { io.sockets.in(room).emit('join', room); socket.join(room); socket.emit('joined', room); } else { // max two clients socket.emit('full', room); } socket.emit('emit(): client ' + socket.id + ' joined room ' + room); socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room); }); }); |
(你並不須要知道這代碼中的node-static是啥,它只是讓服務器代碼簡單點。)
要在本地啓動這個應用,你須要安裝Node, socket.io和node-static。Node能夠直接在官網下載(安裝過程很簡單)。要安裝socket.io和node-static,在你的應用目錄終端運行Node包管理器(NPM)就好了.
1
2 |
npm install socket.io npm install node-static |
要運行應用,只須要在你應用目錄裏終端運行以下命令:
1
|
node server.js |
在你的瀏覽器中打開localhost:2013
。在新的標籤頁或窗口將localhost:2013
再打開一次。看看發生了什麼,檢查下Chrome或Opera的控制檯,你能夠用經過快捷鍵Command-Option-J
或Ctrl-Shift-J
來打開開發者工具DevTool。
無論你選擇什麼來實現你的信令,你的後臺和客戶端都至少至少須要提供一個和這個例子相似的服務。
信令服務器須要初始化一個WebRTC會話。
然而,當兩個終端間的鏈接創建後,RTCDataChannel理論上能夠看成信令通道。這個能夠減小信令的延遲而且減小信令服務器帶寬和cpu的消耗,由於這樣的信息是直接交流的。這裏咱們沒有demo,不過你們仍需留意。
setLocalDescription()
方法被調用前RTCPeerConnection都不會開始收集candidates,這是JSEP IRTF draft中要求的。addIceCandidate()
方法。若是你不想你本身來作信令服務器,這裏有提供一些WebRTC信令服務器,用的也是以前提到的Socket.io,並都集成了WebRTC客戶端JavaScript代碼庫。
…若是你壓根任何代碼都不想寫,這裏也有一些徹底商業化的WebRTC平臺如vLine,OpenTok,Asterisk.
須要指出來,Ericsson在WebRTC早期就已經用PHP在Apache上搭了個信令服務器。可是這個如今多少已經廢棄了,不過若是你在考慮作相似的事的話,這代碼仍是值得一看的。
Security is the art of makeing nothing happen.
—Salman Rushdie
加密在WebRTC組件中是強制的。
然而,信令機制並不禁WebRTC標準所定義,因此讓信令更安全就是你本身的事了。若是攻擊者試圖劫持信令, HTTPS和WSS(i.e TLS),能夠保證他們不會攔截到未加密的信息。你也要注意不要在其餘用同一個服務器的用戶能訪問到的地方廣播信令信息。
要保護WebRTC應用,在信令中使用TLS是絕對必要的。
對於信令元數據,WebRTC應用使用了中介服務器,可是對於會話創建後的真正媒體數據流,RTCPeerConnection試圖讓客戶終端直連:點對點鏈接。
簡單的狀況下,每一個WebRTC終端都有一個惟一的地址,可使得各終端都能互相直接通信。
{}
可是大多數設備都處於一層或多層NAT(網絡地址轉換器)以後,還有殺毒軟件的阻擋了一些端口或協議,又或者使用了代理或者防火牆。防火牆和NAT事實上可能在同一設備上,好比家庭無線路由器。
WebRTC應用可使用ICE框架來克服實際應用中複雜的網絡問題。要使用ICE的話,你的應用必須以下所述的在RTCPeerConnection中傳遞ICE服務器的URL。
ICE試圖找到鏈接端點的最佳路徑。它並行的查找全部可能性,而後選擇最有效率的一項。ICE首先用從設備操做系統和網卡上獲取的主機地址來嘗試鏈接,若是失敗了(好比設備處於NAT以後),ICE會使用從STUN 服務器獲取到的外部地址,若是仍然失敗,則交由TURN中繼服務器來鏈接。
換句話說:
每個TURN服務器都支持STUN,由於TURN就是在STUN服務器中內建了一箇中繼功能。ICE也能夠應付NAT複雜的設定:實際上,NAR’打洞’會有不止一個公共 IP : port 地址。STUN或TURN服務器的URL由WebRTC中RTCPeerConnection的第一個參數iceServers配置對象可選指定。apprtc.appspot.com中的值是這樣的:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{
'iceServers': [ { 'url': 'stun:stun.l.google.com:19302' }, { 'url': 'turn:192.158.29.39:3478?transport=udp', 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 'username': '28224511:1379330808' }, { 'url': 'turn:192.158.29.39:3478?transport=tcp', 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 'username': '28224511:1379330808' } ] } |
一旦RTCPeerConnection中有了這些信息,ICE的神奇就自動展示了: RTCPeerConnection使用ICE框架找到各端點間最合適的路徑,必要時選用STUN和TURN服務器。
NAT在本地私有網絡中爲設備提供了一個IP地址,可是這個地址並不能被外部識別。沒有一個公共地址的話,WebRTC終端是沒有辦法通訊的。要解決這個問題,WebRTC使用了STUN。
STUN服務器處於公網中並有個簡單任務:檢查請求(來自運行於NAT以後的應用)的IP:port 地址,而且將這個地址響應回去。換句話說,NAT後的應用使用STUN服務器來找到他的IP:port 公網地址。這個過程使得WebRTC終端能夠找到本身公共訪問方法,並經過信令機制將之發送給其餘終端,就能夠建立一個直連連接。(在實踐中,不一樣的NAT工做方式不一樣,並有可能有多層NAT,可是原理是同樣的。)
STUN服務器並無作太多東西,也不用記住不少東西,因此一個相對低規格的的STUN服務器能夠處理大量的請求。
根據webrtcstats.com的調查,大部分(86%)WebRTC請求均可以經過STUN成功的建立鏈接,雖然對處於防火牆或者配置複雜的NAT以後的終端要低一些。
RTCPeerConnection嘗試用UDP協議創建終端間的直連。若是失敗了,就嘗試TCP協議,仍是失敗的話,TURN 服務器就會用於作終端間的數據中繼。
重申一下:TURN用於中繼視頻音頻數據流,而不是信令數據!
TURN服務器有公共地址,因此他能夠被終端聯繫到,哪怕終端處於防火牆或者代理以後。TURN服務器有一個概念上簡單的工做—作數據流中繼—可是,不像STUN服務器,它天生須要消耗大量帶寬,也就是說,TURN服務器須要很強大。
這幅圖展示了TURN的運做,純STUN不能成功的話,各終端將使用TURN服務器。
Google運行了一個公用的STUN服務器用做測試,stun.l.google.com:19302
,apprtc.appspot.com用到了它。咱們建議使用rfc5766-turn-server看成產品用途的STUN/TURN服務,STUN/TURN服務器的源代碼能夠在code.google.com/p/rfc5766-turn-server 找到,這裏也提供了一些服務器安裝的相關信息連接。Amazon Web Services(AWS)也提供了WebRTC的虛擬機鏡像。
另外一個備選TURN服務器是restund,有源代碼,也能夠裝到在AWS上。下面是介紹如何將restund裝到Google Compute Engine上。
sudo apt-get install make
sudo apt-get install gcc
patch -p1 < restund-auth.patch
sudo make install
/etc
目錄./client IP:port
你也許會對Justin Uberti爲REST API for access to TURN Services提出的IETF標準感興趣。
很容易想到一個超越簡單的點對點媒體流用例:好比,同事間的視頻會議,或者一個有數百(萬)用戶的公共演講。
WebRTC應用可使用多RTCPeerConnection,讓各終端之間以網狀配置鏈接。這就是如talky.io這類應用所使用的方法,而且在少許終端的狀況下運行的很是良好。不過,CPU和帶寬都消耗很是多,尤爲是在移動終端上。
此外,WebRTC應用能夠按星狀拓撲結構來選擇一個終端分發數據流。在服務器運行一個WebRTC終端來做爲從新分配機制也是可行的(webrtc.org提供了一個簡單例子)。
從Chrome 31和Opera 18開始,RTCPeerConnection的MediaStream能夠看成另外一個RTCPeerConnection的輸入:這裏有個簡單演示simpl.info/rtcpeerconnection/multi, 這使得應用結構更靈活,由於它使網絡應用經過選擇其餘終端的鏈接來處理路由成爲可能。
對於大量終端的更好選擇是使用Multipoint Control Unit(MCU).這是一個服務器,像大量參與者之間的橋樑同樣用於分發媒體信息。MCU能夠在一個視頻會議中使用多種分辨率,編解碼器和幀率,處理轉換編碼,選擇數據流徑,調製或錄製視頻音頻。對於多方通話,有一堆問題須要注意: 特別是,如何顯示多視頻輸入和混調多源音頻。雲平臺如vLine有嘗試優化流量路徑。
能夠考慮買一個MCU的硬件,或者本身作一個。
有很多能用的開源MCU軟件供選擇。好比,Licode(以前叫Lynckia)就爲WebRTC作了一個開源MCU,OpenTok也有一個開源產品Mantis。
WebRTC的標準性質使得瀏覽器中運行的WebRTC應用能夠和運行其餘通訊平臺的設備或者平臺創建通信,好比電話或者視頻會議系統。
SIP是VoIP和視頻會議系統的信令協議。要使WebRTC網頁應用能和其餘如視頻會議系統的SIP客戶端通信,WebRTC須要一個代理服務器作中介信令。信令須要流過網關,可是一旦通訊已經創建起來,SRTP(視頻和音頻)就能夠點對點傳輸。
PSTN(Public Switched Telephone Network),公用電話交換網絡,是全部普通模擬電話的閉路交換網絡。要用WebRTC網頁應用打電話,流量必須通過PSTN網關。此外,WebRTC網頁應用須要用中介XMPP服務器來與Jingle)終端如即時通訊客戶端通信。
Jingle由Google開發來做爲XMPP擴展用於支持視頻和音頻信息:如今WebRTC的實現基於libjingleC++庫,這個Jingle的實現剛開始是爲Google Talk開發的。
一些應用,代碼庫和平臺利用WebRTC的能力來於外界通信,如:
sipML5的開發們也開發了webrtc2sip網關。Tethr and Tropo在筆記本上演示過一個救災通信框架, 使用OpenBTS cell讓電腦能經過WebRTC與一個特別的電話通信。無需電信就能打電話啦!
WebRTC codelab:一步一步介紹如何創建一個視頻文字聊天應用,使用了在Node中運行的Socket.io信令服務器。
2013 Google I/O 大會上由WebRTC技術組長Justin Uberti作的WebRTC報告。
Chris Wilson在SFHTML5上的報告:Introduction to WebRTC Apps
WebRTC Book提供了不少數據和信令路徑的詳細信息,包括了許多詳細的網絡拓撲圖。
WebRTC and Signaling: What Two Years Has Taught Us:TokBox的一篇博文告訴咱們爲何要把信令從WebRTC細則中單獨拎出來。
Ben Strong的報告A Practical Guide to Building WebRTC Apps提供了不少WebRTC拓撲和基礎。
Ilya Grigorik的High Performance Browser Networking一書中的WebRTC章節深刻描述了WebRTC結構,用例和性能。