實時消息傳輸協議(Real-Time Messaging Protocol)是目前直播的主要協議,是Adobe公司爲Flash播放器和服務器之間提供音視頻數據傳輸服務而設計的應用層私有協議。RTMP協議是目前各大雲廠商直線直播業務所公用的基本直播推拉流協議,隨着國內直播行業的發展和5G時代的到來,對RTMP協議有基本的瞭解,也是咱們程序員必需要掌握的基本技能。html
本文主要闡述RTMP的基本思想和核心概念,而且輔之以livego的源碼分析,和你們一塊兒深刻學習RTMP協議最核心的知識點。java
RTMP協議主要的特色有:多路複用,分包和應用層協議。如下將對這些特色進行詳細的描述。nginx
多路複用(multiplex)指的是信號發送端經過一個信道同時傳輸多路信號,而後信號接收端將一個信道中傳遞過來的多個信號分別組合起來,分別造成獨立完整的信號信息,以此來更加有效地使用通訊線路。git
簡而言之,就是在一個 TCP 鏈接上,將須要傳遞的Message分紅一個或者多個 Chunk,同一個Message 的多個Chunk 組成 ChunkStream,在接收端,再把 ChunkStream 中一個個 Chunk 組合起來就能夠還原成一個完整的 Message,這就是多路複用的基本理念。程序員
上圖是一個簡單例子,假設須要傳遞一個300字節長的Message,咱們能夠將其拆分紅3個Chunk,每個Chunk能夠分紅 Chunk Header 和 Chunk Data。在Chunk Header 裏咱們能夠標記這個Chunk中的一些基本信息,如 Chunk Stream Id 和 Message Type;Chunk Data 就是原始信息,上圖中將 Message 分紅128+128+44 =300,這樣就能夠完整的傳輸這個Message了。github
關於 Chunk Header 和 Chunk Data 的格式,後文會進行詳細介紹。json
RTMP協議的第二個大的特性就是分包,與RTSP協議相比,分包是RTMP的一個特色。與普通的業務應用層協議(如:RPC協議)不同的是,在多媒體網絡傳輸案例中,絕大多數的多媒體傳輸的音頻和視頻的數據包都相對比較偏大,在TCP這種可靠的傳輸協議之上進行大的數據包傳遞,頗有可能阻塞鏈接,致使優先級更高的信息沒法傳遞,分包傳輸就是爲了解決這個問題而出現的,具體的分包格式,下文會有介紹。數組
RTMP最後的一個特性,就是應用層協議。RTMP協議默認基於傳輸層協議TCP而實現,可是在RTMP的官方文檔中,只給定了標準的數據傳輸格式說明和一些具體的協議格式說明,並無具體官方的完整實現,這就催生出了不少相關的其餘業內實現,例如RTMP over UDP等等相關的私有改編的協議出現,給了你們更多的可擴展的空間,方便你們解決原生RTMP存在的直播時延等問題。緩存
做爲一種應用層協議,和其餘私有傳輸協議同樣(如RPC協議),RTMP也有一些具體代碼實現,如 nginx-rtmp、livego 和 srs。本文選用基於go語言實現的開源直播服務器 livego 進行源碼級的主流程分析,和你們一塊兒深刻學習 RTMP 推拉流的核心流程的實現,幫助你們對RTMP的協議有一個總體的理解。服務器
在進行源碼分析以前,咱們會經過類比RPC協議的方式,幫助你們對RTMP協議的格式有一個基本的瞭解,首先咱們能夠看一個比較簡單但實用的RPC協議格式,以下圖所示:
咱們能夠看到這是一個在RPC調用過程當中所使用的數據傳輸格式,之因此使用這樣的格式,根本目的仍是爲了解決"粘包和拆包"的問題。
如下簡要描述圖中RPC協議的格式:首先用2個字節,MAGIC來表示魔數,標記該協議是對端都能識別的標識,若是接收到的2個字節不是0xbabe的話,則直接丟棄該包;第二個sign佔用1個字節,低4位表示消息的類型request/response/heartbeat,高4位表示序列化類型例如json,hessian,protobuf,kyro等等;第三個 status 佔用一個字節,表示狀態位;隨後使用8個字節來表示調用的requestId,通常使用低48位(2的48次方)就足夠表示requestId了;接着使用4字節定長的body size來表示Body Content,經過這樣的方式就可以很快的解析出RPC消息Message的完整請求對象了。
經過分析上述的一個簡單的RPC協議,其實咱們可以發現一個很好的思想,就是最大效率的使用字節,即便用最小的字節數組,來傳輸最多的數據信息。小小的一個字節可以帶來不少的信息量,畢竟一個字節它有64種不一樣的變化。在網絡中,若是隻須要利用一個字節就可以傳遞不少有用的信息的話,那麼咱們就可使用極其有限的資源來獲得最大的資源利用了。RTMP的官方文檔在2012年就出現了,雖然以目前的眼光來看,RTMP協議實現的很是複雜,甚至有些臃腫,可是它在2012年的時候,就可以有比較先進的思想,的確是咱們學習的榜樣。
在當今WebRTC協議橫行的年代裏,咱們也可以從WebRTC的設計實現中,看到RTMP的影子,上述的RPC協議咱們就能夠認爲是一個與RTMP具備類似設計理念的簡化版設計。
在分析RTMP源碼以前,咱們先對RTMP協議中的幾個核心概念作具體說明,方便咱們在宏觀上對RTMP整個協議棧有一個基本的瞭解,而且在後文源碼分析期間,咱們也會經過抓包的方式,更加直觀地幫助咱們去分析相關的原理。
首先,和剛纔的RPC協議格式同樣,RTMP實際傳輸的實體對象是Chunk,一個Chunk由Chunk Header和Chunk Body兩個部分組成,以下圖所示。
Chunk Header這個部分和咱們前面說過的RPC協議不太同樣,主要是RTMP協議的Chunk Header的長度不是固定的,爲何不是固定的呢?其實仍是Adobe公司爲了節省數據傳輸開銷。從剛纔將一個300字節的Message拆分紅3個Chunk的例子中,咱們能夠看到多路複用其實也是有一個比較明顯的缺點,就是咱們須要有一個Chunk Header來標記這個Chunk的基本信息,這樣其實就是在傳輸的時候有了額外字節流傳輸的開銷。因此爲了保證傳輸的字節數最少,咱們就須要不斷地壓榨着RTMP的Header的大小,確保Header的大小達到最小,這樣才能達到最高的傳輸效率。
首先咱們研究一下Chunk Header中Basic Header的部分,Basic Header的長度就是不固定的,能夠是1個字節,2個字節或者3個字節,這取決於Chunk Stream Id(縮寫:csid)。
RTMP協議支持的csid的範圍是2~65599,0和1是協議保留值,用戶不可以使用。Basic Header至少含有1個字節(低8位),它的長度就是這1個字節決定的,以下圖所示。該字節高2位留給 fmt,fmt的取值決定了 Message Header 的格式,這個在後面會講到。該字節的低6位就是 csid 的值,當低6位的 csid 取值爲0時,表示真實 csid 值大到沒法用6個bit表示了,須要藉助後續的一個字節才行;當低6位的 csid 取值爲1時,表示真實 csid 值大到沒法用14個bit表示了,須要再借助後續的一個字節才行。因而,整個Basic Header的長度看起來就不是固定的了,徹底取決於首字節的低6位的csid的值。
實際應用中,並無使用到那麼多csid,也就是說通常狀況下,Basic Header長度爲一個字節,csid取值範圍爲 2~63。
剛纔說了那麼多,才僅僅說了Basic Header,而Basci Header只是Chunk Header的組成部分之一,比較喜歡折騰的RTMP協議的做者,把RTMP的Chunk Header模塊又設計成了動態大小的,簡而言之也是爲了節省傳輸空間,這邊可以方便理解的地方就是Chunk Message Header的長度也分四種狀況,這就是前面提到的 fmt 這個值決定的。
Message Header 的四種格式以下圖所示:
當 fmt 爲 0 的時候,Message Header佔用11個字節(請注意,這邊的11個字節不包括Basic Header的長度),由3個字節長度的timestamp,3個字節長度的message length,1個字節長度的message type Id,4個字節長度的message stream Id所組成的。
其中,timestamp 是絕對時間戳,表示的是這個消息發送的時間;message length 表示的是chunk body的長度;message type id 表示的是消息類型,這個在後文會具體講到;message stream id 是消息惟一標識。這邊須要注意的是,若是這個消息的絕對時間戳大於0xFFFFFF,說明這個時間大到沒法用3個字節來表示,須要藉助擴展時間戳(Extended Timestamp)來表示,擴展時間戳長度爲4個字節,默認放在Chunk Header和Chunk Body之間。
當 fmt 爲 1的時候,Message Header佔用7個字節,與以前的11個字節的chunk header相比,少了一個message stream id,這個chunk是複用以前的chunk stream id,這個通常用於可變長的消息結構。
當 fmt 爲 2的時候,Message Header只佔用3個字節,就只包含timestamp的三個字節,與以前相比,既少了stream id也少了message length,這種少了message length的,通常用於固定長度可是須要修正時間的消息(如:音頻數據)。
當 fmt 爲 3的時候,Chunk Header裏就不包含 Message Header 了。通常來講,在拆包的時候,把一個完整的RTMP的Message消息,會拆成第一個是fmt 爲 0的Chunk消息,隨後的消息也會拆成fmt爲3的消息,這樣的作的方式就是第一個Chunk附帶着最全的Chunk消息信息,後續Chunk信息的Header就會比較小,這樣實現比較簡單,壓縮率也是比較好。固然,若是第一個Message發送成功以後,第二個Message再次發送的時候,就會把第二個Message的第一個Chunk設置成fmt爲1類型的Chunk,隨後該Message的Chunk的fmt爲3,這樣就可以進行消息的區分。
剛纔花了不少時間去描述Chunk Header,接下來咱們再針對Chunk Body進行簡單的描述。與Chunk Header相比,Chunk Body就比較簡單,沒有那麼多變長的控制,結構也比較簡單,這個裏面的數據也就是真正有業務含義的數據,長度默認是128個字節(能夠經過 set chunk size 命令協商更改)。裏面的數據包組織格式通常是AMF或者FLV格式的音視頻數據(不含FLV TAG頭)。AMF組織結構的數據組成以下圖所示,FLV格式本文不作深刻描述,感興趣的話能夠閱讀 FLV 官方文檔。
AMF(Action Message Format) 是一種相似JSON,XML的二進制數據序列化格式,Adobe Flash與遠程服務端可經過AMF格式的數據進行數據通訊。
AMF具體的格式其實與Map的數據結構很類似,就是在KV鍵值對的基礎上,中間多加了一個Value值的length。AMF的結果基本以下圖所示,有時候len字段就是空,這個是由type來決定的,咱們舉例來講,例如咱們傳輸的是number類型的AMF格式的數據,那麼len字段咱們就能夠忽略,由於咱們默認number類型的字段佔用8個字節,咱們這邊就能夠忽略了。
再舉例來講,AMF若是傳輸的是0x02 string類型的數據的時候,len的長度就默認佔據2個字節,由於2個字節足夠表示後面value的最大長度了。以此類推,固然有些時候,len和value的值都不存在,就好比傳遞0x05 傳遞null的時候,len和value咱們就都不須要了。
如下列舉一些經常使用的AMF的type的對應表格,更多信息能夠查看官方文檔。
咱們能夠經過WireShark來抓包,實際來體驗一下具體的AMF0的格式。
如上圖所示,這是一個很是典型的AMF0類型string結構的抓包。AMF目前有2個主要的版本,分別是AFM0和AMF3,在目前的實際使用場景中,AMF0仍是佔據主流的地位。那麼AMF0和AMF3有什麼區別呢,當客戶端給服務器端發送AMF格式Chunk Data數據的時候,服務端在接收到該信息的時候,如何是知道AMF0或者是AMF3呢?實際上RTMP在Chunk Header中使用message type id來進行區分,當消息使用AMF0編碼時,message type id等於20,使用AMF3編碼時message type id等於17。
首先,用一句話來總結一下Chunk和Message的關係,一個Message是由多個Chunk組成,多個Chunk Stream id同樣的Chunk稱之爲Chunk Stream,接收端能夠從新合併解析爲完整的Message。RTMP相比於RPC消息來講,消息類型多了不少,前文講的RPC消息類型歸根結底就request,response和heartbeat這三種類型,可是RTMP協議的消息類型就比較豐富。RTMP消息主要分爲如下三大類型:協議控制消息,數據消息和命令消息。
協議控制消息:Message Type ID = 1~6,主要用於協議內的控制。
數據消息:Message Type ID = 8 9
188: Audio 音頻數據
9: Video 視頻數據1
8: Metadata 包括音視頻編碼、視頻寬高等音視頻元數據。
命令消息 Command Message (20, 17):此類型消息主要有 NetConnection 和 NetStream 兩類,兩類分別有多個函數,該消息的調用,可理解爲遠程函數調用。
總覽圖以下,後續在源碼解析章節,會進行具體介紹,其中着色部分爲經常使用消息。
網絡協議的學習是一個枯燥的過程,咱們嘗試結合 RTMP協議原文和WireShark抓包的方式,儘可能形象地給你們描述 RTMP 協議中的核心流程,包括握手,鏈接,createStream,推流和拉流。本節全部的抓包數據的基本環境是:livego做爲RTMP服務器(服務端口爲1935),OBS做爲推流應用,VLC做爲拉流應用。
做爲一個應用層協議解析來講,首先,咱們要注意的就是主體流程的把握,對於每個 RTMP 服務器來講,每個推流和拉流從代碼層面來講,都是一個網絡連接,針對每個鏈接,咱們要進行對應的工序進行處理,咱們能夠看到livego中源碼中所展現的同樣,有一個handleConn方法,顧名思義,就是用來處理每個鏈接,按照主流程來講,分爲第一部分的握手,第二個核心模塊的依據RTMP包協議,進行Chunk header和Chunk body的解析,後續再根據解析出來的Chunk header和Chunk body再作具體的處理。
能夠看到上述代碼塊,主要有2個核心方法:一個是HandshakeServer,主要處理握手邏輯;另外一個是ReadMsg方法,主要處理Chunk header和Chunk body信息的讀取。
協議原文的5.2.5節詳細介紹了 RTMP 握手的過程,圖示以下:
乍一看,可能會以爲此過程有些複雜。因此,咱們仍是先用 WireShark 抓包來總體看看過程吧。
WireShark 抓包的 Info 可以爲咱們解讀 RTMP 包的含義,從下圖能夠看出,握手主要涉及到3個包。其中第16號包是客戶端向服務端發送 C0 和 C1 消息,18號包是服務端向客戶端發送 S0,S1 和 S2 消息,20號包是客戶端向服務端發送 C2 消息。如此,客戶端和服務端就完成了握手過程。
經過 WireShark 抓包能夠看出,握手過程仍是很是簡潔的,有點相似 TCP 三次握手的過程,因此從實際抓包來講,與RTMP協議原文的5.2.5節介紹的仍是有些出入的,總體流程變得很簡潔。
如今能夠回頭看看上面那個比較複雜的握手流程圖了。圖中將客戶端和服務端分爲四種狀態,分別是:未初始化,已發送版本號,已發送 ACK,握手完成。
未初始化:客戶端和服務端無任何交流階段;
已發送版本號:發送了 C0 或者 S0;
已發送 ACK:發送了 C2 或者 S2;
握手完成:接收到了 S2 或者 C2。
RTMP 協議規範並無限定死 C0,C1,C2 和 S0,S1,S2 的順序,可是制定瞭如下規則:
客戶端必須收到服務端發來的 S1 後才能發送 C2;
客戶端必須收到服務端發來的 S2 後才能發送其餘數據;
服務端必須收到客戶端發來的 C0 後才能發送 S0 和 S1;
服務端必須收到客戶端發來的 C1 後才能發送 S2;
服務端必須收到客戶端發來的 C2 後才能發送其餘數據。
從 WireShark 抓包分析能夠看出,整個握手過程的確是遵循了以上規定。如今問題來了,C0,C1,C2,S0,S1 和 S2 這些消息究竟是些什麼玩意?其實,RTMP 協議規範裏面明肯定義了它們的數據格式。
C0 和 S0:1個字節長度,該消息指定了 RTMP 版本號。取值範圍 0~255,咱們只須要知道 3 纔是咱們須要的就行。其餘取值含義感興趣的話能夠閱讀協議原文。
C1 和 S1:1536個字節長度,由 時間戳+零值+隨機數據 組成,握手過程的中間包。
C2 和 S2:1536個字節長度,由 時間戳+時間戳2+隨機數據回傳 組成,基本上是 C1 和 S1 的 echo 數據。通常在實現上,會令 S2 = C1,C2 = S1。
下面咱們結合 livego 源碼來增強對握手過程的理解。
到此爲止,最簡單的握手流程就到此結束了,能夠看出整個握手流程仍是比較清晰的,處理邏輯也是比較簡單,也比較便於理解。
3.2.2.1 解析RTMP協議的Chunk信息
握手以後,就要作開始作鏈接等相關的事情處理了,再作此信息處理以前,工欲善其事必先利其器。
咱們先要按照RTMP協議的規範來解析Chunk Header和Chunk body了,將網絡傳輸的字節包數據轉換成咱們可識別的信息處理,再根據這些可識別的信息數據,再作對應流程的處理,這塊是源碼解析的關鍵核心,涉及的知識點很是多,你們能夠結合上文一塊兒看,能夠方便你們理解ReadMsg這塊核心邏輯的理解。
上述的代碼塊邏輯很清晰,主要是讀取每個conn鏈接中,進行對應的編解碼,獲取到一個個Chunk,而且將相同ChunkStreamId的Chunk再次進行合併,合併成對應的Chunk Stream,最後一個個完整的Chunk Stream就是Message了。
這塊代碼就是和咱們以前理論部分知識介紹的chunkstreamId那塊知識比較接近的地方了,你們能夠結合起來一塊兒看,你們在腦海中,要注意就是一個conn鏈接,會傳遞多個Message,例如鏈接Message,createStreamMessage等等,每個Message就是Chunk Stream,也就是多個csid相同的Chunk,因此livego的做者使用map這樣的數據結構進行存儲,key就是csid,value就是chunkstream,這樣就能夠將向rtmp服務器發送過來的信息可以所有保存下來。
readChunk代碼的具體邏輯實現分紅以下幾個部分:
1)csid的修正,至於理論部分參照上述邏輯,這塊實際上是basic header的處理。
2)Chunk Header按照format的數值進行對應的解析處理,上文理論部分也已經介紹過了,下文也有具體的註釋解釋,有兩個技術點須要注意第一就是timestramp時間戳的處理,第二個注意點是chunk.new(pool)這行代碼,也是須要你們注意,代碼註釋中也寫的比較清楚。
3)Chunk Body的讀取處理,上文理論部分說過,Chunk header中當fmt 爲 0 的時候,會有一個message length字段,這個字段會控制Chunk Body的大小,依據這個字段,咱們能夠很輕鬆地讀取到Chunk body信息的讀取,總體邏輯以下。
到此爲止,咱們已經成功解析了Chunk Header,讀取了Chunk Body,注意咱們只是讀取了Chunk Body尚未按照AMF格式對Chunk Body進行解析,針對Chunk Body部分的邏輯處理,在下文會進行詳細的源碼介紹,不過如今咱們已經解析到了一個鏈接發送過來的ChunkStream了,接下來咱們就能夠回到主流程的分析了。
剛纔說了握手完成後,而且咱們也解析到了ChunkStream信息了,接下來咱們就要依據ChunkStream的typeId和Chunk Body中的AMF數據進行對應的工序流程處理了,具體思路你們能夠這樣理解,客戶端A發送xxxCmd命令,RTMP服務端根據typeId和AMF信息解析出xxxCmd命令,並給以對應命令的響應。
上述代碼塊中的handleCmdMsg中也是這個RTMP服務端處理客戶端命令的代碼精髓了,能夠看出livego是支持AMF3和AMF0的,AMF3和AMF0的區別,上文也已經介紹過了,下文的代碼註釋寫的也比較清楚,而後就是解析AMF格式的Chunk Body的數據,解析出來的結果也是按照Slice格式進行存儲。
解析好typeId和AMF,接下來就是水到渠成的對各個命令進行處理了。
接下來是針對每個客戶端命令的處理了。
3.2.2.2 鏈接
鏈接(Connect)命令處理過程:鏈接過程客戶端和服務端會完成窗口大小,傳輸塊大小和帶寬大小的確認,RTMP 協議原文詳細介紹了鏈接過程,以下圖所示:
一樣,咱們這裏用 WireShark 抓包分析:
從抓包能夠看出,鏈接過程只用了3個包就完成了:
22 號包:客戶端告訴服務端,我想要設置 chunk size 爲 4096;
24 號包:客戶端告訴服務端,我想要鏈接叫 「live」 的應用;
26 號包:服務端響應客戶端的鏈接請求,肯定窗口大小,帶寬大小和 chunk size,以及返回 「_result」 表示響應成功。這些都是經過一個 TCP 包來完成的。
那麼客戶端和服務端是如何知道這些包的含義的呢?這就是 RTMP 協議規範所制定的規則了,咱們能夠經過閱讀規範來了解,固然也能夠經過 wrieshark 來幫助咱們快速解析。如下是 22 號包的詳細解析,咱們重點關注 RTMP 協議解析信息就行。
從圖中能夠看出, RTMP Header 包含有 Format 信息,Chunk Stream ID 信息,Timestamp 信息,Body size 信息,Message Type ID 信息和 Messgae Stream ID 信息。Type ID 的十六進制值爲 0x01,含義爲 Set Chunk Size,屬於協議控制消息(Protocol Control Messages)。
RTMP 協議規範5.4節規定了,對於協議控制消息,Chunk Stream ID 必須設爲 2,Message Stream ID 必須設爲 0,時間戳直接忽略。從 WireShark 抓包解析出的信息可知,22號包的確是符合 RTMP 規範的。
如今咱們來看看 24 號包的詳細解析。
24 號包也是客戶端發出的,能夠看到它設置Message Stream ID 爲 0,Message Type ID 爲 0x14(即十進制的20),含義爲 AMF0 命令。AMF0 屬於 RTMP 命令消息(RTMP Command Messages),RTMP 協議規範並無規定鏈接過程必需要使用的 Chunk Stream ID,由於真正起做用的是 Message Type ID,服務端根據 Message Type ID 來作相應的響應。鏈接過程發送的 AMF0 命令攜帶的是 Object 類型的數據,會告訴服務端要鏈接的應用名和播放地址等信息。
如下代碼是 livego 處理客戶端請求鏈接的過程。
收到客戶端鏈接應用的請求後,服務端須要做出相應響應給客戶端,也就是 WireShark 抓取的 26 號包的內容,詳細內容以下圖所示,能夠看到服務端在一個包裏面作了好幾件事情。
咱們能夠結合 livego 源碼來深刻學習該過程。
3.2.2.3 createStream
鏈接完成後,就能夠建立流了。建立流的過程相對來講比較簡單,只須要兩個包就可以實現,以下所示:
其中 32 號包是客戶端發起 createStream 請求,34 號包是服務端響應,如下是 livego 處理客戶端鏈接請求的源碼。
3.2.2.4 推流
建立流完成後,就能夠開始推流或者拉流了,RTMP 協議規範的7.3.1節也有給出推流示意圖,以下圖所示。其中鏈接和建立流的過程上文已經詳細介紹過了,咱們重點看發佈內容(Publishing Content)的過程就行。
使用 livego 推流前,須要先獲取推流的 channelkey。咱們能夠經過以下命令獲取頻道爲 「movie」 的 channelKey。響應內容中的 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 服務器中應用名爲 live 的 movie 頻道,推流地址爲:rtmp://localhost:1935/live/rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk。一樣,咱們仍是先看一下WireShark 的抓包內容吧。
推流初期,客戶端發起 publish 請求,也就是36號包的內容,該請求中須要帶上頻道名,在這個包裏面就是"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"。
服務端會首先會檢測這個頻道名是否存在以及檢查這個推流名是否被使用中,若是不存在或者在使用的話就會拒絕客戶端的推流請求。因爲咱們在推流前已經生成了該頻道名,客戶端能夠合法使用,因而服務端在38號包中迴應的是 "NetStream.Publish.Start",也就是告訴客戶端能夠開始推流了。客戶端在推流音視頻數據前須要先把音視頻的的元數據發給服務端,也就是40號包所作的事情,咱們能夠看一下該包的詳細內容。從下圖能夠看出,發送元數據信息比較多,包含有視頻分辨率,幀率,音頻採樣率和音頻聲道等關鍵信息。
告訴服務端音視頻元數據後,客戶端就能夠開始發送有效的音視頻數據了,服務端會一直接收這些數據,直到客戶端發出 FCUnpublish 和 deleteStream 命令爲止。stream.go 的 TransStart() 方法主要邏輯爲接收推流客戶端的音視頻數據,而後在本地緩存最新的一個數據包,最後將音視頻數據發給各個拉流端。其中讀取推流客戶單音視頻數據主要是使用到 rtmp.go 中的 VirReader.Read() 方法,相關代碼和註釋以下所示。
附媒體頭信息解析的部分源碼分析。
解析音頻頭
解析視頻頭
3.2.2.5 拉流
有了推流客戶端的持續推流,拉流客戶端就能夠經過服務器持續拉取到音視頻數據了。RTMP 協議規範的7.2.2.1節對拉流過程進行了詳細描述。其中,握手、鏈接和建立流的過程前文已經講述過了,咱們重點關注下 play 命令的過程就行。
一樣,咱們先用 WireShark 抓包來分析下。客戶端經過 640 號包告訴服務器,我想要播放叫 「movie」 的頻道。
此處爲何是叫 「movie」 而不是推流時候用的「rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk」,其實這兩個指向的是同一個頻道,只不過一個用於推流一個用於拉流,咱們能夠從 livego 的源碼來印證這一點。
服務端收到拉流客戶端的 play 請求後,會作出響應 "NetStream.Play.Reset","NetStream.Play.Start" ,"NetStream.Play.PublishNotify" 和音視頻元數據。這些工做作完後,就能夠持續發送音視頻數據給拉流客戶端了。咱們能夠經過 livego 源碼來加深一下對此過程的理解。
經過 chan 讀取推流數據,而後發給拉流客戶端。
到此爲止整個RTMP的主體流程就是這樣了,這邊不涉及FLV,HLS等具體傳輸協議或者格式轉換的源碼說明,也就是說RTMP服務器怎麼收到推流客戶端的音視頻包也會原封不動地分發給拉流客戶端,並無作額外的處理,不過如今各大雲廠商拉流端都支持http-flv,hls等傳輸協議的支持,而且也支持音視頻的錄製回放點播功能,這塊livego其實也是支持的。
由於篇幅限制,這邊就再也不展開介紹,後續有機會,再單獨一塊兒學習分享介紹livego關於這塊邏輯的處理。
目前基於RTMP協議的直播是國內直播的基準協議,也是各大雲廠商都兼容的直播協議,它的多路複用,分包等優秀特性也是各大廠商選擇它的一個重要緣由。在這個基礎之上,也是由於它是應用層協議,騰訊,阿里,聲網等大型雲廠商,也會對其協議的細節,進行源碼的改造,例如實現多路音視頻流的混流,單路的錄製等功能。
可是RTMP也有它本身自己的缺點,時延較高就是RTMP一個最大的問題,在實際的生產過程當中,即便在比較健康的網絡環境中,RTMP的時延也會有3~8s,這與各大雲廠商給出的1~3s理論時延值仍是有較大出入的。那麼時延會帶來哪些問題呢?咱們能夠想象以下的一些場景:
在線教育,學生提問,老師都講到下一個知識點了,纔看到學生上一個提問。
電商直播,詢問寶貝信息,主播「視而不理」。
打賞後遲遲聽不到主播的口播感謝。
在別人的吶喊聲知道球進了,你看的仍是直播嗎?
特別是如今直播已經造成產業鏈的大環境下,不少主播都是將其做爲一個職業,不少主播使用在公司同一個網絡下進行直播,在公司網絡的出口帶寬有限的狀況下,RTMP和FLV格式的延遲會更加嚴重,高時延的直播影響了用戶和主播的實時互動,也阻礙了一些特殊直播場景的落地,例如帶貨直播,教育直播等。
如下是使用RTMP協議常規的解決方案:
根據實際的網絡狀況和推流的一些設置,例如關鍵幀間隔,推流碼率等等,時延通常會在8秒左右,時延主要來自於2個大的方面:
CDN鏈路延遲, 這分爲兩部分,一部分是網絡傳輸延遲。CDN內部有四段網絡傳輸,假設每段網絡傳輸帶來的延遲是20ms,那這四段延遲即是100ms;此外,使用RTMP幀爲傳輸單位,意味着每一個節點都要收滿一幀以後才能啓動向下游轉發的流程;CDN爲了提高併發性能,會有必定的優化發包策略,會增長部分延遲。在網絡抖動的場景下,延遲就更加沒法控制了,可靠傳輸協議下,一旦有網絡抖動,後續的發送流程都將阻塞,須要等待前序包的重傳。
播放端buffer,這個是延遲的主要來源。公網環境千差萬別,推流、CDN傳輸、播放接收這幾個環節任何一個環節發生網絡抖動,都會影響到播放端。爲了對抗前邊鏈路的抖動,播放器的常規策略是保留6s 左右的媒體buffer。
經過上述說明,咱們能夠清楚的知道,直播最大的延遲就是在於拉流端(播放端buffer)的時延,因此如何快速地去消除這個階段的時延,就是各大雲廠商亟待解決的問題,這就是後續各大雲廠商推出消除RTMP協議時延的新的產品,例如騰訊雲的"快"直播,阿里雲的超低時延RTS直播等等,其實這些直播都引入了WebRTC技術,後續咱們有機會能夠一塊兒學習相關知識。
2.AMF0
3.AMF3
4.FLV 官方文檔
做者:vivo互聯網服務器團隊-Xiong Langyu