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 使用體驗徵文大賽 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索