下面這篇介紹webrtc的文章不錯,我花了大半天翻譯了一下.
翻譯的時候不是逐字逐句的,而是按照自己的理解翻譯的,同時為了便於理解,也加入一些自己組織的語言.
本文主要介紹webrtc的信令,stun,turn,轉載請說明出處(博客園RTC.Blacker).
英文來自:http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/
WEBRTC支持點對點通訊,但是WEBRTC仍然需要服務端,因為:
1,為了協調通訊過程客戶端之間需要交換元數據,如一個客戶端找到另一個客戶端以及通知另一個客戶端開始通訊.
2,需要處理NAT或防火牆,這是公網上通訊首要處理的問題.
在這篇文章里我們將告訴您怎么創建一個信令服務,怎么處理現實世界中兩個客戶端的連接,以及怎么處理多方通話和怎么與VOIP,PSTN的交互.如果您不了解webrtc,建議您讀這篇文章前先看:http://www.html5rocks.com/en/tutorials/webrtc/basics/
什么是信令?
信令就是協調通訊的過程,為了建立一個webrtc的通訊過程,客戶端需要交換如下信息:
1,會話控制消息:用來開始和結束通話(即開始視頻,結束視頻這些操作指令)
2,處理錯誤的消息.
3,元數據:如各自的音視頻編解碼方式,帶寬.
4,網絡數據:對方的公網IP,端口,內網IP,端口.
5,......
信令處理過程需要客戶端能夠來回傳遞消息,這個過程在webrtc里面是沒有實現的,需要您自己創建,下面我們會告訴您怎么創建這樣一個過程.
為什么WEBRTC沒有定義信令處理?
為了避免重復定義和最大程度兼容現有技術,JSEP(JavaScript Session Establishment Protocol)上已有概述.
現有的SIP協議就可以較好地處理整個信令過程,另外不同的應用程序可能對信令處理有特別的要求,如我們做的很多項目信令處理都是自己寫的,很靈活.
其實只要你能滿足你自己的業務需求,信令處理你完全可以自己定義,實現起來也不難,就是客戶端和服務端怎么通訊而已,用得最廣的就是websocket了,后面會介紹.
如下是JSEP定義的客戶端通訊架構:
JSEP要求客戶端之間交換offer和answer:其實就是上面提到的元數據,他們是以SDP格式進行交換,格式如下:
1 v=0 2 o=- 7614219274584779017 2 IN IP4 127.0.0.1 3 s=- 4 t=0 0 5 a=group:BUNDLE audio video 6 a=msid-semantic: WMS 7 m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 8 c=IN IP4 0.0.0.0 9 a=rtcp:1 IN IP4 0.0.0.0 10 a=ice-ufrag:W2TGCZw2NZHuwlnf 11 a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW 12 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 13 a=mid:audio 14 a=rtcp-mux 15 a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe 16 a=rtpmap:111 opus/48000/2 17 …
如果您對SDP格式有興趣,可以參考:IETF examples
在webrtc架構里面調用setLocalDiscription,setRemoteDiscription前可通過編輯SDP里面的值來更改offer和anser.如apprtc.appspot.com 中得preferAudioCodec()能用來設置默認的音頻編碼和碼率,sdp用javascript修改起來可能有點痛苦,W3C組織有在討論通過jason方式來編輯,不過目前這種方式也有些優點(some advantages).
RTCPeerConnection + signaling: offer, answer and candidate
RTCPeerConnection就是webrtc應用程序用來創建客戶端連接和視頻通訊的API.為了初始化這個過程 RTCPeerConnection有兩個任務:
1,確定本地媒體條件,如分辨率,編解碼能力,這些需要在offer和answer中用到.
2,取到應用程序所在機器的網絡地址,即稱作candidates.
一旦上面這些東西確定了,他們將通過信令機制和遠端進行交換.
想象一下Alice呼叫Eve的過程( Alice is trying to call Eve.),下面就是完整offer/answer機制的細節:
1,Alice創建一個 RTCPeerConnection對象.
2,Alice創建一個offer(即SDP會話描述)通過RTCPeerConnection createOffer()方法.
3,Alice調用setLocalDescription()方法用他的offer.
4,Alice通過信令機制將他的offer發給Eve.
5,Eve調用setRemoteDescription()方式設置Alice的offer,因此他的RTCPeerConnection知道了Alice的設置.
6,Eve調用方法createAnswer(),然后會觸發一個callback,這個callback里面可以去到自己的answer.
7,Eve設置他自己的anser通過調用方法setLocalDescription().
8,Eve通過信令機制將他的anser發給Alice.
9,Alice設置Eve的anser通過方法setRemoteDescription().
另外Alice和Eve也需要交換網絡信息(即candidates),發現candidates參考了ICE framework.
1,Alice創建RTCPeerConnection對象時設置了onicecandidate handler.
2,hander被調用當candidates找到了的時候.
3,當Eve收到來自Alice的candidate消息的時候,他調用方法addIceCandidate(),添加candidate到遠端描述里面.
JSEP支持ICE Candidate Trickling,他允許呼叫方在offer初始化結束后提供candidates給被叫方.而被叫方開始建立呼叫和連接而不需要等到所有candidate到達.
Coding WebRTC for signaling
下面是一個W3C的例子(W3C code example)概括了一個完整的信令過程,他里面假設已經存在信令機制:SignalingChannel,信令在下面被詳細討論
1 var signalingChannel = new SignalingChannel(); 2 var configuration = { 3 'iceServers': [{ 4 'url': 'stun:stun.example.org'
5 }] 6 }; 7 var pc; 8
9 // call start() to initiate
10
11 function start() { 12 pc = new RTCPeerConnection(configuration); 13
14 // send any ice candidates to the other peer
15 pc.onicecandidate = function (evt) { 16 if (evt.candidate) 17 signalingChannel.send(JSON.stringify({ 18 'candidate': evt.candidate 19 })); 20 }; 21
22 // let the 'negotiationneeded' event trigger offer generation
23 pc.onnegotiationneeded = function () { 24 pc.createOffer(localDescCreated, logError); 25 } 26
27 // once remote stream arrives, show it in the remote video element
28 pc.onaddstream = function (evt) { 29 remoteView.src = URL.createObjectURL(evt.stream); 30 }; 31
32 // get a local stream, show it in a self-view and add it to be sent
33 navigator.getUserMedia({ 34 'audio': true, 35 'video': true
36 }, function (stream) { 37 selfView.src = URL.createObjectURL(stream); 38 pc.addStream(stream); 39 }, logError); 40 } 41
42 function localDescCreated(desc) { 43 pc.setLocalDescription(desc, function () { 44 signalingChannel.send(JSON.stringify({ 45 'sdp': pc.localDescription 46 })); 47 }, logError); 48 } 49
50 signalingChannel.onmessage = function (evt) { 51 if (!pc) 52 start(); 53
54 var message = JSON.parse(evt.data); 55 if (message.sdp) 56 pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () { 57 // if we received an offer, we need to answer
58 if (pc.remoteDescription.type == 'offer') 59 pc.createAnswer(localDescCreated, logError); 60 }, logError); 61 else
62 pc.addIceCandidate(new RTCIceCandidate(message.candidate)); 63 }; 64
65 function logError(error) { 66 log(error.name + ': ' + error.message); 67 }
了解offer,anser,candidate交換過程,可通過simpl.info/pc上視頻聊天的控制台日志,如果您想了解更多,可以下載完整的WebRTC signaling and stats from the chrome://webrtc-internals page in Chrome or the opera://webrtc-internals page in Opera.
怎么發現客戶端
這里有一種很簡單的表述方式---我怎么找到別人視頻?
打電話的時候我們有電話號碼和電話本,知道打給誰,QQ聊天的時候,我們可以通過通訊錄找到要聊天的人,webrtc也一樣,他的客戶端需要通過一種方式找到要聊天的人或要加入的會議.
webrtc沒有定義這樣一個發現過程,這個其實很簡單,可以參考 talky.io, tawk.com and browsermeeting.com,另外Chris Ball創建了serverless-webrtc,他可以通過Emai,IM來參與視頻.
怎么創建信令服務?
再次重申:webrtc沒有定義信令機制,因此無論你選擇什么機制你都的需要一台中間服務端,用來在客戶端之間交換數據,你總不可能直接說:"跟我朋友視頻?",
由於信令消息很小,大多數交互都是在開始通話之前,可以參考 apprtc.appspot.com and samdutton-nodertc.jit.su, 測試發現:一個視頻通話過程大概有35~40消息,數據量在10K左右,
所以相對來說信令服務器不怎么占帶寬,也不需要消耗多大的CPU和內存.
從服務端推送消息給客戶端
信令服務器推送消息需要時雙向的,即客戶端能發消息給服務器,服務器也能發消息給服務端,這種雙向機制就將Http給排除了(當然可以使用長連接,而且很多人都是這么做的,只不過比較占資源).
說到這里很多人會想到WebSocket,沒錯,這是一種很好的解決方案,而且后台實現框架也很多,如PHP,Python,Ruby.
大約3/4的瀏覽器支持webSocekt,更重要的是支持WEBRTC的瀏覽器都支持WebSocket,包括PC和手機, TLS應該被使用為了所有連接,他能確保為被加密的消息不被截獲,同時也能減少使用代理帶來的問題(reduce problems with proxy traversal),更多這方面的知識請參考 WebRTC chapter和WebSocket Cheat Sheet .
apprtc.appspot.com中的視頻通訊使用的信令是 Google App Engine Channel API,他采用的是 Comet技術, HTML5 Rocks WebRTC article有詳細的介紹(detailed code walkthrough)
當然你也可以通過Ajax來實現這樣一個長連接,不過這樣會產生很多重復的網絡請求,而且應用在移動端會有很多問題.
擴展信令的實現
盡管信令服務占用的CPU和帶寬資源都比較少,但實際應用中如果要考慮到高並發,信令服務還是有很大負載的.這些我們不深入討論了,下面有一些不錯的選擇供參考:
1,eXtensible Messaging and Presence Protocol(XMPP):主要是用來給即時通訊用的,開源服務端包括ejabberd and Openfire. 客戶端包括 Strophe.js use BOSH(但因為 various reasons,BOSH沒有WebSocket高效),補充說明:Jingle是XMPP的擴展,支持音視頻,webrtc項目里面的network和transort組件就是來自 libjingle庫.
Developer Phil Leggetter's Real-Time Web Technologies Guide 提供了一個消息服務和庫的綜合清單.
使用Nodejs上的Socket.io實現一個信令服務
下面這個代碼是一個簡單的web應用,使用了 Socket.io on Node, socket.io的設計目標就是為了簡化消息通訊服務的創建,特別適合作為webrtc的信令,因為他內嵌了房間的概念,下面這個樣例設計主要是為了少量用戶的使用,並沒有考慮太多的擴展性.
下面代碼主要用來介紹怎么創建信令服務,可以通過查看日志來了解客戶端加入房間時交換的消息過程, WebRTC codelab提供了怎么集成這個例子到webrtc視頻通訊中的一步步的完整說明.你能從 step 5 of the codelab repo 下載代碼或直接進入 samdutton-nodertc.jit.su查看(用瀏覽器打開兩個URL即可).
下面是客戶端的 index.html:
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>WebRTC client</title>
5 </head>
6 <body>
7 <script src='/socket.io/socket.io.js'></script>
8 <script src='js/main.js'></script>
9 </body>
10 </html>
客戶端的JS
1 var isInitiator; 2
3 room = prompt('Enter room name:'); 4
5 var socket = io.connect(); 6
7 if (room !== '') { 8 console.log('Joining room ' + room); 9 socket.emit('create or join', room); 10 } 11
12 socket.on('full', function (room){ 13 console.log('Room ' + room + ' is full'); 14 }); 15
16 socket.on('empty', function (room){ 17 isInitiator = true; 18 console.log('Room ' + room + ' is empty'); 19 }); 20
21 socket.on('join', function (room){ 22 console.log('Making request to join room ' + room); 23 console.log('You are the initiator!'); 24 }); 25
26 socket.on('log', function (array){ 27 console.log.apply(console, array); 28 });
完整服務端代碼:
1 var static = require('node-static'); 2 var http = require('http'); 3 var file = new(static.Server)(); 4 var app = http.createServer(function (req, res) { 5 file.serve(req, res); 6 }).listen(2013); 7
8 var io = require('socket.io').listen(app); 9
10 io.sockets.on('connection', function (socket){ 11
12 // convenience function to log server messages to the client 13 function log(){ 14 var array = ['>>> Message from server: ']; 15 for (var i = 0; i < arguments.length; i++) { 16 array.push(arguments[i]); 17 } 18 socket.emit('log', array); 19 } 20
21 socket.on('message', function (message) { 22 log('Got message:', message); 23 // for a real app, would be room only (not broadcast) 24 socket.broadcast.emit('message', message); 25 }); 26
27 socket.on('create or join', function (room) { 28 var numClients = io.sockets.clients(room).length; 29
30 log('Room ' + room + ' has ' + numClients + ' client(s)'); 31 log('Request to create or join room ' + room); 32
33 if (numClients === 0){ 34 socket.join(room); 35 socket.emit('created', room); 36 } else if (numClients === 1) { 37 io.sockets.in(room).emit('join', room); 38 socket.join(room); 39 socket.emit('joined', room); 40 } else { // max two clients 41 socket.emit('full', room); 42 } 43 socket.emit('emit(): client ' + socket.id + ' joined room ' + room); 44 socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room); 45
46 }); 47
48 });
如果需要運行上面這個app,需要用到node,詳見 nodejs.org,很好很強大的一個東東,我后面會翻譯一篇介紹nodejs的文章.
其實不管你用什么方式創建信令服務,您的后台和客戶端最少需要具有樣例代碼中的功能.
使用RTCDataChannel控制信令
一旦信令服務建立好了,兩個客戶端之間建立了連接,理論上他們就可以使用RTCDataChannel進行點對點通訊了,這樣可以減輕信令服務的壓力和消息傳遞的延遲,這部分沒有提供Demo.
使用已有信令服務
如果您不想自己動手,這里還有提供幾個webrtc信令服務器,與上述代碼類似他們使用socket.io. 與webrtc客戶端的javascript集成到一起了.
webRTC.io:webrtc的第一個抽想庫.
easyRTC:一個完整的webrtc庫.
Signalmaster:信令服務器,和 SimpleWebRTC作為客戶端腳本庫配套使用.
如果您不想寫任何代碼的花,可以直接使用現有商業產品:vLine, OpenTok and Asterisk.
如果您想實現錄制功能,可參考 signaling server using PHP on Apache,雖然已經過時了,但代碼可供參考.
信令安全性問題
因為信令使我們自己定義的,所以安全性問題跟webrtc無關,需要自己處理.一旦黑客掌握了你的信令,那他就是控制會話的開始,結束,重定向等等.
最重要的因素在信令安全中還是要靠使用安全協議,如HTTPS,WSS(如TLS),他們能確保未加密的消息不能被截取.
為確保信令安全,強烈推薦使用TLS.
使用ICE處理NATs和防火牆
元數據是通過信令服務器中轉發給另一個客戶端,但是對於流媒體數據,一旦會話建立,RTCPeerConnection將首先嘗試使用點對點連接.
簡單一點說就是:每個客戶端都有一個唯一的地址,他能用來和其他客戶端進行通訊和數據交換.
現實生活中客戶端都位於一個或多個NAT之后,或者一些殺毒軟件還阻止了某些端口和協議,或者在公司還有防火牆或代理,等等,防火牆和NAT或許是同一個設備,如我們家里用的路由器.
webrtc就是通過 ICE這套框架來處理復雜的網絡環境的,如果想啟用這個功能,你必須讓你得應用程序傳ice服務器的URL給RTCPeerConnection,描述如下:
ICE試着找最好的路徑來讓客戶端建立連接,他會嘗試所有可能的選項,然后選擇最合適的方案,ICE首先嘗試P2P連接,如果失敗就會通過Turn服務器進行轉接.
換一個說法就是:
1,STUN服務器是用來取外網地址的.
2,TURN服務器是在P2P失敗時進行轉發的.
每個TURN服務器都支持STUN,ICE處理復雜的NAT設置,同時NAT打洞要求不止一個公網IP和端口.
javascript中ice配置如下:
1 { 2 'iceServers': [ 3 { 4 'url': 'stun:stun.l.google.com:19302' 5 }, 6 { 7 'url': 'turn:192.158.29.39:3478?transport=udp', 8 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 9 'username': '28224511:1379330808' 10 }, 11 { 12 'url': 'turn:192.158.29.39:3478?transport=tcp', 13 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 14 'username': '28224511:1379330808' 15 } 16 ] 17 }
一旦RTCPeerConnection取到了所要的信息,ICE過程就自動發生了,RTCPeerConnection使用ICE框架取到兩點之間最好的路徑,當然這個過程離不開STUN和TURN的支持.
STUN
NAT的作用就是提供內外網端口的映射,因為在公網上兩個內網客戶端要建立直接連接就不許先知道彼此對應的公網地址和端口,這時候知道對方內網IP和地址是沒用的.
而STUN的作用就是讓客戶端發現自己的公網IP和端口,所以負載不大,同時目前免費得STUN服務器也很多.一搜一大把.
通過webrtcstats.com可知85%的情況下可以P2P,當然復雜NAT和網絡環境下這個概率會更低.
TURN
RTCPeerConnection首先嘗試使用P2P,如果失敗,他將求助於TCP,使用turn轉發兩個端點的音視頻數據.
重申:turn轉發的是兩個端點之間的音視頻數據,不是信令數據.
因為TURN服務器是在公網上,所以他能被各個客戶端找到,另外TURN服務器轉發的是數據流,很占用帶寬和資源.
部署STUN和TURN服務器
google提供了stun.l.google.com:19302供測試, apprtc.appspot.com用的就是這個stun服務器,實際應用中,我們推薦使用rfc5766-turn-server,同時也提供了一些連接源: VM image for Amazon Web Services
turn服務器的安裝后面我專門寫篇文章來介紹,作者寫的那種方式我也沒有嘗試過,不過看起來比較復雜.有興趣的可以去看原文.
下面這幾部分我放到下一篇文章介紹,內容太多,大家會看得很暈
Beyond one-to-one: multi-party WebRTC
Multipoint Control Unit
Beyond browsers: VoIP, telephones and messaging