您好,登錄后才能下訂單哦!
這篇文章主要介紹“什么是RTMP協(xié)議”,在日常操作中,相信很多人在什么是RTMP協(xié)議問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”什么是RTMP協(xié)議”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
實(shí)時(shí)消息傳輸協(xié)議(Real-Time Messaging Protocol)是目前直播的主要協(xié)議,是Adobe公司為Flash播放器和服務(wù)器之間提供音視頻數(shù)據(jù)傳輸服務(wù)而設(shè)計(jì)的應(yīng)用層私有協(xié)議。RTMP協(xié)議是目前各大云廠商直線直播業(yè)務(wù)所公用的基本直播推拉流協(xié)議,隨著國內(nèi)直播行業(yè)的發(fā)展和5G時(shí)代的到來,對RTMP協(xié)議有基本的了解,也是我們程序員必須要掌握的基本技能。
本文主要闡述RTMP的基本思想和核心概念,并且輔之以livego的源碼分析,和大家一起深入學(xué)習(xí)RTMP協(xié)議最核心的知識(shí)點(diǎn)。
RTMP協(xié)議主要的特點(diǎn)有:多路復(fù)用,分包和應(yīng)用層協(xié)議。以下將對這些特點(diǎn)進(jìn)行詳細(xì)的描述。
多路復(fù)用(multiplex)指的是信號發(fā)送端通過一個(gè)信道同時(shí)傳輸多路信號,然后信號接收端將一個(gè)信道中傳遞過來的多個(gè)信號分別組合起來,分別形成獨(dú)立完整的信號信息,以此來更加有效地使用通信線路。
簡而言之,就是在一個(gè) TCP 連接上,將需要傳遞的Message分成一個(gè)或者多個(gè) Chunk,同一個(gè)Message 的多個(gè)Chunk 組成 ChunkStream,在接收端,再把 ChunkStream 中一個(gè)個(gè) Chunk 組合起來就可以還原成一個(gè)完整的 Message,這就是多路復(fù)用的基本理念。
上圖是一個(gè)簡單例子,假設(shè)需要傳遞一個(gè)300字節(jié)長的Message,我們可以將其拆分成3個(gè)Chunk,每一個(gè)Chunk可以分成 Chunk Header 和 Chunk Data。在Chunk Header 里我們可以標(biāo)記這個(gè)Chunk中的一些基本信息,如 Chunk Stream Id 和 Message Type;Chunk Data 就是原始信息,上圖中將 Message 分成128+128+44 =300,這樣就可以完整的傳輸這個(gè)Message了。
關(guān)于 Chunk Header 和 Chunk Data 的格式,后文會(huì)進(jìn)行詳細(xì)介紹。
RTMP協(xié)議的第二個(gè)大的特性就是分包,與RTSP協(xié)議相比,分包是RTMP的一個(gè)特點(diǎn)。與普通的業(yè)務(wù)應(yīng)用層協(xié)議(如:RPC協(xié)議)不一樣的是,在多媒體網(wǎng)絡(luò)傳輸案例中,絕大多數(shù)的多媒體傳輸?shù)囊纛l和視頻的數(shù)據(jù)包都相對比較偏大,在TCP這種可靠的傳輸協(xié)議之上進(jìn)行大的數(shù)據(jù)包傳遞,很有可能阻塞連接,導(dǎo)致優(yōu)先級更高的信息無法傳遞,分包傳輸就是為了解決這個(gè)問題而出現(xiàn)的,具體的分包格式,下文會(huì)有介紹。
RTMP最后的一個(gè)特性,就是應(yīng)用層協(xié)議。RTMP協(xié)議默認(rèn)基于傳輸層協(xié)議TCP而實(shí)現(xiàn),但是在RTMP的官方文檔中,只給定了標(biāo)準(zhǔn)的數(shù)據(jù)傳輸格式說明和一些具體的協(xié)議格式說明,并沒有具體官方的完整實(shí)現(xiàn),這就催生出了很多相關(guān)的其他業(yè)內(nèi)實(shí)現(xiàn),例如RTMP over UDP等等相關(guān)的私有改編的協(xié)議出現(xiàn),給了大家更多的可擴(kuò)展的空間,方便大家解決原生RTMP存在的直播時(shí)延等問題。
作為一種應(yīng)用層協(xié)議,和其他私有傳輸協(xié)議一樣(如RPC協(xié)議),RTMP也有一些具體代碼實(shí)現(xiàn),如 nginx-rtmp、livego 和 srs。本文選用基于go語言實(shí)現(xiàn)的開源直播服務(wù)器 livego 進(jìn)行源碼級的主流程分析,和大家一起深入學(xué)習(xí) RTMP 推拉流的核心流程的實(shí)現(xiàn),幫助大家對RTMP的協(xié)議有一個(gè)整體的理解。
在進(jìn)行源碼分析之前,我們會(huì)通過類比RPC協(xié)議的方式,幫助大家對RTMP協(xié)議的格式有一個(gè)基本的了解,首先我們可以看一個(gè)比較簡單但實(shí)用的RPC協(xié)議格式,如下圖所示:
我們可以看到這是一個(gè)在RPC調(diào)用過程中所使用的數(shù)據(jù)傳輸格式,之所以使用這樣的格式,根本目的還是為了解決"粘包和拆包"的問題。
以下簡要描述圖中RPC協(xié)議的格式:首先用2個(gè)字節(jié),MAGIC來表示魔數(shù),標(biāo)記該協(xié)議是對端都能識(shí)別的標(biāo)識(shí),如果接收到的2個(gè)字節(jié)不是0xbabe的話,則直接丟棄該包;第二個(gè)sign占用1個(gè)字節(jié),低4位表示消息的類型request/response/heartbeat,高4位表示序列化類型例如json,hessian,protobuf,kyro等等;第三個(gè) status 占用一個(gè)字節(jié),表示狀態(tài)位;隨后使用8個(gè)字節(jié)來表示調(diào)用的requestId,一般使用低48位(2的48次方)就足夠表示requestId了;接著使用4字節(jié)定長的body size來表示Body Content,通過這樣的方式就能夠很快的解析出RPC消息Message的完整請求對象了。
通過分析上述的一個(gè)簡單的RPC協(xié)議,其實(shí)我們能夠發(fā)現(xiàn)一個(gè)很好的思想,就是最大效率的使用字節(jié),即使用最小的字節(jié)數(shù)組,來傳輸最多的數(shù)據(jù)信息。小小的一個(gè)字節(jié)能夠帶來很多的信息量,畢竟一個(gè)字節(jié)它有64種不同的變化。在網(wǎng)絡(luò)中,如果只需要利用一個(gè)字節(jié)就能夠傳遞很多有用的信息的話,那么我們就可以使用極其有限的資源來得到最大的資源利用了。RTMP的官方文檔在2012年就出現(xiàn)了,雖然以目前的眼光來看,RTMP協(xié)議實(shí)現(xiàn)的非常復(fù)雜,甚至有些臃腫,但是它在2012年的時(shí)候,就能夠有比較先進(jìn)的思想,的確是我們學(xué)習(xí)的榜樣。
在當(dāng)今WebRTC協(xié)議橫行的年代里,我們也能夠從WebRTC的設(shè)計(jì)實(shí)現(xiàn)中,看到RTMP的影子,上述的RPC協(xié)議我們就可以認(rèn)為是一個(gè)與RTMP具有相似設(shè)計(jì)理念的簡化版設(shè)計(jì)。
在分析RTMP源碼之前,我們先對RTMP協(xié)議中的幾個(gè)核心概念做具體說明,方便我們在宏觀上對RTMP整個(gè)協(xié)議棧有一個(gè)基本的了解,并且在后文源碼分析期間,我們也會(huì)通過抓包的方式,更加直觀地幫助我們?nèi)シ治鱿嚓P(guān)的原理。
首先,和剛才的RPC協(xié)議格式一樣,RTMP實(shí)際傳輸?shù)膶?shí)體對象是Chunk,一個(gè)Chunk由Chunk Header和Chunk Body兩個(gè)部分組成,如下圖所示。
Chunk Header這個(gè)部分和我們前面說過的RPC協(xié)議不太一樣,主要是RTMP協(xié)議的Chunk Header的長度不是固定的,為什么不是固定的呢?其實(shí)還是Adobe公司為了節(jié)省數(shù)據(jù)傳輸開銷。從剛才將一個(gè)300字節(jié)的Message拆分成3個(gè)Chunk的例子中,我們可以看到多路復(fù)用其實(shí)也是有一個(gè)比較明顯的缺點(diǎn),就是我們需要有一個(gè)Chunk Header來標(biāo)記這個(gè)Chunk的基本信息,這樣其實(shí)就是在傳輸?shù)臅r(shí)候有了額外字節(jié)流傳輸?shù)拈_銷。所以為了保證傳輸?shù)淖止?jié)數(shù)最少,我們就需要不斷地壓榨著RTMP的Header的大小,確保Header的大小達(dá)到最小,這樣才能達(dá)到最高的傳輸效率。
首先我們研究一下Chunk Header中Basic Header的部分,Basic Header的長度就是不固定的,可以是1個(gè)字節(jié),2個(gè)字節(jié)或者3個(gè)字節(jié),這取決于Chunk Stream Id(縮寫:csid)。
RTMP協(xié)議支持的csid的范圍是2~65599,0和1是協(xié)議保留值,用戶不可使用。Basic Header至少含有1個(gè)字節(jié)(低8位),它的長度就是這1個(gè)字節(jié)決定的,如下圖所示。該字節(jié)高2位留給 fmt,fmt的取值決定了 Message Header 的格式,這個(gè)在后面會(huì)講到。該字節(jié)的低6位就是 csid 的值,當(dāng)?shù)?位的 csid 取值為0時(shí),表示真實(shí) csid 值大到無法用6個(gè)bit表示了,需要借助后續(xù)的一個(gè)字節(jié)才行;當(dāng)?shù)?位的 csid 取值為1時(shí),表示真實(shí) csid 值大到無法用14個(gè)bit表示了,需要再借助后續(xù)的一個(gè)字節(jié)才行。于是,整個(gè)Basic Header的長度看起來就不是固定的了,完全取決于首字節(jié)的低6位的csid的值。
實(shí)際應(yīng)用中,并沒有使用到那么多csid,也就是說一般情況下,Basic Header長度為一個(gè)字節(jié),csid取值范圍為 2~63。
剛才說了那么多,才僅僅說了Basic Header,而Basci Header只是Chunk Header的組成部分之一,比較喜歡折騰的RTMP協(xié)議的作者,把RTMP的Chunk Header模塊又設(shè)計(jì)成了動(dòng)態(tài)大小的,簡而言之也是為了節(jié)省傳輸空間,這邊能夠方便理解的地方就是Chunk Message Header的長度也分四種情況,這就是前面提到的 fmt 這個(gè)值決定的。
Message Header 的四種格式如下圖所示:
當(dāng) fmt 為 0 的時(shí)候,Message Header占用11個(gè)字節(jié)(請注意,這邊的11個(gè)字節(jié)不包括Basic Header的長度),由3個(gè)字節(jié)長度的timestamp,3個(gè)字節(jié)長度的message length,1個(gè)字節(jié)長度的message type Id,4個(gè)字節(jié)長度的message stream Id所組成的。
其中,timestamp 是絕對時(shí)間戳,表示的是這個(gè)消息發(fā)送的時(shí)間;message length 表示的是chunk body的長度;message type id 表示的是消息類型,這個(gè)在后文會(huì)具體講到;message stream id 是消息唯一標(biāo)識(shí)。這邊需要注意的是,如果這個(gè)消息的絕對時(shí)間戳大于0xFFFFFF,說明這個(gè)時(shí)間大到無法用3個(gè)字節(jié)來表示,需要借助擴(kuò)展時(shí)間戳(Extended Timestamp)來表示,擴(kuò)展時(shí)間戳長度為4個(gè)字節(jié),默認(rèn)放在Chunk Header和Chunk Body之間。
當(dāng) fmt 為 1的時(shí)候,Message Header占用7個(gè)字節(jié),與之前的11個(gè)字節(jié)的chunk header相比,少了一個(gè)message stream id,這個(gè)chunk是復(fù)用之前的chunk stream id,這個(gè)一般用于可變長的消息結(jié)構(gòu)。
當(dāng) fmt 為 2的時(shí)候,Message Header只占用3個(gè)字節(jié),就只包含timestamp的三個(gè)字節(jié),與之前相比,既少了stream id也少了message length,這種少了message length的,一般用于固定長度但是需要修正時(shí)間的消息(如:音頻數(shù)據(jù))。
當(dāng) fmt 為 3的時(shí)候,Chunk Header里就不包含 Message Header 了。一般來說,在拆包的時(shí)候,把一個(gè)完整的RTMP的Message消息,會(huì)拆成第一個(gè)是fmt 為 0的Chunk消息,隨后的消息也會(huì)拆成fmt為3的消息,這樣的做的方式就是第一個(gè)Chunk附帶著最全的Chunk消息信息,后續(xù)Chunk信息的Header就會(huì)比較小,這樣實(shí)現(xiàn)比較簡單,壓縮率也是比較好。當(dāng)然,如果第一個(gè)Message發(fā)送成功之后,第二個(gè)Message再次發(fā)送的時(shí)候,就會(huì)把第二個(gè)Message的第一個(gè)Chunk設(shè)置成fmt為1類型的Chunk,隨后該Message的Chunk的fmt為3,這樣就能夠進(jìn)行消息的區(qū)分。
剛才花了很多時(shí)間去描述Chunk Header,接下來我們再針對Chunk Body進(jìn)行簡單的描述。與Chunk Header相比,Chunk Body就比較簡單,沒有那么多變長的控制,結(jié)構(gòu)也比較簡單,這個(gè)里面的數(shù)據(jù)也就是真正有業(yè)務(wù)含義的數(shù)據(jù),長度默認(rèn)是128個(gè)字節(jié)(可以通過 set chunk size 命令協(xié)商更改)。里面的數(shù)據(jù)包組織格式一般是AMF或者FLV格式的音視頻數(shù)據(jù)(不含F(xiàn)LV TAG頭)。AMF組織結(jié)構(gòu)的數(shù)據(jù)組成如下圖所示,F(xiàn)LV格式本文不做深入描述,感興趣的話可以閱讀 FLV 官方文檔。
AMF(Action Message Format) 是一種類似JSON,XML的二進(jìn)制數(shù)據(jù)序列化格式,Adobe Flash與遠(yuǎn)程服務(wù)端可通過AMF格式的數(shù)據(jù)進(jìn)行數(shù)據(jù)通信。
AMF具體的格式其實(shí)與Map的數(shù)據(jù)結(jié)構(gòu)很相似,就是在KV鍵值對的基礎(chǔ)上,中間多加了一個(gè)Value值的length。AMF的結(jié)果基本如下圖所示,有時(shí)候len字段就是空,這個(gè)是由type來決定的,我們舉例來說,例如我們傳輸?shù)氖莕umber類型的AMF格式的數(shù)據(jù),那么len字段我們就可以忽略,因?yàn)槲覀兡J(rèn)number類型的字段占用8個(gè)字節(jié),我們這邊就可以忽略了。
再舉例來說,AMF如果傳輸?shù)氖?x02 string類型的數(shù)據(jù)的時(shí)候,len的長度就默認(rèn)占據(jù)2個(gè)字節(jié),因?yàn)?個(gè)字節(jié)足夠表示后面value的最大長度了。以此類推,當(dāng)然有些時(shí)候,len和value的值都不存在,就比如傳遞0x05 傳遞null的時(shí)候,len和value我們就都不需要了。
以下列舉一些常用的AMF的type的對應(yīng)表格,更多信息可以查看官方文檔。
我們可以通過WireShark來抓包,實(shí)際來體驗(yàn)一下具體的AMF0的格式。
如上圖所示,這是一個(gè)非常典型的AMF0類型string結(jié)構(gòu)的抓包。AMF目前有2個(gè)主要的版本,分別是AFM0和AMF3,在目前的實(shí)際使用場景中,AMF0還是占據(jù)主流的地位。那么AMF0和AMF3有什么區(qū)別呢,當(dāng)客戶端給服務(wù)器端發(fā)送AMF格式Chunk Data數(shù)據(jù)的時(shí)候,服務(wù)端在接收到該信息的時(shí)候,如何是知道AMF0或者是AMF3呢?實(shí)際上RTMP在Chunk Header中使用message type id來進(jìn)行區(qū)分,當(dāng)消息使用AMF0編碼時(shí),message type id等于20,使用AMF3編碼時(shí)message type id等于17。
首先,用一句話來總結(jié)一下Chunk和Message的關(guān)系,一個(gè)Message是由多個(gè)Chunk組成,多個(gè)Chunk Stream id一樣的Chunk稱之為Chunk Stream,接收端可以重新合并解析為完整的Message。RTMP相比于RPC消息來說,消息類型多了很多,前文講的RPC消息類型歸根結(jié)底就request,response和heartbeat這三種類型,但是RTMP協(xié)議的消息類型就比較豐富。RTMP消息主要分為以下三大類型:協(xié)議控制消息,數(shù)據(jù)消息和命令消息。
**協(xié)議控制消息:**Message Type ID = 1~6,主要用于協(xié)議內(nèi)的控制。
**數(shù)據(jù)消息:**Message Type ID = 8 9
188: Audio 音頻數(shù)據(jù)
9: Video 視頻數(shù)據(jù)1
8: Metadata 包括音視頻編碼、視頻寬高等音視頻元數(shù)據(jù)。
命令消息 Command Message (20, 17):此類型消息主要有 NetConnection 和 NetStream 兩類,兩類分別有多個(gè)函數(shù),該消息的調(diào)用,可理解為遠(yuǎn)程函數(shù)調(diào)用。
總覽圖如下,后續(xù)在源碼解析章節(jié),會(huì)進(jìn)行具體介紹,其中著色部分為常用消息。
網(wǎng)絡(luò)協(xié)議的學(xué)習(xí)是一個(gè)枯燥的過程,我們嘗試結(jié)合 RTMP協(xié)議原文和WireShark抓包的方式,盡量形象地給大家描述 RTMP 協(xié)議中的核心流程,包括握手,連接,createStream,推流和拉流。本節(jié)所有的抓包數(shù)據(jù)的基本環(huán)境是:livego作為RTMP服務(wù)器(服務(wù)端口為1935),OBS作為推流應(yīng)用,VLC作為拉流應(yīng)用。
作為一個(gè)應(yīng)用層協(xié)議解析來說,首先,我們要注意的就是主體流程的把握,對于每一個(gè) RTMP 服務(wù)器來說,每一個(gè)推流和拉流從代碼層面來說,都是一個(gè)網(wǎng)絡(luò)鏈接,針對每一個(gè)連接,我們要進(jìn)行對應(yīng)的工序進(jìn)行處理,我們可以看到livego中源碼中所展示的一樣,有一個(gè)handleConn方法,顧名思義,就是用來處理每一個(gè)連接,按照主流程來說,分為第一部分的握手,第二個(gè)核心模塊的依據(jù)RTMP包協(xié)議,進(jìn)行Chunk header和Chunk body的解析,后續(xù)再根據(jù)解析出來的Chunk header和Chunk body再做具體的處理。
可以看到上述代碼塊,主要有2個(gè)核心方法:一個(gè)是HandshakeServer,主要處理握手邏輯;另一個(gè)是ReadMsg方法,主要處理Chunk header和Chunk body信息的讀取。
協(xié)議原文的5.2.5節(jié)詳細(xì)介紹了 RTMP 握手的過程,圖示如下:
乍一看,可能會(huì)覺得此過程有些復(fù)雜。所以,我們還是先用 WireShark 抓包來整體看看過程吧。
WireShark 抓包的 Info 能夠?yàn)槲覀兘庾x RTMP 包的含義,從下圖可以看出,握手主要涉及到3個(gè)包。其中第16號包是客戶端向服務(wù)端發(fā)送 C0 和 C1 消息,18號包是服務(wù)端向客戶端發(fā)送 S0,S1 和 S2 消息,20號包是客戶端向服務(wù)端發(fā)送 C2 消息。如此,客戶端和服務(wù)端就完成了握手過程。
通過 WireShark 抓包可以看出,握手過程還是非常簡潔的,有點(diǎn)類似 TCP 三次握手的過程,所以從實(shí)際抓包來說,與RTMP協(xié)議原文的5.2.5節(jié)介紹的還是有些出入的,整體流程變得很簡潔。
現(xiàn)在可以回頭看看上面那個(gè)比較復(fù)雜的握手流程圖了。圖中將客戶端和服務(wù)端分為四種狀態(tài),分別是:未初始化,已發(fā)送版本號,已發(fā)送 ACK,握手完成。
未初始化:客戶端和服務(wù)端無任何交流階段;
已發(fā)送版本號:發(fā)送了 C0 或者 S0;
已發(fā)送 ACK:發(fā)送了 C2 或者 S2;
握手完成:接收到了 S2 或者 C2。
RTMP 協(xié)議規(guī)范并沒有限定死 C0,C1,C2 和 S0,S1,S2 的順序,但是制定了以下規(guī)則:
客戶端必須收到服務(wù)端發(fā)來的 S1 后才能發(fā)送 C2;
客戶端必須收到服務(wù)端發(fā)來的 S2 后才能發(fā)送其他數(shù)據(jù);
服務(wù)端必須收到客戶端發(fā)來的 C0 后才能發(fā)送 S0 和 S1;
服務(wù)端必須收到客戶端發(fā)來的 C1 后才能發(fā)送 S2;
服務(wù)端必須收到客戶端發(fā)來的 C2 后才能發(fā)送其他數(shù)據(jù)。
從 WireShark 抓包分析可以看出,整個(gè)握手過程的確是遵循了以上規(guī)定?,F(xiàn)在問題來了,C0,C1,C2,S0,S1 和 S2 這些消息到底是些什么玩意?其實(shí),RTMP 協(xié)議規(guī)范里面明確定義了它們的數(shù)據(jù)格式。
C0 和 S0:1個(gè)字節(jié)長度,該消息指定了 RTMP 版本號。取值范圍 0~255,我們只需要知道 3 才是我們需要的就行。其他取值含義感興趣的話可以閱讀協(xié)議原文。
C1 和 S1:1536個(gè)字節(jié)長度,由 時(shí)間戳+零值+隨機(jī)數(shù)據(jù) 組成,握手過程的中間包。
C2 和 S2:1536個(gè)字節(jié)長度,由 時(shí)間戳+時(shí)間戳2+隨機(jī)數(shù)據(jù)回傳 組成,基本上是 C1 和 S1 的 echo 數(shù)據(jù)。一般在實(shí)現(xiàn)上,會(huì)令 S2 = C1,C2 = S1。
下面我們結(jié)合 livego 源碼來加強(qiáng)對握手過程的理解。
到此為止,最簡單的握手流程就到此結(jié)束了,可以看出整個(gè)握手流程還是比較清晰的,處理邏輯也是比較簡單,也比較便于理解。
3.2.2.1 解析RTMP協(xié)議的Chunk信息
握手之后,就要做開始做連接等相關(guān)的事情處理了,再做此信息處理之前,工欲善其事必先利其器。
我們先要按照RTMP協(xié)議的規(guī)范來解析Chunk Header和Chunk body了,將網(wǎng)絡(luò)傳輸?shù)淖止?jié)包數(shù)據(jù)轉(zhuǎn)換成我們可識(shí)別的信息處理,再根據(jù)這些可識(shí)別的信息數(shù)據(jù),再做對應(yīng)流程的處理,這塊是源碼解析的關(guān)鍵核心,涉及的知識(shí)點(diǎn)非常多,大家可以結(jié)合上文一起看,可以方便大家理解ReadMsg這塊核心邏輯的理解。
上述的代碼塊邏輯很清晰,主要是讀取每一個(gè)conn連接中,進(jìn)行對應(yīng)的編解碼,獲取到一個(gè)個(gè)Chunk,并且將相同ChunkStreamId的Chunk再次進(jìn)行合并,合并成對應(yīng)的Chunk Stream,最后一個(gè)個(gè)完整的Chunk Stream就是Message了。
這塊代碼就是和我們之前理論部分知識(shí)介紹的chunkstreamId那塊知識(shí)比較接近的地方了,大家可以結(jié)合起來一起看,大家在腦海中,要注意就是一個(gè)conn連接,會(huì)傳遞多個(gè)Message,例如連接Message,createStreamMessage等等,每一個(gè)Message就是Chunk Stream,也就是多個(gè)csid相同的Chunk,所以livego的作者使用map這樣的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲(chǔ),key就是csid,value就是chunkstream,這樣就可以將向rtmp服務(wù)器發(fā)送過來的信息能夠全部保存下來。
readChunk代碼的具體邏輯實(shí)現(xiàn)分成如下幾個(gè)部分:
1)csid的修正,至于理論部分參照上述邏輯,這塊其實(shí)是basic header的處理。
2)Chunk Header按照format的數(shù)值進(jìn)行對應(yīng)的解析處理,上文理論部分也已經(jīng)介紹過了,下文也有具體的注釋解釋,有兩個(gè)技術(shù)點(diǎn)需要注意第一就是timestramp時(shí)間戳的處理,第二個(gè)注意點(diǎn)是chunk.new(pool)這行代碼,也是需要大家注意,代碼注釋中也寫的比較清楚。
3)Chunk Body的讀取處理,上文理論部分說過,Chunk header中當(dāng)fmt 為 0 的時(shí)候,會(huì)有一個(gè)message length字段,這個(gè)字段會(huì)控制Chunk Body的大小,依據(jù)這個(gè)字段,我們可以很輕松地讀取到Chunk body信息的讀取,整體邏輯如下。
到此為止,我們已經(jīng)成功解析了Chunk Header,讀取了Chunk Body,注意我們只是讀取了Chunk Body還沒有按照AMF格式對Chunk Body進(jìn)行解析,針對Chunk Body部分的邏輯處理,在下文會(huì)進(jìn)行詳細(xì)的源碼介紹,不過現(xiàn)在我們已經(jīng)解析到了一個(gè)連接發(fā)送過來的ChunkStream了,接下來我們就可以回到主流程的分析了。
剛才說了握手完成后,并且我們也解析到了ChunkStream信息了,接下來我們就要依據(jù)ChunkStream的typeId和Chunk Body中的AMF數(shù)據(jù)進(jìn)行對應(yīng)的工序流程處理了,具體思路大家可以這樣理解,客戶端A發(fā)送xxxCmd命令,RTMP服務(wù)端根據(jù)typeId和AMF信息解析出xxxCmd命令,并給以對應(yīng)命令的響應(yīng)。
上述代碼塊中的handleCmdMsg中也是這個(gè)RTMP服務(wù)端處理客戶端命令的代碼精髓了,可以看出livego是支持AMF3和AMF0的,AMF3和AMF0的區(qū)別,上文也已經(jīng)介紹過了,下文的代碼注釋寫的也比較清楚,然后就是解析AMF格式的Chunk Body的數(shù)據(jù),解析出來的結(jié)果也是按照Slice格式進(jìn)行存儲(chǔ)。
解析好typeId和AMF,接下來就是水到渠成的對各個(gè)命令進(jìn)行處理了。
接下來是針對每一個(gè)客戶端命令的處理了。
3.2.2.2 連接
連接(Connect)命令處理過程:連接過程客戶端和服務(wù)端會(huì)完成窗口大小,傳輸塊大小和帶寬大小的確認(rèn),RTMP 協(xié)議原文詳細(xì)介紹了連接過程,如下圖所示:
同樣,我們這里用 WireShark 抓包分析:
從抓包可以看出,連接過程只用了3個(gè)包就完成了:
22 號包:客戶端告訴服務(wù)端,我想要設(shè)置 chunk size 為 4096;
24 號包:客戶端告訴服務(wù)端,我想要連接叫 “l(fā)ive” 的應(yīng)用;
26 號包:服務(wù)端響應(yīng)客戶端的連接請求,確定窗口大小,帶寬大小和 chunk size,以及返回 “_result” 表示響應(yīng)成功。這些都是通過一個(gè) TCP 包來完成的。
那么客戶端和服務(wù)端是如何知道這些包的含義的呢?這就是 RTMP 協(xié)議規(guī)范所制定的規(guī)則了,我們可以通過閱讀規(guī)范來了解,當(dāng)然也可以通過 wrieshark 來幫助我們快速解析。以下是 22 號包的詳細(xì)解析,我們重點(diǎn)關(guān)注 RTMP 協(xié)議解析信息就行。
從圖中可以看出, RTMP Header 包含有 Format 信息,Chunk Stream ID 信息,Timestamp 信息,Body size 信息,Message Type ID 信息和 Messgae Stream ID 信息。Type ID 的十六進(jìn)制值為 0x01,含義為 Set Chunk Size,屬于協(xié)議控制消息(Protocol Control Messages)。
RTMP 協(xié)議規(guī)范5.4節(jié)規(guī)定了,對于協(xié)議控制消息,Chunk Stream ID 必須設(shè)為 2,Message Stream ID 必須設(shè)為 0,時(shí)間戳直接忽略。從 WireShark 抓包解析出的信息可知,22號包的確是符合 RTMP 規(guī)范的。
現(xiàn)在我們來看看 24 號包的詳細(xì)解析。
24 號包也是客戶端發(fā)出的,可以看到它設(shè)置Message Stream ID 為 0,Message Type ID 為 0x14(即十進(jìn)制的20),含義為 AMF0 命令。AMF0 屬于 RTMP 命令消息(RTMP Command Messages),RTMP 協(xié)議規(guī)范并沒有規(guī)定連接過程必須要使用的 Chunk Stream ID,因?yàn)檎嬲鹱饔玫氖?Message Type ID,服務(wù)端根據(jù) Message Type ID 來做相應(yīng)的響應(yīng)。連接過程發(fā)送的 AMF0 命令攜帶的是 Object 類型的數(shù)據(jù),會(huì)告訴服務(wù)端要連接的應(yīng)用名和播放地址等信息。
以下代碼是 livego 處理客戶端請求連接的過程。
收到客戶端連接應(yīng)用的請求后,服務(wù)端需要作出相應(yīng)響應(yīng)給客戶端,也就是 WireShark 抓取的 26 號包的內(nèi)容,詳細(xì)內(nèi)容如下圖所示,可以看到服務(wù)端在一個(gè)包里面做了好幾件事情。
我們可以結(jié)合 livego 源碼來深入學(xué)習(xí)該過程。
3.2.2.3 createStream
連接完成后,就可以創(chuàng)建流了。創(chuàng)建流的過程相對來說比較簡單,只需要兩個(gè)包就能夠?qū)崿F(xiàn),如下所示:
其中 32 號包是客戶端發(fā)起 createStream 請求,34 號包是服務(wù)端響應(yīng),以下是 livego 處理客戶端連接請求的源碼。
3.2.2.4 推流
創(chuàng)建流完成后,就可以開始推流或者拉流了,RTMP 協(xié)議規(guī)范的7.3.1節(jié)也有給出推流示意圖,如下圖所示。其中連接和創(chuàng)建流的過程上文已經(jīng)詳細(xì)介紹過了,我們重點(diǎn)看發(fā)布內(nèi)容(Publishing Content)的過程就行。
使用 livego 推流前,需要先獲取推流的 channelkey。我們可以通過如下命令獲取頻道為 “movie” 的 channelKey。響應(yīng)內(nèi)容中的 Content 的 data 字段值就是推流需要的 channelKey。
$ curl http://localhost:8090/control/get?room=movie StatusCode : 200 StatusDescription : OK Content : {"status":200,"data":"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575K LkIZ9PYk"} RawContent : HTTP/1.1 200 OK Content-Length: 72 Content-Type: application/json Date: Tue, 09 Feb 2021 09:19:34 GMT {"status":200,"data":"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575K LkIZ9PYk"} Forms : {} Headers : {[Content-Length, 72], [Content-Type, application/json], [Date , Tue, 09 Feb 2021 09:19:34 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 72
使用OBS推流到 livego 服務(wù)器中應(yīng)用名為 live 的 movie 頻道,推流地址為:rtmp://localhost:1935/live/rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk。同樣,我們還是先看一下WireShark 的抓包內(nèi)容吧。
推流初期,客戶端發(fā)起 publish 請求,也就是36號包的內(nèi)容,該請求中需要帶上頻道名,在這個(gè)包里面就是"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"。
服務(wù)端會(huì)首先會(huì)檢測這個(gè)頻道名是否存在以及檢查這個(gè)推流名是否被使用中,如果不存在或者在使用的話就會(huì)拒絕客戶端的推流請求。由于我們在推流前已經(jīng)生成了該頻道名,客戶端可以合法使用,于是服務(wù)端在38號包中回應(yīng)的是 "NetStream.Publish.Start",也就是告訴客戶端可以開始推流了??蛻舳嗽谕屏饕粢曨l數(shù)據(jù)前需要先把音視頻的的元數(shù)據(jù)發(fā)給服務(wù)端,也就是40號包所做的事情,我們可以看一下該包的詳細(xì)內(nèi)容。從下圖可以看出,發(fā)送元數(shù)據(jù)信息比較多,包含有視頻分辨率,幀率,音頻采樣率和音頻聲道等關(guān)鍵信息。
告訴服務(wù)端音視頻元數(shù)據(jù)后,客戶端就可以開始發(fā)送有效的音視頻數(shù)據(jù)了,服務(wù)端會(huì)一直接收這些數(shù)據(jù),直到客戶端發(fā)出 FCUnpublish 和 deleteStream 命令為止。stream.go 的 TransStart() 方法主要邏輯為接收推流客戶端的音視頻數(shù)據(jù),然后在本地緩存最新的一個(gè)數(shù)據(jù)包,最后將音視頻數(shù)據(jù)發(fā)給各個(gè)拉流端。其中讀取推流客戶單音視頻數(shù)據(jù)主要是使用到 rtmp.go 中的 VirReader.Read() 方法,相關(guān)代碼和注釋如下所示。
附媒體頭信息解析的部分源碼分析。
解析音頻頭
解析視頻頭
3.2.2.5 拉流
有了推流客戶端的持續(xù)推流,拉流客戶端就可以通過服務(wù)器持續(xù)拉取到音視頻數(shù)據(jù)了。RTMP 協(xié)議規(guī)范的7.2.2.1節(jié)對拉流過程進(jìn)行了詳細(xì)描述。其中,握手、連接和創(chuàng)建流的過程前文已經(jīng)講述過了,我們重點(diǎn)關(guān)注下 play 命令的過程就行。
同樣,我們先用 WireShark 抓包來分析下??蛻舳送ㄟ^ 640 號包告訴服務(wù)器,我想要播放叫 “movie” 的頻道。
此處為什么是叫 “movie” 而不是推流時(shí)候用的“rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk”,其實(shí)這兩個(gè)指向的是同一個(gè)頻道,只不過一個(gè)用于推流一個(gè)用于拉流,我們可以從 livego 的源碼來印證這一點(diǎn)。
服務(wù)端收到拉流客戶端的 play 請求后,會(huì)做出響應(yīng) "NetStream.Play.Reset","NetStream.Play.Start" ,"NetStream.Play.PublishNotify" 和音視頻元數(shù)據(jù)。這些工作做完后,就可以持續(xù)發(fā)送音視頻數(shù)據(jù)給拉流客戶端了。我們可以通過 livego 源碼來加深一下對此過程的理解。
通過 chan 讀取推流數(shù)據(jù),然后發(fā)給拉流客戶端。
到此為止整個(gè)RTMP的主體流程就是這樣了,這邊不涉及FLV,HLS等具體傳輸協(xié)議或者格式轉(zhuǎn)換的源碼說明,也就是說RTMP服務(wù)器怎么收到推流客戶端的音視頻包也會(huì)原封不動(dòng)地分發(fā)給拉流客戶端,并沒有做額外的處理,不過現(xiàn)在各大云廠商拉流端都支持http-flv,hls等傳輸協(xié)議的支持,并且也支持音視頻的錄制回放點(diǎn)播功能,這塊livego其實(shí)也是支持的。
因?yàn)槠拗?,這邊就不再展開介紹,后續(xù)有機(jī)會(huì),再單獨(dú)一起學(xué)習(xí)分享介紹livego關(guān)于這塊邏輯的處理。
目前基于RTMP協(xié)議的直播是國內(nèi)直播的基準(zhǔn)協(xié)議,也是各大云廠商都兼容的直播協(xié)議,它的多路復(fù)用,分包等優(yōu)秀特性也是各大廠商選擇它的一個(gè)重要原因。在這個(gè)基礎(chǔ)之上,也是因?yàn)樗菓?yīng)用層協(xié)議,騰訊,阿里,聲網(wǎng)等大型云廠商,也會(huì)對其協(xié)議的細(xì)節(jié),進(jìn)行源碼的改造,例如實(shí)現(xiàn)多路音視頻流的混流,單路的錄制等功能。
但是RTMP也有它自己本身的缺點(diǎn),時(shí)延較高就是RTMP一個(gè)最大的問題,在實(shí)際的生產(chǎn)過程中,即使在比較健康的網(wǎng)絡(luò)環(huán)境中,RTMP的時(shí)延也會(huì)有3~8s,這與各大云廠商給出的1~3s理論時(shí)延值還是有較大出入的。那么時(shí)延會(huì)帶來哪些問題呢?我們可以想象如下的一些場景:
在線教育,學(xué)生提問,老師都講到下一個(gè)知識(shí)點(diǎn)了,才看到學(xué)生上一個(gè)提問。
電商直播,詢問寶貝信息,主播“視而不理”。
打賞后遲遲聽不到主播的口播感謝。
在別人的吶喊聲知道球進(jìn)了,你看的還是直播嗎?
特別是現(xiàn)在直播已經(jīng)形成產(chǎn)業(yè)鏈的大環(huán)境下,很多主播都是將其作為一個(gè)職業(yè),很多主播使用在公司同一個(gè)網(wǎng)絡(luò)下進(jìn)行直播,在公司網(wǎng)絡(luò)的出口帶寬有限的情況下,RTMP和FLV格式的延遲會(huì)更加嚴(yán)重,高時(shí)延的直播影響了用戶和主播的實(shí)時(shí)互動(dòng),也阻礙了一些特殊直播場景的落地,例如帶貨直播,教育直播等。
以下是使用RTMP協(xié)議常規(guī)的解決方案:
根據(jù)實(shí)際的網(wǎng)絡(luò)情況和推流的一些設(shè)置,例如關(guān)鍵幀間隔,推流碼率等等,時(shí)延一般會(huì)在8秒左右,時(shí)延主要來自于2個(gè)大的方面:
CDN鏈路延遲, 這分為兩部分,一部分是網(wǎng)絡(luò)傳輸延遲。CDN內(nèi)部有四段網(wǎng)絡(luò)傳輸,假設(shè)每段網(wǎng)絡(luò)傳輸帶來的延遲是20ms,那這四段延遲便是100ms;此外,使用RTMP幀為傳輸單位,意味著每個(gè)節(jié)點(diǎn)都要收滿一幀之后才能啟動(dòng)向下游轉(zhuǎn)發(fā)的流程;CDN為了提升并發(fā)性能,會(huì)有一定的優(yōu)化發(fā)包策略,會(huì)增加部分延遲。在網(wǎng)絡(luò)抖動(dòng)的場景下,延遲就更加無法控制了,可靠傳輸協(xié)議下,一旦有網(wǎng)絡(luò)抖動(dòng),后續(xù)的發(fā)送流程都將阻塞,需要等待前序包的重傳。
播放端buffer,這個(gè)是延遲的主要來源。公網(wǎng)環(huán)境千差萬別,推流、CDN傳輸、播放接收這幾個(gè)環(huán)節(jié)任何一個(gè)環(huán)節(jié)發(fā)生網(wǎng)絡(luò)抖動(dòng),都會(huì)影響到播放端。為了對抗前邊鏈路的抖動(dòng),播放器的常規(guī)策略是保留6s 左右的媒體buffer。
到此,關(guān)于“什么是RTMP協(xié)議”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。