下面这篇介绍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格式进行交换,格式如下:
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架构里面调用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,信令在下面被详细讨论
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,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:
<!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>
客户端的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); }); });
如果需要运行上面这个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配置如下:
{
'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
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服务器的安装后面我专门写篇文章来介绍,作者写的那种方式我也没有尝试过,不过看起来比较复杂.有兴趣的可以去看原文.
下面这几部分我放到下一篇文章介绍,内容太多,大家会看得很晕