WebRTC实时通讯协议详解 | 掘金技术征文

这一篇咱们来说一下WebRTC协议,以前我总结过一篇各类网络协议的总结,没看过的朋友建议先看下这篇web知识梳理,有助于加深这篇关于WebRTC的理解。android

基本概念

WebRTC是由Google主导的,由一组标准、协议和JavaScript API组成,用于实现浏览器之间(端到端之间)的音频、视频及数据共享。WebRTC不须要安装任何插件,经过简单的JavaScript API就可使得实时通讯变成一种标准功能。web

如今各大浏览器以及终端已经逐渐加大对WebRTC技术的支持。下图是webrtc官网给出的如今已经提供支持了的浏览器和平台。 json

webrtc官网给出的如今支持webrtc的浏览器和平台

Android上实现一个WebRTC项目

在深刻讲解协议以前,咱们先来看实例。咱们先来看下在Android中实现一个WebRTC的代码示例。api

引入依赖包

首先,引入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

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());
复制代码

设置相关ICE设置

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

    }
复制代码

初始化控件

布局文件写两个控件,一个显示本地视频流,一个显示远端视频流。缓存

<org.webrtc.SurfaceViewRenderer
        android:id="@+id/view_local"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_gravity="center_horizontal"/>

    <org.webrtc.SurfaceViewRenderer
        android:id="@+id/view_remote"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="50dp"/>
复制代码

并对这两个控件进行一些基础设置tomcat

//初始化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 peerConnection = factory.createPeerConnection(
            iceServers,     //ICE服务器列表,干什么用下面会详细解释
            constraints,   //MediaConstraints
            this);              //Context
复制代码

链接服务器

注意这里要把地址换成你的服务端的地址,我这里WebRTC信令服务端使用的NodeJS编写,而后用的是本身本地的tomcat地址。bash

//链接服务器
        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进行数据传递

/**
*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协议详解

下面这张图清楚的描述了WebRTC的协议分层,该图引自《web性能权威指南》,若有侵权,立马删掉。

WebRTC协议分层
下面咱们就一点点来剖析WebRTC。

传输层协议

WebRTC实时通讯传输音视频的场景,讲究的是实时,当下,处理音频和视频流的应用必定要补偿间歇性的丢包,因此实时性的需求是大于可靠性的需求的。

若是使用TCP当传输层协议的话,若是中间出现丢包的状况,那么后续的全部的包都会被缓冲起来,由于TCP讲究可靠、有序,若是不清楚的朋友能够去看我上一篇关于TCP的内容讲解,web知识梳理。而UDP则正好相反,它只负责有什么消息我就传过去,不负责安全,不负责有没有到达,不负责交付顺序,这里从底层来看是知足WebRTC的需求的,因此WebRTC是采用UDP来当它的传输层协议的。

固然这里UDP只是做为传输层的基础,想要真正的达到WebRTC的要求,咱们就要来分析在传输层之上,WebRTC作了哪些操做,用了哪些协议,来达到WebRTC的要求了。

RTCPeerConnection通道

RTCPeerConnection表明一个由本地计算机到远端的WebRTC链接。 该接口提供了建立,保持,监控,关闭链接的方法的实现,简而言之就是表明了端到端之间的一条通道。api调用上面代码里已经看到了,接下来咱们就一点点的来剖析这条通道都用了哪些协议。

P2P内网穿透

上面咱们也提到了UDP其实只是在IP层的基础上作了一些简单封装而已。而WebRTC若是要实现端到端的通讯效果的话,一定要面临端到端之间不少层防火墙,NAT设备阻隔这些一系列的问题。以前我试过写了原生的webrtc 发现只要不在同一段局域网下面,常常会出现掉线连不上的状况。相同的道理这里就须要作 NAT 穿透处理了。

NAT穿透是啥,在讲NAT穿透以前咱们须要先提几个概念:

  • 公有IP地址是在Internet上全局惟一的IP地址,仅有一个设备可能拥有公有IP地址。
  • 私有IP地址是非全局的惟一的IP地址,可能同时存在于不少不一样的设备上。私有IP地址永远不会直接链接到internet。那私有IP地址的设备如何访问网络呢?

就是这个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没法处理这种不可靠性,而没法处理这种不可靠性就带来了两个问题:

  1. TLS没法对某个记录单独解密,什么意思呢?若是A和B之间传递消息,消息以1到10依次为序列号,若是消息5没收到,那么6之后的消息都会报错,这里没法经过TLS的完整性校验,并且若是顺序不对,也没法经过TLS的完整性校验。
  2. TLS握手层假定握手消息是可靠投递的,若是消息丢失则会中断。

那么DTLS是如何在尽量与TLS相同的状况下解决以上两个问题的呢?

首先在DTLS中,每一个握手消息都会在握手的时候分配一个序列号和分段偏移字段,当收消息的那一方收到一个握手消息的时候,会根据这个序列号来判断是不是指望的下一个消息,若是不是则放入队列中,这样就知足了有序交付的条件,若是顺序不对就报错,跟TLS同样。而分段偏移字段是为了补偿UDP报文的1500字节大小限制问题。

至于丢包问题,DTLS采用了两端都使用一个简单的重传计时器的方法,仍是上面序列号为1到10的例子,若是A发给B一个序列号为5的消息,而后但愿从B那里获取到序列号为6的消息,可是没收到,超时了,A就知道他发的5或者B给的6这个消息丢失了,而后就会从新发送一个重传包,也就是5这个消息。

为保证过程完整,A和B两端都要生成自已签名的证书,而WebRTC会自动为每一端生成自已签名的证书,而后按照常规的 TLS 握手协议走。

DataChannel

除了传输音频和视频数据,WebRTC 还支持经过 DataChannel API 在端到端之间传 输任意应用数据。DataChannel 依赖于 SCTP(Stream Control Transmission Protocol,流控制传输协议),而 SCTP 在两端之间创建的 DTLS 信道之上运行的。

DataChannel api调用和WebSocket相似,上面的Android项目中咱们已经讲过了,接下来咱们来详细讲下DataChannel所依赖的SCTP协议。

SCTP

SCTP同时具有了TCP和UDP中最好的功能:面向消息的 API、可配置的可靠性及交付语义,并且内置流量和拥塞控制机制。

由于自己UDP相对TCP来讲比较简单,以前也提到UDP只是对IP层的一个简单封装而已,因此这里咱们就经过比较TCP和SCTP的区别来简单的讲讲SCTP究竟是什么东西和为何具有TCP和UDP二者最好的功能。

  1. TCP是单流有序传输,SCTP是多流可配置传输 上一篇web知识梳理中咱们也讲过,TCP在一条链接中能够复用TCP链接,是单流的,并且是有顺序的,若是一条消息出了问题的话,后面的全部消息都会出现阻塞的状况,强调顺序。而SCTP能够区分多条不一样的流,不一样的流之间传输数据互不干扰,在有序和无序的问题上,SCTP是可配置的,又能够像TCP那样交付次序可序化,也能够像UDP那样乱序交付。

  2. TCP是单路径传输,SCTP是多路径传输 SCTP两端之间的链接能够绑定多条IP,只要有一条链接是通的,那么就是通的,熟悉TCP的朋友应该都知道,TCP之间只能用一个IP来链接。

  3. TCP链接创建是三次握手,SCTP则须要四次握手 上一篇web知识梳理中咱们也已经讲过TCP的三次握手了,SCTP的四次握手比TCP多了一个步骤:server端在收到链接请求时,不会像TCP三次握手那样子收到请求消息之后立马分配内存,将其缓存起来,而是返回一个COOKIE消息。 client端须要回送这个COOKIE,server端对这个COOKIE进行校验之后,从cookie中从新获取有效信息(好比对端地址列表),两端之间才会链接成功。

  4. TCP以字节为单位传输,SCTP以数据块为单位传输 块是SCTP 分组中的最小通讯单位,核心概念与HTTP 2.0分帧层中的那些概念基本同样,没看过的朋友能够参考web知识梳理

ok,到这里基本把WebRTC通讯协议的应用,以及要用到的协议啊概念啊什么的都过了一遍,要实现低延迟的,端到端的通讯传输不是一件容易的事情。相信随着WebRTC不断的完善,支持的端也会愈来愈多,性能也会愈来愈完善。

Agora SDK 使用体验征文大赛 | 掘金技术征文,征文活动正在进行中

相关文章
相关标签/搜索