本文是《Web性能权威指南》第四部分——WebRTC的读书笔记。
第一部分——网络技术概览,请参考网络技术概览;
第二部分——无线网络性能,请参考无线网络性能;
第三部分——HTTP,请参考HTTP;
第四部分——浏览器API与协议的前四章,请参考浏览器API与协议。
WebRTC
Web Real-Time Communication,Web实时通信,WebRTC,由一组标准、协议和JS API组成,用于实现浏览器之间(端到端)的音频、视频及数据共享。WebRTC使得实时通信变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是通过简单的JavaScript API即可利用。
要实现涵盖音频和视频的电话会议等完善、高品质的RTC应用、端到端的数据交换,需要浏览器具备很多新功能:音频和视频处理能力、支持新应用API、支持好几种新网络协议。浏览器把前述这些复杂性抽象成三个主要API:
- MediaStream:获取音频和视频流;
- RTCPeerConnection:音频和视频数据通信;
- RTCDataChannel:任意应用数据通信。
WebRTC通过UDP传输数据。
标准和WebRTC的发展
WebRTC架构由十余个标准组成,涵盖应用和浏览器API,以及很多必要的协议和数据格式:
- W3C的Web Real-Time Communications(WEBRTC)Working Group负责制定浏览器API;
- IETF的Real-Time Communication in Web-browsers(RTCWEB)工作组负责定义协议、数据格式、安全及其他在浏览器中实现端到端通信必需的内容。
设计WebRTC时也会考虑已有通信系统:VOIP(VoiceOver IP)、各种SIP客户端、PSTN(Public Switched Telephone Network,公共交换电话网)等。
音频和视频引擎
要实现电话会议功能,浏览器必须访问系统硬件采集音频和视频;还需对它们分别加以处理以增强品质,保证同步,而且要适应不断变化的带宽和客户端之间的网络延迟调整输出的比特率。
接收端的处理过程相反,必须实时解码音频和视频流,并适应网络抖动和时延。
如上图,浏览器会负责:
- 音频流降噪和回声消除处理、优化的窄带或宽带音频编解码器编码、错误补偿算法消除网络抖动和丢包造成的损失;
- 视频的影像品质,选择最优的压缩和编解码方案,应用抖动和丢包补偿。
浏览器要动态调整其处理流程,以适应不断变化的音频和视频流及网络条件。
W3C的Media Capture and Streams规范规定一套JS API:
解读:
- MediaStream对象包含一或多个Track(MediaStreamTrack);
- MediaStream中的多个Track相互之间是同步的;
- 输入源可以是物理设备,如麦克风、摄像头、用户硬盘或另一端远程服务器中的文件;
- MediaStream的输出可被发送到一或多个目的地:本地的视频或音频元素、后期处理的JS代理,或远程另一端。
MediaStream对象表示一个实时的媒体流,以便应用代码从中取得数据,操作个别的Track和控制输出。所有的音频和视频处理,比如降噪、均衡、影像增强等都由音频和视频引擎自动完成。
getUserMedia()
,从底层平台取得音频和视频流的简单API;负责获准访问用户的麦克风和摄像机,并获取符合指定要求的流。取得流之后,还可以将它们提供给其他浏览器API:
- 通过Web Audio API在浏览器中处理音频;
- 通过Canvas API采集个别视频帧并加以处理;
- 通过CSS3和WebGL API为输出的流应用各种2D/3D特效。
当前的WebRTC实现使用Opus和VP8编解码器:
- Opus编解码器用于音频,支持固定和可变的比特率编码,适合的带宽范围为
6~510
Kbit/s。这个编解码器可以无缝切换,以适应不同的带宽。 - VP8编解码器用于视频编码,要求带宽为
100~2000+
Kbit/s,比特率取决于流的品质,如180p,360p,720p。
实时网络传输
实时通信讲究的就是及时、当下。因此,处理音频和视频流的应用一定要补偿间歇性的丢包:音频和视频编解码器可以填充小的数据空白,通常对输出品质的影响也很小。类似地,应用必须实现自己的逻辑,以便因传输其他应用数据而丢包或延迟时快速恢复。及时和低延迟比可靠更重要。
UDP协议才更适合用于传输实时数据。UDP提供下列不服务:
- 不保证消息交付:不确认,不重传,无超时;
- 不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞;
- 不跟踪连接状态:不必建立连接或重启状态机;
- 不需要拥塞控制:不内置客户端或网络反馈机制。
UDP是浏览器实时通信的基础,但要完全达到WebRTC的要求,浏览器还需要位于其上的大量协议和服务的支持,如下图所示:
WebRTC的协议分层:
- ICE:Interactive Connectivity Establishment,RFC 5245
- STUN:Session Traversal Utilities for NAT,RFC 5389
- TURN:Traversal Using Relays around NAT,RFC 5766
- SDP:Session Description Protocol,会话描述协议,RFC 4566
- DTLS:Datagram Transport Layer Security,RFC 6347
- SCTP:Stream Control Transport Protocol,RFC 4960
- SRTP:Secure Real-Time Transport Protocol,RFC 3711
ICE、STUN和TURN是通过UDP建立并维护端到端连接所必需的。DTLS用于保障传输数据的安全,加密是WebRTC强制的功能。SCTP和SRTP属于应用层协议,用于在UDP之上提供不同流的多路复用、拥塞和流量控制,以及部分可靠的交付和其他服务。
RTCPeerConnection API
RTCPeerConnection接口负责维护每一个端到端连接的完整生命周期:
- 管理穿越NAT的完整ICE工作流;
- 发送自动(STUN)持久化信号;
- 跟踪本地流;
- 跟踪远程流;
- 按需触发自动流协商;
- 提供必要的API,以生成连接提议,接收应答,允许查询连接的当前状态等。
RTCPeerConnection把所有连接设置、管理和状态都封装在一个接口中。
DataChannel API用于实现端到端之间的任意应用数据交换,类似于WebSocket,但却是端到端交换。而且,底层传输机制的属性也是可定制的。每个DataChannel可以经过配置提供以下特性:
- 发送消息可靠或部分可靠的交付;
- 发送消息有序或乱序交付。
不可靠的乱序交付等同于原始UDP,即消息可能到达,也可能不到达,而且到达次序也没有保证。然而可让信道部分可靠,也就是设定重传的最大次数或时间限制:WebRTC的各个层负责处理确认和超时。
建立端到端的连接
打开XHR、EventSource或新WebSocket会话很简单:依赖于定义完善的HTTP握手机制协商连接参数,都假定客户端可访问到目标服务器(比如,服务器具有公开的可路由到的IP地址,或客户端和服务器都在一个内部网中)。
WebRTC两端则很可能分别位于自己的私有网络中,中间还隔着一或多层NAT。为了发起会话,首先必须找到两端的候选IP和端口,穿越NAT,检查连接,以期找到可用路径。
要想成功地建立端到端的连接,必须首先解决另外几个问题:
- 必须通知另一端想打开一个端到端的连接,以便它知道开始监听到来的分组;
- 必须找出两端之间建立连接所需的路由线路,并在两端传播这个信息;
- 必须交换有关媒介和数据流的必要信息,如协议、编码等。
WebRTC解决其中一个问题:内置ICE协议会执行必要的路由和连接检查。然而,发送通知(信号)和协商会话仍然要由应用负责。
发信号和协商会话
在检查连接或协商会话前,必须知道能否将信息发送到另一个端,另一端是否愿意建立连接。为此,必须发出一个信号,而另一端必须返回应答。问题来了:如果另一端没有监听数据包,怎么办?最低限度,需要一个共享的发信通道。
WebRTC把发送信号和协议的选择交给应用,而标准有意未给发送信号的过程提供建议或实现。这样可让现有通信基础设施中的其他发信协议能够互操作,包括如下几个协议:
- SIP:Session Initiation Protocol,会话初始协议,应用级发信协议,广泛用于通过IP实现的语音通话(VoIP)和视频会议;
- Jingle:XMPP协议的发信扩展,用于VoIP和视频会议的会话控制;
- ISUP:ISDN User Part,ISDN用户部分,全球各大公共电话交换网中用于启动电话呼叫的发信协议。
WebRTC应用可以选择已有的任何发信协议和网关,利用既有通信系统协商一次通话或视频会议。
发信服务器可以作为已有通信网络的网关,此时由网络负责将连接提议发送给目标端,然后再将应答返回给WebRTC客户端,以初始化信息交换。而应用也可使用自定义发信信道,可能由一或多台服务器和一个自定义通信协议构成:如果两端都连到同一个发信服务,那这个服务就可以为它们传递消息。
可与WebRTC互操作的通信网关,如开源的Asterisk。Asterisk有一个WebSocket模块,该模块支持将SIP作为发信协议,浏览器建立到Asterisk网关的WebSocket连接,然后两者通过交换SIP消息来协商会话。
SDP
Session Description Protocol,会话描述协议。
应用实现共享的发信通道后,接下来就可以发起WebRTC连接:
// 初始化共享的发信通道
var signalingChannel = new SignalingChannel();
var pc = new RTCPeerConnection({});
// 向浏览器请求音频流
navigator.getUserMedia({ "audio": true }, gotStream, logError);
function gotStream(stream) {
// 通过RTCPeerConnection注册本地音频流
pc.addstream(stream);
// 创建端到端连接的SDP(提议)描述
pc.createOffer(function(offer) {
// 以生成的SDP作为端到端连接的本地描述
pc.setLocalDescription(offer);
// 通过发信通道向远端发送SDP提议
signalingChannel.send(offer.sdp);
});
}
function logError() { ... }
WebRTC使用SDP描述端到端连接的参数。SDP不包含媒体本身的任何信息,仅用于描述会话状况,表现为一系列的连接属性:要交换的媒体类型(音频、视频及应用数据)、网络传输协议、使用的编解码器及其设置、带宽及其他元数据。
SDP是一个基于文本的简单协议(RFC 4568),用于描述会话属性。WebRTC应用不必直接处理SDP。JSEP( JavaScript Session Establishment Protocol , JS会话建立协议)定义对RTCPeerConnection对象几个方法的简单调用,就把SDP所有的内部工作全都隐藏起来。生成提议之后,就可通过发信通道将它发送给远端。如何编码SDP取决于应用:SDP字符串可以像前面那样(作为简单的文本blob)直接传输,也可以将它编码成任意格式后再传输。Jingle协议提供从SDP到XMPP(XML)格式的映射。
要建立端到端的连接,两端都必须遵循一个对称的工作流,以交换各自音频、视频及其他数据流的SDP描述。
ICE
端与端之间往往有很多层防火墙和NAT设备阻隔。
查询操作系统获知IP地址(如果有多块网卡,就需要多个IP地址),将IP地址加端口号追加到生成的SDP字符串中。
WebRTC框架可处理大部分复杂工作:
- 每个RTCPeerConnection连接对象都包含一个ICE代理;
- ICE代理负责收集IP地址和端口;
- ICE代理负责执行两端的连接检查;
- ICE代理负责发送连接持久化信息。
设置好会话描述后(无论本地还是远程),本地ICE代理会自动开始发现本地端所有可能的候选IP和端口的进程:
- ICE代理向操作系统查询本地IP地址;
- 如果有配置,ICE代理会查询外部STUN服务器,以取得本地端的公共IP和端口号;
- 如果有配置,ICE代理会将TURN服务器追加为最后一个候选项;假如端到端的连接失败,数据将通过指定的中间设备转发。
每发现一个新候选项(一个IP加一个端口号),代理就会自动通过RTCPeerConnection对象注册它,并通过一个回调函数(onicecandidate)通知应用。ICE在完成收集工作后,也会再触发同一个回调函数,以通知应用。
var ice = {"iceServers": [
// STUN服务器,配置为使用谷歌的公共测试服务器
{"url": "stun:stun.l.google.com:19302"},
// TURN服务器,用于端到端连接失败时转发数据
{"url": "turn:user@turnserver.com", "credential": "pass"}
]};
var signalingChannel = new SignalingChannel();
var pc = new RTCPeerConnection(ice);
navigator.getUserMedia({ "audio": true }, gotStream, logError);
function gotStream(stream) {
pc.addstream(stream);
pc.createOffer(function(offer) {
// 应用本地会话描述:初始化ICE收集过程
pc.setLocalDescription(offer);
});
}
pc.onicecandidate = function(evt) {
// 预订ICE事件,监听ICE收集完成
if (evt.target.iceGatheringState == "complete") {
local.createOffer(function(offer) {
console.log("Offer with ICE candidates: " + offer.sdp);
// 生成SDP提议(此时包含发现的ICE候选项)
signalingChannel.send(offer.sdp);
});
}
}
// 包含ICE候选项的提议:
// a=candidate:1862263974 1 udp 2113937151 192.168.1.73 60834 typ host // 本地端的私有ICE候选项
// a=candidate:2565840242 1 udp 1845501695 50.76.44.100 60834 typ srflx // STUN服务器返回的公有ICE候选项
ICE代理处理大部分复杂工作:ICE收集过程是自动触发的,STUN查找是在后台执行的,而发现的候选项也会自动通过RTCPeerConnection对象注册。上述过程完成后,可生成SDP提议,并通过发信通道发送给另一端。另一端接收到ICE候选项后,就可建立端到端的连接:只要RTCPeerConnection对象设置远程会话描述(包含另一端的一组候选IP和端口号),ICE代理就会执行连接检查,以确定能否抵达另一端。
ICE代理发送消息(STUN绑定请求),另一端接收之后必须以一个成功的STUN响应确认。如果这个过程完成,则代表有一条端到端连接的路由线路!相反,如果所有候选项都绑定失败,要么将RTCPeerConnection标记为失败,要么回退到靠TURN转发服务器建立连接。
ICE代理自动确定连接检查时候选项的次序和优先级:首先检查本地IP地址,然后是公共IP,最后才检查TURN。建立连接后,ICE代理周期性地向另一端发送STUN请求,以此保证连接的持久化。
Trickle ICE
ICE收集过程决不是瞬间就能完成的:取得本地IP地址很快,但查询STUN服务器需要经过到外部服务器的往返,还有另一次端到端的STUN连接检查。Trickle ICE是对ICE协议的扩展,用于在于实现端与端之间的增量收集和连接检查:
- 两端交换没有ICE候选项的SPD提议;
- 发现ICE候选项之后,通过发信通道发送到另一端;
- 新候选描述一就绪,立即执行ICE连接检查。
不等到ICE收集过程完成,而是依靠发信通道向另一端递增地交付更新,从而加快协商。
Trickle ICE导致发信通道的流量增加,但可显著减少初始化端到端连接的时间。所有WebRTC应用都应该考虑这一点:尽快发送提议,然后依次发现依次发送ICE候选项。
跟踪ICE收集和连接状态
内置的ICE框架负责候选项发现、连接检查、持久化等。开发者要做的只是在初始化RTCPeerConnection对象时指定STUN和TURN服务器。
iceGatheringState属性保存本地端候选项的收集状态:
- new:对象刚刚创建,还没有连网;
- gathering:ICE代理正在收集本地候选项;
- complete:ICE代理收集过程完成。
如上图,iceConnectionState属性中保存着端到端的连接状态:
- new:ICE代理正在收集候选项且/或正在等待远程候选项的到来;
- checking:ICE代理至少已经收到来自一个组件的远程候选项,而且正在检查候选项,但尚未发现连接;除检查外,可能仍然在收集;
- connected:ICE代理已经找到一条通过所有组件的可用连接,但仍在检查其他候选项,以确定是否存在更好的连接;此时仍有可能还在收集;
- completed:ICE代理已经完成收集和检查,且发现通过所有组件的连接;
- failed:ICE代理检查完所有候选项,但至少有一个组件的连接失败;其他一些组件的连接可能成功;
- disconnected:一或多个组件的活动检查失败,相对failed更严重,在不稳定的网络上可能会间歇性触发(不需要采取什么行动);
- closed:ICE代理已经关闭,不再响应STUN请求。
ICE代理最重要的目标,就是识别端到端之间的可行路径。可ICE代理不会就此止步。即便是连接已经建立,ICE代理也可能周期性地尝试其他候选项,以确定其他路径的性能是否更好。
Chrome提供一个工具,可检查任何WebRTC连接的工作流和状态。打开标签页chrome://webrtc-internals
,可检查所有打开的端到端连接,查看交换的SDP描述:
完整示例
初始化WebRTC连接
响应WebRTC连接
simpleWebRTC库:使用一个用于穿透NAT的公共STUN服务器初始化了RTCPeerConnection,使用getUserMedia请求音频和视频流,并初始化了连接到它自己发信服务器的WebSocket连接。
交付媒体和应用数据
要实现实时通信,还需要流量控制、拥塞控制、错误校验、带宽预测、延迟机制、通信加密等能力。
WebRTC又在UDP之上增加几层协议:
- DTLS:Datagram Transport Layer Security,数据报传输层安全,用于加密传输应用数据时针对要传输的媒体数据协商密钥;
- SRTP:Secure Real-Time Transport,安全实时传输,用于传输音频和视频流;
- SCTP:Stream Control Transport Protocol,流控制传输协议,用于传输应用数据。
通过DTLS实现安全通信
DTLS本质上就是TLS,为兼容UDP的数据报传输而做一些微小修改。
DTLS解决下列问题:
- TLS要求可靠的有序的适合分段的握手记录以协商信道;
- 如果在混合多个分组的基础上对记录分段,就不能保证TLS的完整性校验;
- 如果记录的顺序不对,也不能保证TLS的完整性校验。
DTLS对TLS记录协议的扩展,就是为每条握手记录明确添加分段偏移字段和序号。这样就满足有序交付的条件,也能让大记录可以被分段成多个分组并在另一端再进行组装。DTLS握手记录严格按照TLS协议规定的顺序传输,顺序不对就报错。DTLS还要处理丢包问题:两端都使用计时器,如果预定时间内没有收到应答,就重传握手记录。
记录序号、偏移值和重传计时器让DTLS在UDP之上实现握手。为保证过程完整,两端都要生成自已签名的证书,然后按照常规的TLS握手协议走。
完整的DTLS握手需要两次往返。即,建立端到端的连接会产生额外延迟。
WebRTC客户端自动为每一端生成自已签名的证书。因此,也就没有证书链需要验证。DTLS保证加密和完整性,但把身份验证工作留给应用。最后在满足握手要求的基础上,DTLS为处理常规记录可能出现的分段和乱序问题,又增加两条重要的规则:
- DTLS记录必须刚好放到一个网络分组中;
- 必须有一个分组密码用于加密记录数据。
常规TLS记录最大可以达到16KB。TCP可以处理分段和组装,但UDP不提供这些服务。结果,为适应UDP协议的乱序发送,也为了最大程度保持其语义,每个携带应用数据的DTLS记录都必须放到一个UDP分组中。类似地,由于它们潜在依赖记录数据的有序发送,因此也不允许使用流密码。
通过SRTP和SRTCP交付媒体
WebRTC以完全托管的形式提供媒体获取和交付服务:从摄像头到网络,再从网络到屏幕。WebRTC应用指定获取流的媒体约束,然后通过RTCPeerConnection对象注册它们。从此以后,就都是浏览器提供的WebRTC媒体和网络引擎的事:编码优化、处理丢包、网络抖动、错误恢复、流量、控制等。
RTP:Real-Time Transport Protocol,实时传输协议,由RFC 3550定义。
WebRTC实际上并不是通过IP网络实时交付音频和视频的第一个应用。WebRTC只是重用了VoIP电话使用的传输协议、通信网关和各种商业或开源的通信服务:
- 安全实时传输协议(SRTP,Secure RTP)通过IP网络交付音频和视频等实时数据的标准安全格式。
- 安全实时控制传输协议(SRTCP,Secure Real-time Control Transport Protocol)通过SRTP流交付发送和接收方统计及控制信息的安全控制协议。
SRTP为通过IP网络交付音频和视频定义标准的分组格式。SRTP本身并不对传输数据的及时性、可靠性或数据恢复提供任何保证机制,它只负责把数字化的音频采样和视频帧用一些元数据封装起来,以辅助接收方处理这些流。
解读:
- 每个SRTP分组都包含一个自动递增的序号,以便接收端检测和发现媒体数据是否乱序;
- 每个SRTP分组都包含一个时间戳,表示媒体净荷第一字节的采样时间,用于多个媒体流(如音频和视频)的同步;
- 每个SRTP分组都包含一个SSRC标识符,这是个别媒体流中每个分组的唯一流ID;
- 每个SRTP分组可以包含其他可选的元数据;
- 每个SRTP分组都包含加密的媒体净荷,以及(可选的)认证标签,后者用于验证分组的完整性。
SRTP分组中包含媒体引擎实时回放流必需的所有信息。而控制每个SRTP分组交付则是SRTCP协议的责任,SRTCP针对每个媒体流实现独立的外部反馈渠道。
SRTCP会跟踪发送及丢失字节和分组的数量,跟踪每个SRTP分组的序号、交错到达抖动,以及其他SRTP统计信息。然后,两端定时交换这些数据,以便调整每个流的发送速率、编码品质和其他参数。
SRTP和SRTCP直接在UDP之上运行,共同完成对应用提供的音频和视频流的实时适配和优化。WebRTC应用不会接触内部的SRTP或SRTCP协议:如果你要构建自定义的WebRTC客户端,那得直接操作这两个协议,否则浏览器会替你搭建好所有必要的基础设施。
关于SRTP和SRTCP,还需要考虑另外一些细节:
- SRTP和SRTCP都会加密应用净荷数据(WebRTC要求),但它们都没有提供协商密钥的机制!这就是为什么必须先进行DTLS握手的原因:DTLS握手会为两端确定一个共享密钥,随后的SRTP和SRTCP可以使用这个密钥。
- SRTP和SRTCP都要求对不同的流分配不同的端口,而这对于NAT或防火墙后面的客户端当然就是一个问题。为解决这个问题,WebRTC使用另一个多路复用扩展,以便向同一个目标端口交付多个流(以及相应的控制信道)。
- IETF还制定一个新的拥塞控制算法,该算法利用SRTCP的反馈对WebRTC应用生成的音频和视频流进行优化。
通过SCTP交付应用数据
WebRTC对RTCDataChannel接口及其传输协议有哪些要求:
- 传输层必须支持多个独立信道的复用:
- 每个信道必须支持有序或乱序交付;
- 每个信道必须支持可靠或不可靠交付;
- 每个信道可以支持应用定义的优先级。
- 传输层必须提供一个面向消息的API:
- 每条应用消息都可能在传输层被分段和组装。
- 传输层必须实现流量和拥塞控制机制。
- 传输层必须保证数据的机密性和完整性。
TCP、UDP与SCTP比较
对比项目 | TCP | UDP | SCTP |
---|---|---|---|
可靠性 | 可靠 | 不可靠 | 可配置 |
交付次序 | 有序 | 乱序 | 可配置 |
传输方式 | 面向字节 | 面向消息 | 面向消息 |
流量控制 | 支持 | 不支持 | 支持 |
拥塞控制 | 支持 | 不支持 | 支持 |
DataChannel
设置与协商
配置消息次序和可靠性
部分可靠交付与消息大小
使用场景及性能
音频、视频和数据流
多方通信架构
基础设施及容量规划
数据效率及压缩
性能检查表
注意事项:
- 发信服务
- 使用低延迟传输机制;
- 提供足够的容量;
- 建立连接后,考虑使用DataChannel发信。
- 防火墙和NAT穿越
- 初始化RTCPeerConnection时提供STUN服务器;
- 尽可能使用增量ICE,虽然发信次数多,但建立连接速度快;
- 提供STUN服务器,以备端到端连接失败后转发数据;
- 预计并保证TURN转发时容量足够用。
- 数据分发
- 对于大型多方通信,考虑使用超级节点或专用的中间设备;
- 中间设备在转发数据前,考虑先对其进行优化或压缩。
- 数据效率
- 对音频和视频流指定适当的媒体约束;
- 优化通过DataChannel发送的二进制净荷;
- 考虑压缩通过DataChannel发送的UTF-8数据;
- 监控DataChannel缓冲数据的量,同时注意适应网络条件变化。
- 交付及可靠性
- 使用乱序交付避免队首阻塞;
- 如果使用有序交付,把消息大小控制到最小,以降低队首阻塞的影响;
- 发送小消息(<1150字节),以便将分段应用消息造成的丢包损失降至最低;
- 对部分可靠交付,设置适当的重传次数和超时间隔;
- 正确地设置取决于消息大小、应用数据类型和端与端之间的延迟。