下面这篇介绍webrtc的文章不错,我花了大半天翻译了一下.javascript
翻译的时候不是逐字逐句的,而是按照本身的理解翻译的,同时为了便于理解,也加入一些本身组织的语言.css
本文主要介绍webrtc的信令,stun,turn,转载请说明出处(博客园RTC.Blacker).html
英文来自:http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/ html5
WEBRTC支持点对点通信,可是WEBRTC仍然须要服务端,由于:java
1,为了协调通信过程客户端之间须要交换元数据,如一个客户端找到另外一个客户端以及通知另外一个客户端开始通信.node
2,须要处理NAT或防火墙,这是公网上通信首要处理的问题.git
在这篇文章里咱们将告诉您怎么建立一个信令服务,怎么处理现实世界中两个客户端的链接,以及怎么处理多方通话和怎么与VOIP,PSTN的交互.若是您不了解webrtc,建议您读这篇文章前先看:http://www.html5rocks.com/en/tutorials/webrtc/basics/github
什么是信令?web
信令就是协调通信的过程,为了创建一个webrtc的通信过程,客户端须要交换以下信息:chrome
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就是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到达.
下面是一个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 提供了一个消息服务和库的综合清单.
下面这个代码是一个简单的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进行点对点通信了,这样能够减轻信令服务的压力和消息传递的延迟,这部分没有提供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.
元数据是经过信令服务器中转发给另外一个客户端,可是对于流媒体数据,一旦会话创建,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的支持.
NAT的做用就是提供内外网端口的映射,由于在公网上两个内网客户端要创建直接链接就不准先知道彼此对应的公网地址和端口,这时候知道对方内网IP和地址是没用的.
而STUN的做用就是让客户端发现本身的公网IP和端口,因此负载不大,同时目前免费得STUN服务器也不少.一搜一大把.
经过webrtcstats.com可知85%的状况下能够P2P,固然复杂NAT和网络环境下这个几率会更低.
RTCPeerConnection首先尝试使用P2P,若是失败,他将求助于TCP,使用turn转发两个端点的音视频数据.
重申:turn转发的是两个端点之间的音视频数据,不是信令数据.
由于TURN服务器是在公网上,因此他能被各个客户端找到,另外TURN服务器转发的是数据流,很占用带宽和资源.
google提供了stun.l.google.com:19302供测试, apprtc.appspot.com用的就是这个stun服务器,实际应用中,咱们推荐使用rfc5766-turn-server,同时也提供了一些链接源: VM image for Amazon Web Services
turn服务器的安装后面我专门写篇文章来介绍,做者写的那种方式我也没有尝试过,不过看起来比较复杂.有兴趣的能够去看原文.
下面这几部分我放到下一篇文章介绍,内容太多,你们会看得很晕