译:WebRTC视频通信

来源:互联网 发布:东京大学医学部 知乎 编辑:程序博客网 时间:2024/05/14 18:32

原文:http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/


WebRTC可以进行p2p之间的通信,但是仍需要服务支持.
1. signaling服务: 客户端之间交换元数据来建立通信.
2. 穿透NATs和防火墙.

在本文中,我们想你展示如何构建一个signaling服务,如何用STUN/TURN服务去做NATs穿透.另外,解释WebRTC是如何进行多端通话的.以及如何利用VoIP/PSTN 建立通话.

如果你不熟悉基本的WebRTC,我们强烈建议你阅读本文之前先看看 Getting Started With WebRTC.

什么是信令服务(Signaling)?

信令是在协调沟通的过程,为了让WebRTC应用程序建立一个”通话”,客户端之间要交换信令信息:

  • 打开或关闭一个会话的控制信息
  • 错误信息
  • 媒体源数据,比如编码解码设置,带宽和媒体类型
  • 关键数据,用于建立安全连接
  • 网络数据,例如主机地址和端口.

信令服务是一个在客户端间来回传递信息的机制.这个机制没有在WebRTC中实现,需要我们自己创建它们.在说明几种创建信令方法之前,先交代一下背景.

为什么WebRTC没有指定信令?

为了避免冗余数据和保持最大的兼容性.在建立信令时WebRTC没有指定一个标准的协议.


WebRTC设计思想是在完全控制媒体层部分,让信令层尽量脱离应用,因为很可能不同的应用在使用不同的协议.在这种模式下,需要交换的关键信息是多媒体的会话描述,它指定了必要的运输和媒体配置信息. JSEP结构也避免了让浏览器保存状态信息,防止每次页面重载,信令就会丢失. 所以把信令状态保存到服务器中.

JSEP architecture
JSEP协议要求客户端之间需要交换offer和answer: 上面提到的媒体源数据,offer和answer遵守会话描述协议(SDP)进行交流.如下:

v=0o=- 7614219274584779017 2 IN IP4 127.0.0.1s=-t=0 0a=group:BUNDLE audio videoa=msid-semantic: WMSm=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126c=IN IP4 0.0.0.0a=rtcp:1 IN IP4 0.0.0.0a=ice-ufrag:W2TGCZw2NZHuwlnfa=ice-pwd:xdQEccP40E+P0L5qTyzDgfmWa=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levela=mid:audioa=rtcp-muxa=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQea=rtpmap:111 opus/48000/2

如果想了解SDP,可以查看IETF examples.
WebRTC这样设计是为了让offer和answer端能够在调整之前通过SDP协议设置好本地或远程描述. 例如: preferAudioCodec()方法用来设置默认编解码方式和比特.虽然SDP在JavaScript中难操作,以后可能用JSON代替,但是还是有一些优势的.

RTCPeerConnection + 信令: offer, answer and candidate

RTCPeerConnection是WebRTC客户端之间建立音视频通信的API.
初始化RTCPeerConnection需要两个步骤:

  1. 确定本地媒体条件,如:分辨率,编解码方式.这些是offer和answer所需的原始数据.
  2. 获得潜在应用的主机地址,即candidate.

一旦本地数据确定,就必须通过信令机制与远端进行对等交换.
假设Alice呼叫Eve.下面详细描述offer/answer机制.

  1. Alice创建一个RTCPeerConnection对象,
  2. Alice用此对象的createrOffer()创建一个offer.并调用setLocalDescription()设置本地描述offer.(如:分辨率和编解码方式)
  3. Alice把offer通过信令机制发送给Eva.
  4. Eva调用setRemoteDescription()方法传入offer,来告诉她的PeerConnection对象远端Alice的配置描述.
  5. 之后Eva会调用creatAnswer(),如果成功就会返回一个对应的本地session描述,即answer.(如果不成功,表示Eva没有Alice的offer对应的本地设置)
  6. Eva根据生成的answer通过setLocalDescription()设置本地的描述.
  7. 然后用信令机制发送序列化后的answer返回给Alice.
  8. Alice根据Eva的answer调用setRemoteDescription()设置对方的描述.

至此,Alice和Eva都设置了本地及对方的描述,
接下来还需要交换网络信息.”finding candidates”是指用ICE framework寻找网络接口和端口的过程:

  1. Alice用onlceCandidate handler 创建一个RTCPeerConnection对象.
  2. 当网络candidates有效时这个handler会被调用.
  3. 在这个handler里,Alice通过信令机制发送序列化的candidates数据给Eva.
  4. 当Eva从Alice获得candidate信息,她会调用addIceCandidate()去给对方描述添加candidate.

JSEP 支持ICE Candidate Trickling(允许呼叫者在首次初始化offer后,可以逐步发送candidate给被呼叫者,这样让被呼叫者可以马上开始设置连接,而不用等到所有candidates到达)

WebRTC的信令编码

下面是一个W3C 代码范例,概括了完整的信令流程.假设已经有了信令机制:SignalingChannel. 下面进行详细讨论.

var signalingChannel = new SignalingChannel();var configuration = {  'iceServers': [{    'url': 'stun:stun.example.org'  }]};var pc;//调用 start()  启动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);}

查看”单页面”视频聊天的例子simpl.info/pc,可以在控制台log中看到offer/answer和candidate的交换过程.

Peer discovery

这里有个问题: 我们如何找到别人进行会话?
对于电话来说我们有电话号码查询,而对于在线视频聊天和通信,我们需要身份(id)和业务管理系统,以及一个让用户发起会话的手段. WebRTC apps需要一个让用户彼此表明自己可以开始或加入一个会话的信号.

WebRTC没有定义成员发现机制(peer discovery),我们也不需要在这里做设置.这个过程可以像发送一个URL地址那么简单,对于视频聊天应用,例如talky.io等,你可以通过分享一个自定义连接邀请别人;开发人员做了一个实验serverless-webrtc,可以让webRTC用户通过任何信息服务方式交换元数据,比如:IM,email;

如何创建一个信令服务

再次重申:webRTC中并没有定义信令协议和机制的标准.无论你选择什么,你只需要一个中介服务器来交换信令消息和客户端间的应用程序数据.可悲的是,一个web应用不能简单的在网络喊”联系我,我的朋友”.

庆幸的是,信令的消息小,大部分的交换都是在呼叫开始的时候.
在测试apprtc.appspot.com和samduttion-nodertc.jit.su时,我们可以发现,一个视频通话总共只有大约10KB的消息是由信令服务处理的.

除了相对宽松的带宽要求,webRTC的信令服务也不会花费过多的进程和内存.因为他们只需要交换消息和保留少量的的会话状态数据(比如,哪个客户端连接了).

信令机制不仅用来交换session元数据,也能用于应用程序数据通信,它只是一个消息传递服务.

服务端推送信息到客户端

一个信令服务需要是双向的:服务端到客户端/客户端到服务端. 双向通讯虽然违反了HTTP的请求/回复模式,但是有一些发展多年的技术,例如long polling(长时间问询)用来从服务器发送数据给一个在浏览器中运行的web应用.

最近,EventSource API被广泛应用,可以让数据通过HTTP从服务端发送给浏览器. 有一个简单的demo:simpl.info/es. EventSource是专门用于一个消息传递的方式,但它可以结合XHR 做成一个交换signaling消息的服务:从一个呼叫者传递一个消息,由XHR请求,推送给被呼叫者.

WebSocket是一种更自然的双向通讯解决方法,设计为全双工客户端-服务端通信(消息可以同时在两个方向流动). 使用纯WebSocket或者EventSource建立信令服务可以让后端用多种Web框架实现这些API调用.

大约四分之三的浏览器支持WebSocket,更重要的是,所有支持WebRTC的移动或PC浏览器也支持WebSocket.TLS(安全传输协议)应该用于所有连接,以确保消息不会被截获. 同时 reduce problems with proxy traversa.(关于更多的WebSocket和proxy traversal资料可以查询WebRTC chapte和WebSocket cheat sheet)

apart.appspot.com的信令是通过Google App Engine Channel API完成的,这个API使用了Comet技术(长时间轮询)让信令推动App Engine端和web客户端之间的通信.(这里有个long-standing bug,关于App Engine 对WebSocket的支持.)这里有个代码演示在HTML5 Rocks WebRTC article.

还可以通过Ajax轮询WebRTC服务端获取信令传递消息.但这会产生很多冗余请求,特别是在移动端. 即是已经建立了一个会话, 用户仍然需要去多次轮询signaing信息. 因为会话可能被其他用户更改或终止.<<WebRTC>>中就使用了这个例子,经过了优化轮询频率.

信令扩展

虽然一个signaling服务在每个客户端只花费很少的带宽和CPU,但是一个受欢迎的应用程序服务器可能会需要处理大量的信息和高并发数,所以一个完善的webRTC应用需要信令服务能处理这些负荷.

下面有些处理大数据量的高性能信息通信选择:

  • XMPP,最初被称为Jabber: 一种被开发用于即是通讯的协议. 可以用来做signaling.服务端可以用egabberd和Opendire实现.JavaScript客户端,例如Strophe.js使用BOSH去模仿双向通讯流, 但由于各种原因,BOSH不像WebSocket那么小了(Jingle是一种支持视频和语音的XMPP扩展,WebRTC从libjingle库使用网络和传输组件).
  • 使用像ZeroMQ,QpenMQ的开源库.利用STOMO webSocket协议适用于网络平台.
  • 使用支持WebSocket的商业云服务平台,如:Ousher,Kaazing和PubNub.
  • 商业WebRTC平台.

一个综合的信息服务和库列表.

基于Socket.io建立信令服务

下面代码是个简单的web应用程序,使用Socket.io建立的signaling. Socket.io可以轻易的创建一个用于交换信息的服务,它非常适合WebRTC的信令,因为Socket.io是基于”rooms”的概念设计的. 这个demo不是一个产品级的服务,适合相对较少的用户.

Socket.io通过下面的回调使用WebSocket:Adobe Flash Socket,AJAX long polling,AJAX multipart streaming,FOrever Iframe and JSONP polling.
它已经被移植到各种后端,最有名的是Node.下面的例子中我们也将使用.

这个demo没有WebRTC,它只展示怎么创建一个webapp的signaling.用控制台查看log,了解用户加入一个房间交换数发生了什么,WebRTC codelab会一步步指示如何将这个示例集成到一个完整的webRTC视频聊天应用中.你可以从step 5 of the codelab repo下载源码或者在samdutton-nodertc.jit.su运行(两个浏览器中打开).

这是客户端的 index.html:

<!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>

这是JavaScript 的main.js文件 ,客户端引用.

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);});

完整服务端:

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-sattic,可以在nodes.org下载Node,再安装socket.io和node-static. 在程序目录终端运行

npm install socket.ionpm install node-static

启动服务器,在终端中输入

node server.js

在浏览器中打开localhost:2013. 打开一个新标签页或在任何浏览器窗口再打开localhost:2013. 用控制台看发生了什么. 无论哪种信令方式,都要你的后端和客户端应用提供类似的服务.

使用RTCDataChannel 信令

一个WebRTC会话必须要有一个signaling

然而,一旦两端建立了连接,理论上RTCDataChannel可以接替信令通道,这可以减少信号延迟,RTCDataChannel会帮忙减少带宽使用和进程开销.可以看下面:

信令性能和扩展性:

  • RTCPeerConnection只有setLocalDescription()被调用后才会收集candidates.
  • 利用Trickle ICE:接受到candidates后立即调用addIceCandidate()

使用现成的信令服务

如果你不想用自己的,这里有一些现成的webRTC signaling服务端,就像上面的例子使用socket.io,结合WebRTC和JavaScript库:

  • webRTC.io:第一个WebRTC的抽想库
  • esayRTC:一个完整的WebRTC包
  • Signalmaster:一个使用SimpleWebRTC JavaScript客户端库创建的signaling server.

如果你不想写任何代码,去找商业平台:vLine,OpenTok,Asterisk.

信令安全

所有WebRTC组件都要强制加密

然而,信令机制不是WebRTC所定义的,所以信令的安全取决于你,如果一个攻击者成功截获信号,他们可以停止,重定向链接,改变或者注入内容.

一个牢固的信令最重要的是使用安全协议,HTTPS和WSS.可以确保不被非法拦截.
同时,也不要广播信令信息,不然攻击者可以使用相同的信令服务访问其他来电用户.

使用TLS 确保WebRTC应用安全.

信令交互完之后,使用ICE去处理NATs和防火墙对于元数据的信令

WebRTC应用可以使用中间服务,但实际的媒体和数据流在一个会话确立后,RTCPeerConnection 尝试去直连p2p客户端:在一个简单的世界里,每一个WebRTC端都有一个唯一的地址,这样他可以与其他端交换数据,以便直接 通讯。

实际情况下,大多数设备都在一个或多个NAT层后面,有些有防毒软件阻碍确定的端口和协议,还有很多在代理和公司的防火墙后面。防火墙和NAT实际上可能由一些类似家庭wifi路由器产生的。

WebRTC 可以使用ICE框架去克服真实世界的复杂网络。为了实现这个功能,你的应用必须传ICE服务地址给RTCPeerConnection,如下所述。
ICE 试着寻找最佳路线去连接对方,它会并行的寻找所有可能性,然后选择最有效的可行方式。
ICE首先会尝试用设备系统或网卡获取到的主机地址去建立连接;如果这个失败了(设备在NATs后面就会)ICE从STUN服务器获得外部的地址,如果这个也失败了,就用TURN中转服务器做通讯。
也就是说:

  • STUN服务器用来获取外部网络地址。
  • 如果失败,使用TURN服务器作为中继服务器传输数据.

每一个TURN服务器都支持STUN:一个TURN服务器是由一个STUN服务器加上中继功能。ICE也可以用来应付复杂的NAT设置:事实上,NAT的”打洞“可能需要除了公共IP之外的端口地址。

WebRTC应用在iceServers配置对象(RTCPeerConnection constructor)里设置STUN and/or TURN服务器地址。

{  '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服务器。

STUN

NATs会给它的设备提供一个内部网络IP地址,但这个地址不能在外网使用,所以WebRTC没办法做连接,为解决这个问题,WebRTC使用了STUN。STUN服务架设在外网,它有一个简单的任务:获取一个发送请求的设备(运行在NAT后边的应用)的IP和端口,然后返回这个地址。

换句话说,应用使用STUN服务器发现它的外网IP和端口,这个过程确保了一个WebRTC端获得它自己的公共地址,然后通过signaling机制发送这个信息给另一端,这样就可以建立起一个直接连接.

STUN服务器不需要做太多工作和存储太多东西,所以简单的STUN服务器可以应付大量的请求。根据 webrtcstats.com的统计,使用STUN方式建立WebRTC通话的成功率有86%的。

TURN

RTCPeerConnection 会试着用UDP在两端建立一个直连,如果失败了,RTCPeerConnection 会改用TCP,如果这个也失败了,TURN服务器会被作为后备方案使用,作为在两端间中继服务器。
所以:TURN是在两端间中转视频/语音/数据 流,而不是发送数据。

TURN 有个公共地址,所以每个端即使在防火墙或者代理后面,也能访问到。
TURN有个简单的任务,中转数据流,但不像STUN,TURN会花费大量带宽。所有,TURN需要够强壮。
这里写图片描述
图显示了TURN的作用: 单纯的STUN不起作用,客户端就会转向使用TURN.

部署 STUN and TURN servers

谷歌公布了一个公共的STUN服务,stun.l.google.com:19302, apprtc.appspot.com用的就是这个。作为一个产品级别的 STUN/TURN服务器,我们建议使用 rfc5766-turn-server,STUN 和TURN的源码可以从code.google.com/p/rfc5766-turn-server获取,这个链接也包括了部署的资料。A VM image for Amazon Web Services is also available.本社区也发布了部署教程:部署教程一个可代替的TURN服务器是restrund,可以在source code 下载到,下面介绍在谷歌Compute Engine部署resrund的步骤:
1. Open firewall as necessary, for tcp=443, udp/tcp=3478
2. Create four instances, one for each public IP, Standard Ubuntu 12.06 image
3. Set up local firewall config (allow ANY from ANY)
4. Install tools:
sudo apt-get install make
sudo apt-get install gcc
5. Install libre from creytiv.com/re.html
6. Fetch restund from creytiv.com/restund.html and unpack
7. wget hancke.name/restund-auth.patch and apply with patch -p1 < restund-auth.patch
8. Run make, sudo make install for libre and resound
9. Adapt restund.conf to your needs (replace IP addresses and make sure it contains the same shared secret) and copy to /etc
10. Copy restund/etc/restund to /etc/init.d/
11. Configure restund:
Set LD_LIBRARY_PATH
Copy restund.conf to /etc/restund.conf
Set restund.conf to use the right 10. IP address
12. Run resound
13. Test using stund client from remote machine: ./client IP:port

多人会议

你可能要看一下Justin Uberti’s 提议的IETF标准:REST API for access to TURN Services

一个WebRTC应用可以使用多个RTCPeerConnection, 这样每一个端就可以连接到其他段形成一个网络. talk.io就是用这种方式实现的.对于少数用户可以很好的工作牡丹石进程和带宽消耗很大,尤其对于移动端.
全网状拓扑结构:每个人都连接到每个人
全网状拓扑结构:每个人都连接到每个人

在一个星型结构里,一个WebRTC应用可以选择一个端去分布数据流给所有的用户,你可以自己设计重新分配机制的服务和构造去实现中转方式,webrtc.org提供了一个示例客户端应用.

从Chrome31何Opera18开始,从一个RTCPeerConnection获取的媒体流可以作为另一个输入: 这里有个demosimpl.info/multi. 这样可以保证更灵活的结构,因为它允许web应用通过选择哪个用户可以连接可以控制一个通话路由,

Multipoint Control Unit

大量用户通话,更好的解决方案是使用Multipoint Control Unit(MCU). 这是一个在大量参与者间分布媒体的桥接服务器.MCUs 可以在一个视频会议里处理不同的分辨率,编解码,帧速率. 对于多端会议,有很多需要考虑: 最重要的是,从多个源里,怎么显示多个视频和混合音频. 云平台如vLine还试图优化交通路由。

你可以去买一个MCU硬件或自己搭一个.
Cisco MCU背部有几个开源的MCU硬件款可以选择, 例如Licode生成的开源 MCU for WebRTC; OpenTok 平台的Mantis.

突破浏览器: VoIP, telephones 和 messaging

WebRTC 的标准让浏览器和不同设备不同平台,例如手机或者一个视频会议系统,进行通话称为可能。

SIP是一种信令协议,用来做VoIP和视频系统。为了让WebRTC和SIP端通讯,WebRTC需要一个代理服务器去调解信令。信令一定会经过网关,但是一旦会话建立,视频和语音就能在两端传输。

PSTN,公共电话交换网络,是旧式模拟电话的交换网络。为了WebRTC和电话进行通话,必须通过一个PSTN网关。同理,要让WebRTC跟Jingle端(像IM客户端)通讯,需要一个中间XMPP服务器。Jingle作为XMPP的扩展,用来实现视频和语音能够作为信息服务:现在的WebRTC就是基于C++实现libjingle 库发展来的,Jingle最初是Google Talk的技术。

一堆应用库,平台让WebRTC能在实际中通讯:
sipML5:一个 开源的 JavaScript SIP 客户端
jsSIP: JavaScript SIP库
Phono: 开源JavaScript phone API, 作为一个插件
Zingaya: 一个嵌入式电话部件
Twilio: 音频和信息
Uberconference: 会议技术

sipML5 的开发者也开发了webrtc2sip的网关Tethr and Tropo have demonstrated a framework for disaster communications ‘in a briefcase’, using an OpenBTS cell to enable communications between feature phones and computers via WebRTC. Telephone communication without a carrier!

Find out more

WebRTC codelab: step-by-step instructions how to build a video and text chat application, using a Socket.io signaling service running on Node.

2013 Google I/O WebRTC presentation with WebRTC tech lead, Justin Uberti.

Chris Wilson’s SFHTML5 presentation: Introduction to WebRTC Apps.

The WebRTC Book gives a lot of detail about data and signaling pathways, and includes a number of detailed network topology diagrams.

WebRTC and Signaling: What Two Years Has Taught Us: TokBox blog post about why leaving signaling out of the spec was a good idea.

Ben Strong’s presentation A Practical Guide to Building WebRTC Apps provides a lot of information about WebRTC topologies and infrastructure.

The WebRTC chapter in Ilya Grigorik’s High Performance Browser Networking goes deep into WebRTC architecture, use cases and performance.

0 0