这一篇我们来讲一下WebRTC协议,之前我总结过一篇各种网络协议的总结,没看过的朋友建议先看下这篇web知识梳理,有助于加深这篇关于WebRTC的理解。
WebRTC是由Google主导的,由一组标准、协议和JavaScript API组成,用于实现浏览器之间(端到端之间)的音频、视频及数据共享。WebRTC不需要安装任何插件,通过简单的JavaScript API就可以使得实时通信变成一种标准功能。
现在各大浏览器以及终端已经逐渐加大对WebRTC技术的支持。下图是webrtc官网给出的现在已经提供支持了的浏览器和平台。
在深入讲解协议之前,我们先来看实例。我们先来看下在Android中实现一个WebRTC的代码示例。
首先,引入WebRTC依赖包,这里我是使用Nodejs下的socket.io库实现WebRTC信令服务器的,所以也要引入socket.io依赖包。
dependencies {
implementation 'io.socket:socket.io-client:1.0.0'
implementation 'org.webrtc:google-webrtc:1.0.+'
implementation 'pub.devrel:easypermissions:1.0.0'
}
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(getApplicationContext())
.setEnableVideoHwAcceleration(true)
.createInitializationOptions());
//创建PeerConnectionFactory
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
mPeerConnectionFactory = new PeerConnectionFactory(options);
//设置视频Hw加速,否则视频播放闪屏
mPeerConnectionFactory.setVideoHwAccelerationOptions(mEglBase.getEglBaseContext(), mEglBase.getEglBaseContext());
private void initConstraints() {
iceServers = new LinkedList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:23.21.150.121").createIceServer());
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
pcConstraints = new MediaConstraints();
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
sdpConstraints = new MediaConstraints();
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
}
布局文件写两个控件,一个显示本地视频流,一个显示远端视频流。
并对这两个控件进行一些基础设置
//初始化localView
localView.init(mEglBase.getEglBaseContext(), null);
localView.setKeepScreenOn(true);
localView.setMirror(true);
localView.setZOrderMediaOverlay(true);
localView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
localView.setEnableHardwareScaler(false);
//初始化remoteView
remoteView.init(mEglBase.getEglBaseContext(), null);
remoteView.setMirror(false);
remoteView.setZOrderMediaOverlay(true);
remoteView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
remoteView.setEnableHardwareScaler(false);
mVideoCapturer = createVideoCapture(this);
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(mVideoCapturer);
mVideoTrack = mPeerConnectionFactory.createVideoTrack("videtrack", videoSource);
//设置视频画质 i:width i1 :height i2:fps
mVideoCapturer.startCapture(720, 1280, 30);
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack("audiotrack", audioSource);
//播放本地视频
mVideoTrack.addRenderer(new VideoRenderer(localView));
//创建媒体流并加入本地音视频
mMediaStream = mPeerConnectionFactory.createLocalMediaStream("localstream");
mMediaStream.addTrack(mVideoTrack);
mMediaStream.addTrack(mAudioTrack);
要想从远端获取数据,我们就必须创建 PeerConnection 对象。该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。
PeerConnection peerConnection = factory.createPeerConnection(
iceServers, //ICE服务器列表,干什么用下面会详细解释
constraints, //MediaConstraints
this); //Context
注意这里要把地址换成你的服务端的地址,我这里WebRTC信令服务端使用的NodeJS编写,然后用的是自己本地的tomcat地址。
//连接服务器
try {
mSocket = IO.socket("http://192.168.31.172:3000/");
} catch (URISyntaxException e) {
e.printStackTrace();
}
mSocket.on("SomeOneOnline", new Emitter.Listener() {
@Override
public void call(Object... args) {
isOffer = true;
if (mPeer == null) {
mPeer = new Peer();
}
mPeer.peerConnection.createOffer(mPeer, sdpConstraints);
}
}).on("IceInfo", new Emitter.Listener() {
@Override
public void call(Object... args) {
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
IceCandidate candidate = null;
candidate = new IceCandidate(
jsonObject.getString("id"),
jsonObject.getInt("label"),
jsonObject.getString("candidate")
);
mPeer.peerConnection.addIceCandidate(candidate);
} catch (JSONException e) {
e.printStackTrace();
}
}
}).on("SdpInfo", new Emitter.Listener() {
@Override
public void call(Object... args) {
if (mPeer == null) {
mPeer = new Peer();
}
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
SessionDescription description = new SessionDescription
(SessionDescription.Type.fromCanonicalForm(jsonObject.getString("type")),
jsonObject.getString("description"));
mPeer.peerConnection.setRemoteDescription(mPeer, description);
if (!isOffer) {
mPeer.peerConnection.createAnswer(mPeer, sdpConstraints);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
mSocket.connect();
@Override
public void onAddStream(MediaStream mediaStream) {
remoteVideoTrack = mediaStream.videoTracks.get(0);
remoteVideoTrack.addRenderer(new VideoRenderer(remoteView));
}
/**
*DataChannel.Init 可配参数说明:
*ordered:是否保证顺序传输;
*maxRetransmitTimeMs:重传允许的最长时间;
*maxRetransmits:重传允许的最大次数;
**/
DataChannel.Init init = new DataChannel.Init();
dataChannel = peerConnection.createDataChannel("dataChannel", init);
发送消息:
byte[] msg = message.getBytes();
DataChannel.Buffer buffer = new DataChannel.Buffer(
ByteBuffer.wrap(msg),
false);
dataChannel.send(buffer);
onMessage()回调收消息:
ByteBuffer data = buffer.data;
byte[] bytes = new byte[data.capacity()];
data.get(bytes);
String msg = new String(bytes);
附上两张手绘流程图,清晰的一丿:
下面这张图清楚的描述了WebRTC的协议分层,该图引自《web性能权威指南》,如有侵权,立马删掉。
下面我们就一点点来剖析WebRTC。
WebRTC实时通信传输音视频的场景,讲究的是实时,当下,处理音频和视频流的应用一定要补偿间歇性的丢包,所以实时性的需求是大于可靠性的需求的。
如果使用TCP当传输层协议的话,如果中间出现丢包的情况,那么后续的所有的包都会被缓冲起来,因为TCP讲究可靠、有序,如果不清楚的朋友可以去看我上一篇关于TCP的内容讲解,web知识梳理。而UDP则正好相反,它只负责有什么消息我就传过去,不负责安全,不负责有没有到达,不负责交付顺序,这里从底层来看是满足WebRTC的需求的,所以WebRTC是采用UDP来当它的传输层协议的。
当然这里UDP只是作为传输层的基础,想要真正的达到WebRTC的要求,我们就要来分析在传输层之上,WebRTC做了哪些操作,用了哪些协议,来达到WebRTC的要求了。
RTCPeerConnection代表一个由本地计算机到远端的WebRTC连接。
该接口提供了创建,保持,监控,关闭连接的方法的实现,简而言之就是代表了端到端之间的一条通道。api调用上面代码里已经看到了,接下来我们就一点点的来剖析这条通道都用了哪些协议。
上面我们也提到了UDP其实只是在IP层的基础上做了一些简单封装而已。而WebRTC如果要实现端到端的通信效果的话,必定要面临端到端之间很多层防火墙,NAT设备阻隔这些一系列的问题。之前我试过写了原生的webrtc 发现只要不在同一段局域网下面,经常会出现掉线连不上的情况。相同的道理这里就需要做 NAT 穿透处理了。
NAT穿透是啥,在讲NAT穿透之前我们需要先提几个概念:
就是这个NAT(NetWork Address Translation),它允许单个设备(比如路由器)充当Internet(公有IP)和专有网络(私有IP)之间的代理。所以我们就可以通过这个NAT来处理很多层防火墙后那个设备是私有IP的问题。
路通了,那么就有另一个问题了,两个WebRTC客户端之间,大概率会存在A不知道B的可以直接发送到的IP地址和端口,B也不知道A的,那么又该如何通信呢?
这就要说到ICE了,也就是交互式连接建立。ICE允许WebRTC克服显示网络复杂性的框架,找到连接同伴的最佳途径并连接起来。
在大多数的情况下,ICE将会使用STUN服务器,其实使用的是在STUN服务器上运行的STUN协议,它允许客户端发现他们的公共IP地址以及他们所支持的NAT类型,所以理所当然STUN服务器必须架设在公网上。在大多数情况下,STUN服务器仅在连接设置期间使用,并且一旦建立该会话,媒体将直接在客户端之间流动。
具体过程让我们来看图会更清楚,WebRTC两个端各自有一个STUN服务器,通过STUN服务器来设置连接,一旦建立连接会话,媒体数据就可以直接在两个端之间流动。
刚才也说了大多数的情况,如果发生STUN服务器无法建立连接的情况的话,ICE将会使用TURN中继服务器,TURN是STUN的扩展,它允许媒体遍历NAT,而不会执行STUN流量所需的“一致打孔”,TURN服务器实际上在WebRTC对等体之间中继媒体,所以我这里理解的话使用TURN就很难被称为端对端之间通信了。
同样,我们画个图来形容TURN中继服务器的数据流动方式:
TURN 是在任何网络中为两端提供连接的最可靠方式,但现实情况下运维 TURN 服务器的投入也很大。因此,最好在其他直连手段都失败的情况下,再使用 TURN。 所以一般情况下每个WebRTC解决方案都会准备好支持这两种服务类型,并设计为处理TURN服务器上的处理要求。
WebRTC 以完全托管的形式提供媒体获取和交付服务:从摄像头到网络,再从网络到屏幕。
从上面的Android demo中我们可以看到,我们除了一开始制定媒体流的约束以外,编码优化、处理丢包、网络抖动、错误恢复、流量、控制等等操作我们都没做,都是WebRTC自己来控制的。这里WebRTC 是怎么优化和调整媒体流的品质的呢?
其实WebRTC 只是重用了 VoIP 电话使用的传输 协议、通信网关和各种商业或开源的通信服务:
安全实时传输协议(SRTP,Secure Real-time Transport Protocol) 通过 IP 网络交付音频和视频等实时数据的标准安全格式。
安全实时控制传输协议(SRTCP,Secure Real-time Control Transport Protocol) 通过 SRTP 流交付发送和接收方统计及控制信息的安全控制协议。
我们都知道UDP是不安全的,但是WebRTC要求所有传输的数据(音频、视频和自定义应用数据)都必须加密,所以这里就要引入一个DTLS协议的概念。
DTLS说白了,其实就是因为TLS无法保证UDP上传输的数据的安全,所以在现存的TLS协议架构上提出了扩展,用来支持UDP。其实就是TLS的一个支持数据报传输的版本。
既然知道了DTLS可以说是TLS的扩展版以后,我们再来看看dtls解决了哪些问题。首先先来看TLS的问题,刚才我们也提到了TLS不能直接用于数据报环境,主要的原因是包可能会出现丢失或者重排序的情况,而TLS无法处理这种不可靠性,而无法处理这种不可靠性就带来了两个问题:
那么DTLS是如何在尽可能与TLS相同的情况下解决以上两个问题的呢?
首先在DTLS中,每个握手消息都会在握手的时候分配一个序列号和分段偏移字段,当收消息的那一方收到一个握手消息的时候,会根据这个序列号来判断是否是期望的下一个消息,如果不是则放入队列中,这样就满足了有序交付的条件,如果顺序不对就报错,跟TLS一样。而分段偏移字段是为了补偿UDP报文的1500字节大小限制问题。
至于丢包问题,DTLS采用了两端都使用一个简单的重传计时器的方法,还是上面序列号为1到10的例子,如果A发给B一个序列号为5的消息,然后希望从B那里获取到序列号为6的消息,但是没收到,超时了,A就知道他发的5或者B给的6这个消息丢失了,然后就会重新发送一个重传包,也就是5这个消息。
为保证过程完整,A和B两端都要生成自已签名的证书,而WebRTC会自动为每一端生成自已签名的证书,然后按照常规的 TLS 握手协议走。
除了传输音频和视频数据,WebRTC 还支持通过 DataChannel API 在端到端之间传 输任意应用数据。DataChannel 依赖于 SCTP(Stream Control Transmission Protocol,流控制传输协议),而 SCTP 在两端之间建立的 DTLS 信道之上运行的。
DataChannel api调用和WebSocket类似,上面的Android项目中我们已经讲过了,接下来我们来详细讲下DataChannel所依赖的SCTP协议。
SCTP同时具备了TCP和UDP中最好的功能:面向消息的 API、可配置的可靠性及交付语义,而且内置流量和拥塞控制机制。
因为本身UDP相对TCP来说比较简单,之前也提到UDP只是对IP层的一个简单封装而已,所以这里我们就通过比较TCP和SCTP的区别来简单的讲讲SCTP到底是什么东西和为什么具备TCP和UDP两者最好的功能。
TCP是单流有序传输,SCTP是多流可配置传输
上一篇web知识梳理中我们也讲过,TCP在一条连接中可以复用TCP连接,是单流的,而且是有顺序的,如果一条消息出了问题的话,后面的所有消息都会出现阻塞的情况,强调顺序。而SCTP可以区分多条不同的流,不同的流之间传输数据互不干扰,在有序和无序的问题上,SCTP是可配置的,又可以像TCP那样交付次序可序化,也可以像UDP那样乱序交付。
TCP是单路径传输,SCTP是多路径传输
SCTP两端之间的连接可以绑定多条IP,只要有一条连接是通的,那么就是通的,熟悉TCP的朋友应该都知道,TCP之间只能用一个IP来连接。
TCP连接建立是三次握手,SCTP则需要四次握手
上一篇web知识梳理中我们也已经讲过TCP的三次握手了,SCTP的四次握手比TCP多了一个步骤:server端在收到连接请求时,不会像TCP三次握手那样子收到请求消息以后立马分配内存,将其缓存起来,而是返回一个COOKIE消息。
client端需要回送这个COOKIE,server端对这个COOKIE进行校验以后,从cookie中重新获取有效信息(比如对端地址列表),两端之间才会连接成功。
TCP以字节为单位传输,SCTP以数据块为单位传输
块是SCTP 分组中的最小通信单位,核心概念与HTTP 2.0分帧层中的那些概念基本一样,没看过的朋友可以参考web知识梳理
ok,到这里基本把WebRTC通信协议的应用,以及要用到的协议啊概念啊什么的都过了一遍,要实现低延迟的,端到端的通信传输不是一件容易的事情。相信随着WebRTC不断的完善,支持的端也会越来越多,性能也会越来越完善。
参考资料: