原文:Build the backend services needed for a WebRTC app: STUN, TURN, and signaling - HTML5 Rocks
WebRTC支持对等通信,但它仍然需要服务器,以便客户端可以交换元数据,以通过称为信令的过程协调通信,并处理网络地址转换器(NAT)和防火墙。
本文将向您展示如何构建信令服务,以及如何处理与 STUN 和 TURN 服务器进行实际连接的怪癖。它还解释了WebRTC应用程序如何处理多方呼叫并与服务(如VoIP和PSTN(也称为电话))进行交互。
如果您不熟悉 WebRTC 的基础知识,请在阅读本文之前参阅 WebRTC 入门。
信令是协调通信的过程。为了使WebRTC应用程序设置呼叫,其客户端需要交换以下信息:
此信令过程需要一种让客户端来回传递消息的方法。该机制不是由WebRTC API实现的。您需要自己构建它。在本文的后面部分,您将学习构建信令服务的方法。但是,首先,您需要一些上下文。
为了避免冗余并最大限度地提高与现有技术的兼容性,WebRTC标准没有指定信令方法和协议。JavaScript 会话建立协议 (JSEP) 概述了这种方法:
WebRTC调用设置背后的想法是完全指定和控制媒体平面,但尽可能多地将信令平面留给应用程序。其基本原理是,不同的应用程序可能更喜欢使用不同的协议,例如现有的SIP或Jingle呼叫信令协议,或者针对特定应用程序定制的东西,也许是为了一个新的用例。在这种方法中,需要交换的关键信息是多媒体会话描述,它指定建立媒体平面所需的必要传输和媒体配置信息。
JSEP的架构还避免了浏览器必须保存状态,即充当信令状态机。例如,如果每次重新加载页面时都会丢失信令数据,这将有问题。相反,信令状态可以保存在服务器上。
JSEP 架构
JSEP 要求在提供和答案的对等体之间交换,即上面提到的媒体元数据。优惠和答案以会话描述协议 (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) 示例。
请记住,WebRTC 的设计使得在将产品/服务或答案设置为本地或远程描述之前,可以通过编辑 SDP 文本中的值来调整产品/服务或答案。例如,appr.tc 中的函数可用于设置默认编解码器和比特率。使用JavaScript操纵SDP有些痛苦,并且有关于WebRTC的未来版本是否应该使用JSON的讨论,但是坚持使用SDP有一些好处。preferAudioCodec()
RTCPeerConnection
API 和信令:聘约、答案和候选RTCPeerConnection
是 WebRTC 应用程序用于在对等方之间创建连接以及传输音频和视频的 API。
要初始化此过程,有两个任务:RTCPeerConnection
一旦确定了此本地数据,就必须通过信令机制与远程对等体进行交换。
想象一下,爱丽丝正试图给夏娃打电话。以下是完整的报价/答案机制的所有血腥细节:
RTCPeerConnection
RTCPeerConnection
createOffer()
setLocalDescription()
Alice 将报价串联起来,并使用信号机制将其发送给 Eve。
setRemoteDescription()
RTCPeerConnection
createAnswer()
setLocalDescription()
Alice 使用 将 Eve 的答案设置为远程会话描述。setRemoteDescription()
斯特鲁特!
爱丽丝和夏娃还需要交换网络信息。"查找候选项"一词是指使用 ICE 框架查找网络接口和端口的过程。
RTCPeerConnection
onicecandidate
addIceCandidate()
JSEP 支持 ICE 候选滴流,它允许呼叫者在初始报价后以增量方式向被叫方提供应聘者,并允许被叫方开始对呼叫执行操作并建立连接,而无需等待所有应聘者到达。
以下代码片段是一个 W3C 代码示例,它总结了完整的信令过程。该代码假定存在某种信令机制 。稍后将更详细地讨论信令。SignalingChannel
// handles JSON.stringify/parse
const signaling = new SignalingChannel();
const constraints = {audio: true, video: true};
const configuration = {iceServers: [{urls: 'stuns:stun.example.org'}]};
const pc = new RTCPeerConnection(configuration);
// Send any ice candidates to the other peer.
pc.onicecandidate = ({candidate}) => signaling.send({candidate});
// Let the "negotiationneeded" event trigger offer generation.
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
// send the offer to the other peer
signaling.send({desc: pc.localDescription});
} catch (err) {
console.error(err);
}
};
// After remote track media arrives, show it in remote video element.
pc.ontrack = (event) => {
// Don't set srcObject again if it is already set.
if (remoteView.srcObject) return;
remoteView.srcObject = event.streams[0];
};
// Call start() to initiate.
async function start() {
try {
// Get local stream, show it in self-view, and add it to be sent.
const stream =
await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) =>
pc.addTrack(track, stream));
selfView.srcObject = stream;
} catch (err) {
console.error(err);
}
}
signaling.onmessage = async ({desc, candidate}) => {
try {
if (desc) {
// If you get an offer, you need to reply with an answer.
if (desc.type === 'offer') {
await pc.setRemoteDescription(desc);
const stream =
await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) =>
pc.addTrack(track, stream));
await pc.setLocalDescription(await pc.createAnswer());
signaling.send({desc: pc.localDescription});
} else if (desc.type === 'answer') {
await pc.setRemoteDescription(desc);
} else {
console.log('Unsupported SDP type.');
}
} else if (candidate) {
await pc.addIceCandidate(candidate);
}
} catch (err) {
console.error(err);
}
};
若要查看聘约/应答和候选人交换流程的实际应用,请参阅 simpl.info RTCPeerConnection,并查看控制台日志以获取单页视频聊天示例。如果您想要更多,请从Google Chrome中的 chrome://webrtc-internals 页面或Opera中的 opera://webrtc-internals 页面下载WebRTC信号和统计信息的完整转储。
这是一种奇特的提问方式,"我如何找到可以交谈的人?
对于电话呼叫,您有电话号码和目录。对于在线视频聊天和消息传递,您需要身份和状态管理系统,以及用户启动会话的方法。WebRTC应用程序需要一种方式,让客户端相互发出信号,表明他们想要开始或加入呼叫。
WebRTC没有定义对等发现机制,您不会进入此处的选项。该过程可以像通过电子邮件发送或消息传递URL一样简单。对于视频聊天应用(如 Talky、tawk.to 和浏览器会议),您可以通过共享自定义链接来邀请他人加入通话。开发人员 Chris Ball 构建了一个有趣的无服务器 webrtc 实验,使 WebRTC 调用参与者能够通过他们喜欢的任何消息服务(如 IM、电子邮件或归巢鸽)交换元数据。
重申一下,WebRTC标准没有定义信令协议和机制。无论您选择什么,都需要一个中间服务器在客户端之间交换信令消息和应用程序数据。可悲的是,网络应用程序不能简单地对着互联网大喊,"把我和我的朋友联系起来!
值得庆幸的是,信号消息很小,并且主要在呼叫开始时交换。在对视频聊天会话使用 appr.tc 进行测试时,信令服务总共处理了大约 30-45 条消息,所有消息的总大小约为 10KB。
除了在带宽方面要求相对较低之外,WebRTC信令服务不会消耗太多的处理或内存,因为它们只需要中继消息并保留少量的会话状态数据,例如连接了哪些客户端。
用于交换会话元数据的信令机制也可用于传达应用数据。它只是一个消息传递服务!
用于信令的消息服务必须是双向的:客户端到服务器和服务器到客户端。双向通信与HTTP客户端/服务器请求/响应模型背道而驰,但是多年来已经开发了各种黑客(例如长时间轮询),以便将数据从Web服务器上运行的服务推送到在浏览器中运行的Web应用程序。
最近,EventSource API已被广泛实施。这将启用服务器发送的事件 - 通过 HTTP 从 Web 服务器发送到浏览器客户端的数据。 是为单向消息传递而设计的,但它可以与 XHR 结合使用,以构建用于交换信令消息的服务。信令服务通过 XHR 请求传递来自调用方的消息,方法是将消息推送给被调用方。EventSource
EventSource
WebSocket 是一种更自然的解决方案,专为全双工客户端-服务器通信而设计,即可以同时在两个方向上流动的消息。使用纯 WebSocket 或服务器发送的事件 () 构建的信令服务的一个优点是,这些 API 的后端可以在 PHP、Python 和 Ruby 等语言的大多数 Web 托管包通用的各种 Web 框架上实现。EventSource
除了Opera Mini之外,所有现代浏览器都支持WebSocket,更重要的是,所有支持WebRTC的浏览器也支持WebSocket,无论是在桌面还是移动设备上。TLS 应用于所有连接,以确保消息无法在未加密的情况下被截获,并减少代理遍历的问题。(有关 WebSocket 和代理遍历的更多信息,请参阅 Ilya Grigorik 的高性能浏览器网络中的 WebRTC 一章。
也可以通过让WebRTC客户端通过Ajax反复轮询消息传递服务器来处理信令,但这会导致大量冗余的网络请求,这对于移动设备来说尤其成问题。即使在建立会话后,对等方也需要轮询信号消息,以防其他对等方更改或会话终止。WebRTC Book 应用程序示例采用此选项,并对轮询频率进行了一些优化。
尽管信令服务每个客户端消耗的带宽和 CPU 相对较少,但常用应用的信令服务器可能必须处理来自不同位置的大量消息,并且具有很高的并发性。获得大量流量的WebRTC应用程序需要能够处理大量负载的信令服务器。
此处不详细介绍,但有许多选项可用于高容量、高性能消息传递,其中包括:
(开发人员 Phil Leggetter 的《实时 Web 技术指南》提供了消息传递服务和库的完整列表。
以下是使用 Node 上的 Socket.io 构建的信令服务的简单 Web 应用的代码。Socket.io 的设计使得构建交换消息的服务变得简单,Socket.io 由于其内置的房间概念,因此特别适合WebRTC信令。此示例并非旨在作为生产级信令服务进行扩展,但对于相对较少的用户来说,它很容易理解。
Socket.io 使用带有回退的WebSocket:AJAX长轮询,AJAX多部分流,Forever Iframe和JSONP轮询。它已被移植到各种后端,但也许最出名的是本例中使用的Node版本。
此示例中没有 WebRTC。它仅用于演示如何将信令构建到 Web 应用中。查看控制台日志,了解客户端加入聊天室和交换消息时发生的情况。这个WebRTC代码实验室提供了有关如何将其集成到完整的WebRTC视频聊天应用程序中的分步说明。
这是客户端:index.html
WebRTC client
以下是客户端中引用的 JavaScript 文件:main.js
const isInitiator;
room = prompt('Enter room name:');
const socket = io.connect();
if (room !== '') {
console.log('Joining room ' + room);
socket.emit('create or join', room);
}
socket.on('full', (room) => {
console.log('Room ' + room + ' is full');
});
socket.on('empty', (room) => {
isInitiator = true;
console.log('Room ' + room + ' is empty');
});
socket.on('join', (room) => {
console.log('Making request to join room ' + room);
console.log('You are the initiator!');
});
socket.on('log', (array) => {
console.log.apply(console, array);
});
以下是完整的服务器应用程序:
const static = require('node-static');
const http = require('http');
const file = new(static.Server)();
const app = http.createServer(function (req, res) {
file.serve(req, res);
}).listen(2013);
const io = require('socket.io').listen(app);
io.sockets.on('connection', (socket) => {
// Convenience function to log server messages to the client
function log(){
const array = ['>>> Message from server: '];
for (const i = 0; i < arguments.length; i++) {
array.push(arguments[i]);
}
socket.emit('log', array);
}
socket.on('message', (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', (room) => {
const 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);
});
});
(为此,您无需了解节点静态。它只是碰巧在此示例中使用。
若要在 localhost 上运行此应用,需要安装 Node、Socket.IO 和 node-static。Node可以从Node.js下载(安装简单快捷)。若要安装 Socket.IO 和节点静态,请从应用目录中的终端运行节点包管理器:
npm install socket.io
npm install node-static
要启动服务器,请从应用程序目录中的终端运行以下命令:
node server.js
从浏览器中,打开 。在任何浏览器中打开新选项卡或窗口,然后再次打开。若要查看发生的情况,请查看主机。在 Chrome 和 Opera 中,您可以通过 Google Chrome Developer Tools 使用(或在 Mac 上)访问主机。localhost:2013
localhost:2013
Ctrl+Shift+JCommand+Option+J
无论你选择哪种信令方法,后端和客户端应用(至少)都需要提供与此示例类似的服务。
RTCPeerConnection
在被调用之前不会开始收集候选人。这在JSEP IETF草案中是强制性的。setLocalDescription()
addIceCandidate()
如果您不想推出自己的服务器,可以使用几个WebRTC信令服务器,它们使用 Socket.IO 如上一个示例,并与WebRTC客户端JavaScript库集成:
如果您根本不想编写任何代码,则可以从vLine,OpenTok和Asterisk等公司获得完整的商业WebRTC平台。
根据记录,爱立信在WebRTC的早期在Apache上使用PHP构建了一个信令服务器。这现在已经有些过时了,但是如果您正在考虑类似的东西,则值得查看代码。
"安全是让任何事情都无法实现的艺术。
— 萨尔曼·拉什迪
加密对于所有 WebRTC 组件都是必需的。
但是,WebRTC标准没有定义信令机制,因此由您来确保信令安全。如果攻击者设法劫持信令,他们可以停止会话、重定向连接以及记录、更改或注入内容。
保护信令的最重要因素是使用安全协议 ( HTTPS 和 WSS(例如 TLS)) - 它们确保消息不会在未加密的情况下被截获。此外,请注意不要以其他调用方可以使用同一信令服务器访问的方式广播信令消息。
为了保护WebRTC应用程序,信令必须使用TLS。
对于元数据信令,WebRTC应用程序使用中间服务器,但对于会话建立后的实际媒体和数据流,会尝试直接或对等连接客户端。RTCPeerConnection
在更简单的世界中,每个WebRTC端点都有一个唯一的地址,它可以与其他对等体交换,以便直接通信。
没有 NAT 和防火墙的世界
实际上,大多数设备都位于一层或多层 NAT 后面,有些设备具有阻止某些端口和协议的防病毒软件,许多设备位于代理和企业防火墙后面。防火墙和NAT实际上可以由同一设备实现,例如家庭WIFI路由器。
现实世界
WebRTC应用程序可以使用ICE框架来克服现实世界网络的复杂性。若要启用此操作,你的应用必须将 ICE 服务器 URL 传递给 ,如本文中所述。RTCPeerConnection
ICE 试图找到连接对等方的最佳路径。它并行尝试所有可能性,并选择最有效的选项。ICE 首先尝试使用从设备的操作系统和网卡获取的主机地址建立连接。如果失败(对于 NAT 后面的设备,ICE 将使用 STUN 服务器获取外部地址,如果失败,则流量将通过 TURN 中继服务器路由。
换句话说,STUN服务器用于获取外部网络地址,而TURN服务器用于在直接(对等)连接失败时中继流量。
每个 TURN 服务器都支持 STUN。TURN 服务器是具有附加内置中继功能的 STUN 服务器。ICE 还可以应对 NAT 设置的复杂性。实际上,NAT 打孔可能不仅仅是一个公共 IP:端口地址。
STUN 和/或 TURN 服务器的 URL(可选)由 WebRTC 应用在配置对象中指定,该对象是构造函数的第一个参数。对于 appr.tc,该值如下所示:iceServers
RTCPeerConnection
{
'iceServers': [
{
'urls': 'stun:stun.l.google.com:19302'
},
{
'urls': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
},
{
'urls': 'turn:192.158.29.39:3478?transport=tcp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}
]
}
注意:TURN 凭据示例有时间限制,于 2013 年 9 月过期。TURN服务器的运行成本很高,您需要为自己的服务器付费或寻找服务提供商。若要测试凭据,可以使用候选集合示例并检查是否获得类型为 的候选项。relay
一旦有了这些信息,ICE的魔力就会自动发生。 使用ICE框架来计算对等体之间的最佳路径,并根据需要使用STUN和TURN服务器。RTCPeerConnection
RTCPeerConnection
NAT 为设备提供在专用本地网络中使用的 IP 地址,但此地址不能在外部使用。没有公共地址,WebRTC对等体就无法进行通信。为了解决这个问题,WebRTC使用STUN。
STUN 服务器位于公共互联网上,并且有一个简单的任务 - 检查传入请求的 IP:port 地址(来自在 NAT 后面运行的应用程序),并将该地址作为响应发送回去。换句话说,该应用程序使用STUN服务器从公共角度发现其IP:端口。此过程使WebRTC对等体能够为自己获取一个可公开访问的地址,然后通过信令机制将其传递给另一个对等体,以便建立直接链接。(在实践中,不同的 NAT 以不同的方式工作,并且可能有多个 NAT 层,但原理仍然相同。
STUN服务器不必做太多或记住太多,因此相对低规格的STUN服务器可以处理大量请求。
大多数WebRTC呼叫使用STUN成功建立连接 - 根据 Webrtcstats.com 为86%,尽管对于防火墙和复杂NAT配置后面的对等体之间的呼叫,这可能更少。
使用 STUN 服务器获取公共 IP:端口地址
RTCPeerConnection
尝试通过 UDP 在对等方之间建立直接通信。如果失败,则诉诸 TCP。如果失败,TURN 服务器可用作回退,在端点之间中继数据。RTCPeerConnection
重申一下,TURN用于在对等体之间中继音频,视频和数据流,而不是信号数据!
TURN 服务器具有公共地址,因此即使对等方位于防火墙或代理后面,对等方也可以与它们联系。TURN 服务器在概念上执行一项简单的任务 — 中继流。但是,与STUN服务器不同,它们本身会消耗大量带宽。换句话说,TURN服务器需要更强大。
The full Monty: STUN, TURN, and signaling
此图显示了 TURN 的实际应用。Pure STUN没有成功,因此每个对等方都使用TURN服务器。
为了进行测试,Google运行一个公共STUN服务器,stun.l.google.com:19302,如 appr.tc 使用的那样。对于生产 STUN/TURN 服务,请使用 rfc5766-turn-server。STUN 和 TURN 服务器的源代码可在 GitHub 上找到,您还可以在其中找到指向有关服务器安装的多个信息源的链接。此外,还提供了 Amazon Web Services 的 VM 映像。
另一个 TURN 服务器是 restund,可作为源代码提供,也可用于 AWS。以下是有关如何在 Compute Engine 上设置 restund 的说明。
sudo apt-get install make
sudo apt-get install gcc
wget
hancke.name/restund-auth.patch 并申请。patch -p1 < restund-auth.patch
make
sudo make install
restund.conf
/etc
restund/etc/restund
/etc/init.d/
LD_LIBRARY_PATH
restund.conf
/etc/restund.conf
restund.conf
./client IP:port
您可能还想看看Justin Uberti提出的用于REST API的IETF标准,用于访问TURN服务。
很容易想象媒体流的用例超出了简单的一对一呼叫。例如,一组同事之间的视频会议或一个演讲者和数亿观众的公共活动。
WebRTC 应用程序可以使用多个 RTCPeerConnections,以便每个终结点都连接到网格配置中的每个其他终结点。这是应用程序(如 talky.io)采用的方法,对于少数对等方来说效果非常好。除此之外,处理和带宽消耗变得过多,特别是对于移动客户端。
全网状拓扑:人人互联
或者,WebRTC 应用程序可以选择一个端点,以星形配置将流分发给所有其他端点。还可以在服务器上运行 WebRTC 端点并构建自己的再分发机制(webrtc.org 提供了示例客户端应用)。
从Chrome 31和Opera 18开始,一个来自一个可以用作另一个的输入。这可以实现更灵活的体系结构,因为它使 Web 应用能够通过选择要连接到的其他对等来处理呼叫路由。若要查看其实际效果,请参阅 WebRTC 示例对等连接中继和 WebRTC 示例多个对等连接。MediaStream
RTCPeerConnection
对于大量端点,更好的选择是使用多点控制单元(MCU)。这是一个服务器,可用作在大量参与者之间分发媒体的桥梁。MCU可以应对视频会议中不同的分辨率、编解码器和帧速率;句柄转码;做选择性流转发;并混合或录制音频和视频。对于多方通话,需要考虑许多问题,特别是如何显示多个视频输入和混合来自多个源的音频。云平台(如 vLine)也尝试优化流量路由。
可以购买完整的MCU硬件包或构建自己的MCU硬件包。
思科 MCU 的背面
有几种开源MCU软件选项可用。例如,Licode(以前称为Lynckia)为WebRTC生成开源MCU。OpenTok有Mantis。
WebRTC的标准化性质使得在浏览器中运行的WebRTC应用程序与在另一个通信平台(如电话或视频会议系统)上运行的设备或平台之间建立通信成为可能。
SIP是VoIP和视频会议系统使用的信令协议。为了启用 WebRTC Web 应用程序和 SIP 客户端(如视频会议系统)之间的通信,WebRTC 需要一个代理服务器来调解信令。信令必须流经网关,但是,一旦建立了通信,SRTP 流量(视频和音频)就可以直接对等地流动。
公共交换电话网 (PSTN) 是所有"普通旧"模拟电话的电路交换网络。对于 WebRTC Web 应用和电话之间的呼叫,流量必须通过 PSTN 网关。同样,WebRTC Web应用程序需要一个中间的XMPP服务器来与IM客户端等Jingle端点进行通信。Jingle由Google开发,作为XMPP的扩展,为消息传递服务启用语音和视频。目前的WebRTC实现基于C++ libjingle库,这是最初为Talk开发的Jingle的实现。
许多应用程序,库和平台都利用WebRTC与外界通信的能力:
sipML5开发人员还构建了webrtc2sip网关。Tethr和Tropo已经展示了一个"在公文包中"的灾难通信框架,该框架使用OpenBTS单元通过WebRTC实现功能手机和计算机之间的通信。这就是没有运营商的电话通信!
WebRTC codelab 提供了有关如何使用在 Node 上运行的 Socket.io 信令服务构建视频和文本聊天应用程序的分步说明。
2013年Google I/O WebRTC演讲,WebRTC技术主管Justin Uberti
Chris Wilson 的 SFHTML5 演示文稿 — WebRTC 应用程序简介
350页的书WebRTC:HTML5 Real-Time Web的API和RTCWEB协议提供了大量关于数据和信令路径的详细信息,并包括许多详细的网络拓扑图。
WebRTC和信令:两年教会了我们什么 - TokBox博客文章,关于为什么将信令排除在规范之外是一个好主意
Ben Strong的《构建WebRTC应用程序实用指南》提供了大量有关WebRTC拓扑和基础架构的信息。
Ilya Grigorik的高性能浏览器网络中的WebRTC章节深入探讨了WebRTC架构,用例和性能。