這一篇咱們來說一下WebRTC協議,以前我總結過一篇各類網絡協議的總結,沒看過的朋友建議先看下這篇web知識梳理,有助於加深這篇關於WebRTC的理解。android
WebRTC是由Google主導的,由一組標準、協議和JavaScript API組成,用於實現瀏覽器之間(端到端之間)的音頻、視頻及數據共享。WebRTC不須要安裝任何插件,經過簡單的JavaScript API就可使得實時通訊變成一種標準功能。web
如今各大瀏覽器以及終端已經逐漸加大對WebRTC技術的支持。下圖是webrtc官網給出的如今已經提供支持了的瀏覽器和平臺。 json
在深刻講解協議以前,咱們先來看實例。咱們先來看下在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.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"));
}
複製代碼
佈局文件寫兩個控件,一個顯示本地視頻流,一個顯示遠端視頻流。緩存
<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 = 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.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實時通訊傳輸音視頻的場景,講究的是實時,當下,處理音頻和視頻流的應用必定要補償間歇性的丟包,因此實時性的需求是大於可靠性的需求的。
若是使用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中繼服務器的數據流動方式:
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不斷的完善,支持的端也會愈來愈多,性能也會愈來愈完善。