一、背景
實時消息傳輸協(xié)議(Real-Time Messaging Protocol)是目前直播的主要協(xié)議阁谆,是Adobe公司為Flash播放器和服務(wù)器之間提供音視頻數(shù)據(jù)傳輸服務(wù)而設(shè)計的應(yīng)用層私有協(xié)議巍沙。RTMP協(xié)議是目前各大云廠商直線直播業(yè)務(wù)所公用的基本直播推拉流協(xié)議谤狡,隨著國內(nèi)直播行業(yè)的發(fā)展和5G時代的到來崖飘,對RTMP協(xié)議有基本的了解,也是我們程序員必須要掌握的基本技能诱桂。
本文主要闡述RTMP的基本思想和核心概念,并且輔之以livego的源碼分析呈昔,和大家一起深入學(xué)習(xí)RTMP協(xié)議最核心的知識點挥等。
二、RTMP協(xié)議特點
RTMP協(xié)議主要的特點有:多路復(fù)用堤尾,分包和應(yīng)用層協(xié)議肝劲。以下將對這些特點進(jìn)行詳細(xì)的描述。
2.1 多路復(fù)用
多路復(fù)用(multiplex)指的是信號發(fā)送端通過一個信道同時傳輸多路信號郭宝,然后信號接收端將一個信道中傳遞過來的多個信號分別組合起來辞槐,分別形成獨立完整的信號信息,以此來更加有效地使用通信線路粘室。
簡而言之榄檬,就是在一個 TCP 連接上,將需要傳遞的Message分成一個或者多個 Chunk衔统,同一個Message 的多個Chunk 組成 ChunkStream鹿榜,在接收端,再把 ChunkStream 中一個個 Chunk 組合起來就可以還原成一個完整的 Message锦爵,這就是多路復(fù)用的基本理念舱殿。
上圖是一個簡單例子,假設(shè)需要傳遞一個300字節(jié)長的Message棉浸,我們可以將其拆分成3個Chunk怀薛,每一個Chunk可以分成 Chunk Header 和 Chunk Data。在Chunk Header 里我們可以標(biāo)記這個Chunk中的一些基本信息迷郑,如 Chunk Stream Id 和 Message Type枝恋;Chunk Data 就是原始信息,上圖中將 Message 分成128+128+44 =300嗡害,這樣就可以完整的傳輸這個Message了焚碌。
關(guān)于 Chunk Header 和 Chunk Data 的格式,后文會進(jìn)行詳細(xì)介紹霸妹。
2.2 分包
RTMP協(xié)議的第二個大的特性就是分包十电,與RTSP協(xié)議相比,分包是RTMP的一個特點。與普通的業(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)先級更高的信息無法傳遞,分包傳輸就是為了解決這個問題而出現(xiàn)的寝殴,具體的分包格式蒿叠,下文會有介紹。
2.3 應(yīng)用層協(xié)議
RTMP最后的一個特性蚣常,就是應(yīng)用層協(xié)議市咽。RTMP協(xié)議默認(rèn)基于傳輸層協(xié)議TCP而實現(xiàn),但是在RTMP的官方文檔中抵蚊,只給定了標(biāo)準(zhǔn)的數(shù)據(jù)傳輸格式說明和一些具體的協(xié)議格式說明施绎,并沒有具體官方的完整實現(xiàn),這就催生出了很多相關(guān)的其他業(yè)內(nèi)實現(xiàn)泌射,例如RTMP over UDP等等相關(guān)的私有改編的協(xié)議出現(xiàn)粘姜,給了大家更多的可擴(kuò)展的空間,方便大家解決原生RTMP存在的直播時延等問題熔酷。
三孤紧、RTMP協(xié)議解析
作為一種應(yīng)用層協(xié)議,和其他私有傳輸協(xié)議一樣(如RPC協(xié)議)拒秘,RTMP也有一些具體代碼實現(xiàn)号显,如 nginx-rtmp、livego 和 srs躺酒。本文選用基于go語言實現(xiàn)的開源直播服務(wù)器 livego 進(jìn)行源碼級的主流程分析押蚤,和大家一起深入學(xué)習(xí) RTMP 推拉流的核心流程的實現(xiàn),幫助大家對RTMP的協(xié)議有一個整體的理解羹应。
在進(jìn)行源碼分析之前揽碘,我們會通過類比RPC協(xié)議的方式,幫助大家對RTMP協(xié)議的格式有一個基本的了解园匹,首先我們可以看一個比較簡單但實用的RPC協(xié)議格式雳刺,如下圖所示:
我們可以看到這是一個在RPC調(diào)用過程中所使用的數(shù)據(jù)傳輸格式,之所以使用這樣的格式裸违,根本目的還是為了解決"粘包和拆包"的問題掖桦。
以下簡要描述圖中RPC協(xié)議的格式:首先用2個字節(jié),MAGIC來表示魔數(shù)供汛,標(biāo)記該協(xié)議是對端都能識別的標(biāo)識枪汪,如果接收到的2個字節(jié)不是0xbabe的話涌穆,則直接丟棄該包;第二個sign占用1個字節(jié)雀久,低4位表示消息的類型request/response/heartbeat宿稀,高4位表示序列化類型例如json,hessian岸啡,protobuf原叮,kyro等等;第三個 status 占用一個字節(jié)巡蘸,表示狀態(tài)位;隨后使用8個字節(jié)來表示調(diào)用的requestId擂送,一般使用低48位(2的48次方)就足夠表示requestId了悦荒;接著使用4字節(jié)定長的body size來表示Body Content,通過這樣的方式就能夠很快的解析出RPC消息Message的完整請求對象了嘹吨。
通過分析上述的一個簡單的RPC協(xié)議搬味,其實我們能夠發(fā)現(xiàn)一個很好的思想,就是最大效率的使用字節(jié)蟀拷,即使用最小的字節(jié)數(shù)組碰纬,來傳輸最多的數(shù)據(jù)信息。小小的一個字節(jié)能夠帶來很多的信息量问芬,畢竟一個字節(jié)它有64種不同的變化悦析。在網(wǎng)絡(luò)中,如果只需要利用一個字節(jié)就能夠傳遞很多有用的信息的話此衅,那么我們就可以使用極其有限的資源來得到最大的資源利用了强戴。RTMP的官方文檔在2012年就出現(xiàn)了,雖然以目前的眼光來看挡鞍,RTMP協(xié)議實現(xiàn)的非常復(fù)雜骑歹,甚至有些臃腫,但是它在2012年的時候墨微,就能夠有比較先進(jìn)的思想道媚,的確是我們學(xué)習(xí)的榜樣。
在當(dāng)今WebRTC協(xié)議橫行的年代里翘县,我們也能夠從WebRTC的設(shè)計實現(xiàn)中最域,看到RTMP的影子,上述的RPC協(xié)議我們就可以認(rèn)為是一個與RTMP具有相似設(shè)計理念的簡化版設(shè)計炼蹦。
3.1 RTMP核心概念說明
在分析RTMP源碼之前羡宙,我們先對RTMP協(xié)議中的幾個核心概念做具體說明,方便我們在宏觀上對RTMP整個協(xié)議棧有一個基本的了解掐隐,并且在后文源碼分析期間狗热,我們也會通過抓包的方式钞馁,更加直觀地幫助我們?nèi)シ治鱿嚓P(guān)的原理。
首先匿刮,和剛才的RPC協(xié)議格式一樣僧凰,RTMP實際傳輸?shù)膶嶓w對象是Chunk,一個Chunk由Chunk Header和Chunk Body兩個部分組成熟丸,如下圖所示训措。
3.1.1Chunk Header
Chunk Header這個部分和我們前面說過的RPC協(xié)議不太一樣,主要是RTMP協(xié)議的Chunk Header的長度不是固定的光羞,為什么不是固定的呢绩鸣?其實還是Adobe公司為了節(jié)省數(shù)據(jù)傳輸開銷。從剛才將一個300字節(jié)的Message拆分成3個Chunk的例子中纱兑,我們可以看到多路復(fù)用其實也是有一個比較明顯的缺點呀闻,就是我們需要有一個Chunk Header來標(biāo)記這個Chunk的基本信息,這樣其實就是在傳輸?shù)臅r候有了額外字節(jié)流傳輸?shù)拈_銷潜慎。所以為了保證傳輸?shù)淖止?jié)數(shù)最少捡多,我們就需要不斷地壓榨著RTMP的Header的大小,確保Header的大小達(dá)到最小铐炫,這樣才能達(dá)到最高的傳輸效率垒手。
首先我們研究一下Chunk Header中Basic Header的部分,Basic Header的長度就是不固定的倒信,可以是1個字節(jié)科贬,2個字節(jié)或者3個字節(jié),這取決于Chunk Stream Id(縮寫:csid)堤结。
RTMP協(xié)議支持的csid的范圍是2~65599唆迁,0和1是協(xié)議保留值,用戶不可使用竞穷。Basic Header至少含有1個字節(jié)(低8位)唐责,它的長度就是這1個字節(jié)決定的,如下圖所示瘾带。該字節(jié)高2位留給 fmt鼠哥,fmt的取值決定了 Message Header 的格式,這個在后面會講到看政。該字節(jié)的低6位就是 csid 的值朴恳,當(dāng)?shù)?位的 csid 取值為0時,表示真實 csid 值大到無法用6個bit表示了允蚣,需要借助后續(xù)的一個字節(jié)才行于颖;當(dāng)?shù)?位的 csid 取值為1時,表示真實 csid 值大到無法用14個bit表示了嚷兔,需要再借助后續(xù)的一個字節(jié)才行森渐。于是做入,整個Basic Header的長度看起來就不是固定的了,完全取決于首字節(jié)的低6位的csid的值同衣。
實際應(yīng)用中竟块,并沒有使用到那么多csid,也就是說一般情況下耐齐,Basic Header長度為一個字節(jié)浪秘,csid取值范圍為 2~63。
剛才說了那么多埠况,才僅僅說了Basic Header耸携,而Basci Header只是Chunk Header的組成部分之一,比較喜歡折騰的RTMP協(xié)議的作者辕翰,把RTMP的Chunk Header模塊又設(shè)計成了動態(tài)大小的违帆,簡而言之也是為了節(jié)省傳輸空間,這邊能夠方便理解的地方就是Chunk Message Header的長度也分四種情況金蜀,這就是前面提到的 fmt 這個值決定的。
Message Header 的四種格式如下圖所示:
當(dāng) fmt 為 0 的時候的畴,Message Header占用11個字節(jié)(請注意渊抄,這邊的11個字節(jié)不包括Basic Header的長度),由3個字節(jié)長度的timestamp丧裁,3個字節(jié)長度的message length护桦,1個字節(jié)長度的message type Id,4個字節(jié)長度的message stream Id所組成的煎娇。
其中二庵,timestamp 是絕對時間戳,表示的是這個消息發(fā)送的時間缓呛;message length 表示的是chunk body的長度催享;message type id 表示的是消息類型,這個在后文會具體講到哟绊;message stream id 是消息唯一標(biāo)識因妙。這邊需要注意的是,如果這個消息的絕對時間戳大于0xFFFFFF票髓,說明這個時間大到無法用3個字節(jié)來表示攀涵,需要借助擴(kuò)展時間戳(Extended Timestamp)來表示,擴(kuò)展時間戳長度為4個字節(jié)洽沟,默認(rèn)放在Chunk Header和Chunk Body之間以故。
當(dāng) fmt 為 1的時候,Message Header占用7個字節(jié)裆操,與之前的11個字節(jié)的chunk header相比怒详,少了一個message stream id炉媒,這個chunk是復(fù)用之前的chunk stream id,這個一般用于可變長的消息結(jié)構(gòu)棘利。
當(dāng) fmt 為 2的時候橱野,Message Header只占用3個字節(jié),就只包含timestamp的三個字節(jié)善玫,與之前相比水援,既少了stream id也少了message length,這種少了message length的茅郎,一般用于固定長度但是需要修正時間的消息(如:音頻數(shù)據(jù))蜗元。
當(dāng) fmt 為 3的時候,Chunk Header里就不包含 Message Header 了系冗。一般來說奕扣,在拆包的時候,把一個完整的RTMP的Message消息掌敬,會拆成第一個是fmt 為 0的Chunk消息惯豆,隨后的消息也會拆成fmt為3的消息,這樣的做的方式就是第一個Chunk附帶著最全的Chunk消息信息,后續(xù)Chunk信息的Header就會比較小,這樣實現(xiàn)比較簡單拔恰,壓縮率也是比較好幽污。當(dāng)然,如果第一個Message發(fā)送成功之后,第二個Message再次發(fā)送的時候,就會把第二個Message的第一個Chunk設(shè)置成fmt為1類型的Chunk,隨后該Message的Chunk的fmt為3揭厚,這樣就能夠進(jìn)行消息的區(qū)分。
3.1.2 Chunk Body
剛才花了很多時間去描述Chunk Header扶供,接下來我們再針對Chunk Body進(jìn)行簡單的描述筛圆。與Chunk Header相比,Chunk Body就比較簡單诚欠,沒有那么多變長的控制顽染,結(jié)構(gòu)也比較簡單,這個里面的數(shù)據(jù)也就是真正有業(yè)務(wù)含義的數(shù)據(jù)轰绵,長度默認(rèn)是128個字節(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 官方文檔。
3.1.3 AMF
AMF(Action Message Format) 是一種類似JSON液样,XML的二進(jìn)制數(shù)據(jù)序列化格式振亮,Adobe Flash與遠(yuǎn)程服務(wù)端可通過AMF格式的數(shù)據(jù)進(jìn)行數(shù)據(jù)通信巧还。
AMF具體的格式其實與Map的數(shù)據(jù)結(jié)構(gòu)很相似,就是在KV鍵值對的基礎(chǔ)上坊秸,中間多加了一個Value值的length麸祷。AMF的結(jié)果基本如下圖所示,有時候len字段就是空褒搔,這個是由type來決定的阶牍,我們舉例來說,例如我們傳輸?shù)氖莕umber類型的AMF格式的數(shù)據(jù)星瘾,那么len字段我們就可以忽略走孽,因為我們默認(rèn)number類型的字段占用8個字節(jié),我們這邊就可以忽略了琳状。
再舉例來說磕瓷,AMF如果傳輸?shù)氖?x02 string類型的數(shù)據(jù)的時候,len的長度就默認(rèn)占據(jù)2個字節(jié)念逞,因為2個字節(jié)足夠表示后面value的最大長度了困食。以此類推,當(dāng)然有些時候翎承,len和value的值都不存在陷舅,就比如傳遞0x05 傳遞null的時候,len和value我們就都不需要了审洞。
以下列舉一些常用的AMF的type的對應(yīng)表格,更多信息可以查看官方文檔待讳。
我們可以通過WireShark來抓包芒澜,實際來體驗一下具體的AMF0的格式。
如上圖所示创淡,這是一個非常典型的AMF0類型string結(jié)構(gòu)的抓包痴晦。AMF目前有2個主要的版本,分別是AFM0和AMF3琳彩,在目前的實際使用場景中誊酌,AMF0還是占據(jù)主流的地位。那么AMF0和AMF3有什么區(qū)別呢露乏,當(dāng)客戶端給服務(wù)器端發(fā)送AMF格式Chunk Data數(shù)據(jù)的時候碧浊,服務(wù)端在接收到該信息的時候,如何是知道AMF0或者是AMF3呢瘟仿?實際上RTMP在Chunk Header中使用message type id來進(jìn)行區(qū)分箱锐,當(dāng)消息使用AMF0編碼時,message type id等于20劳较,使用AMF3編碼時message type id等于17驹止。
3.1.4 Chunk & Message
首先浩聋,用一句話來總結(jié)一下Chunk和Message的關(guān)系,一個Message是由多個Chunk組成臊恋,多個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 兩類,兩類分別有多個函數(shù)腻惠,該消息的調(diào)用环肘,可理解為遠(yuǎn)程函數(shù)調(diào)用。
總覽圖如下集灌,后續(xù)在源碼解析章節(jié)悔雹,會進(jìn)行具體介紹,其中著色部分為常用消息欣喧。
3.2 核心實現(xiàn)流程
網(wǎng)絡(luò)協(xié)議的學(xué)習(xí)是一個枯燥的過程腌零,我們嘗試結(jié)合 RTMP協(xié)議原文和WireShark抓包的方式,盡量形象地給大家描述 RTMP 協(xié)議中的核心流程唆阿,包括握手益涧,連接,createStream驯鳖,推流和拉流闲询。本節(jié)所有的抓包數(shù)據(jù)的基本環(huán)境是:livego作為RTMP服務(wù)器(服務(wù)端口為1935),OBS作為推流應(yīng)用浅辙,VLC作為拉流應(yīng)用扭弧。
作為一個應(yīng)用層協(xié)議解析來說,首先记舆,我們要注意的就是主體流程的把握寄狼,對于每一個 RTMP 服務(wù)器來說,每一個推流和拉流從代碼層面來說,都是一個網(wǎng)絡(luò)鏈接泊愧,針對每一個連接伊磺,我們要進(jìn)行對應(yīng)的工序進(jìn)行處理,我們可以看到livego中源碼中所展示的一樣删咱,有一個handleConn方法屑埋,顧名思義,就是用來處理每一個連接痰滋,按照主流程來說摘能,分為第一部分的握手,第二個核心模塊的依據(jù)RTMP包協(xié)議敲街,進(jìn)行Chunk header和Chunk body的解析团搞,后續(xù)再根據(jù)解析出來的Chunk header和Chunk body再做具體的處理。
可以看到上述代碼塊多艇,主要有2個核心方法:一個是HandshakeServer逻恐,主要處理握手邏輯;另一個是ReadMsg方法峻黍,主要處理Chunk header和Chunk body信息的讀取复隆。
3.2.1 第一部分-握手(Handshake)
協(xié)議原文的5.2.5節(jié)詳細(xì)介紹了 RTMP 握手的過程,圖示如下:
乍一看姆涩,可能會覺得此過程有些復(fù)雜挽拂。所以,我們還是先用 WireShark 抓包來整體看看過程吧骨饿。
WireShark 抓包的 Info 能夠為我們解讀 RTMP 包的含義亏栈,從下圖可以看出,握手主要涉及到3個包宏赘。其中第16號包是客戶端向服務(wù)端發(fā)送 C0 和 C1 消息仑扑,18號包是服務(wù)端向客戶端發(fā)送 S0,S1 和 S2 消息置鼻,20號包是客戶端向服務(wù)端發(fā)送 C2 消息。如此蜓竹,客戶端和服務(wù)端就完成了握手過程箕母。
通過 WireShark 抓包可以看出,握手過程還是非常簡潔的俱济,有點類似 TCP 三次握手的過程嘶是,所以從實際抓包來說,與RTMP協(xié)議原文的5.2.5節(jié)介紹的還是有些出入的蛛碌,整體流程變得很簡潔聂喇。
現(xiàn)在可以回頭看看上面那個比較復(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 抓包分析可以看出枉证,整個握手過程的確是遵循了以上規(guī)定“校現(xiàn)在問題來了,C0室谚,C1毡鉴,C2,S0秒赤,S1 和 S2 這些消息到底是些什么玩意猪瞬?其實,RTMP 協(xié)議規(guī)范里面明確定義了它們的數(shù)據(jù)格式入篮。
C0 和 S0:1個字節(jié)長度陈瘦,該消息指定了 RTMP 版本號。取值范圍 0~255潮售,我們只需要知道 3 才是我們需要的就行痊项。其他取值含義感興趣的話可以閱讀協(xié)議原文。
C1 和 S1:1536個字節(jié)長度酥诽,由 時間戳+零值+隨機(jī)數(shù)據(jù) 組成鞍泉,握手過程的中間包。
C2 和 S2:1536個字節(jié)長度肮帐,由 時間戳+時間戳2+隨機(jī)數(shù)據(jù)回傳 組成咖驮,基本上是 C1 和 S1 的 echo 數(shù)據(jù)。一般在實現(xiàn)上,會令 S2 = C1托修,C2 = S1忘巧。
下面我們結(jié)合 livego 源碼來加強(qiáng)對握手過程的理解。
到此為止诀黍,最簡單的握手流程就到此結(jié)束了袋坑,可以看出整個握手流程還是比較清晰的,處理邏輯也是比較簡單眯勾,也比較便于理解枣宫。
3.2.2 第二部分-信息交換
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)換成我們可識別的信息處理翅娶,再根據(jù)這些可識別的信息數(shù)據(jù),再做對應(yīng)流程的處理好唯,這塊是源碼解析的關(guān)鍵核心竭沫,涉及的知識點非常多,大家可以結(jié)合上文一起看骑篙,可以方便大家理解ReadMsg這塊核心邏輯的理解蜕提。
上述的代碼塊邏輯很清晰,主要是讀取每一個conn連接中靶端,進(jìn)行對應(yīng)的編解碼谎势,獲取到一個個Chunk,并且將相同ChunkStreamId的Chunk再次進(jìn)行合并杨名,合并成對應(yīng)的Chunk Stream脏榆,最后一個個完整的Chunk Stream就是Message了。
這塊代碼就是和我們之前理論部分知識介紹的chunkstreamId那塊知識比較接近的地方了台谍,大家可以結(jié)合起來一起看须喂,大家在腦海中,要注意就是一個conn連接趁蕊,會傳遞多個Message坞生,例如連接Message,createStreamMessage等等介衔,每一個Message就是Chunk Stream,也就是多個csid相同的Chunk,所以livego的作者使用map這樣的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲骂因,key就是csid炎咖,value就是chunkstream,這樣就可以將向rtmp服務(wù)器發(fā)送過來的信息能夠全部保存下來。
readChunk代碼的具體邏輯實現(xiàn)分成如下幾個部分:
1)csid的修正乘盼,至于理論部分參照上述邏輯升熊,這塊其實是basic header的處理。
2)Chunk Header按照format的數(shù)值進(jìn)行對應(yīng)的解析處理绸栅,上文理論部分也已經(jīng)介紹過了级野,下文也有具體的注釋解釋,有兩個技術(shù)點需要注意第一就是timestramp時間戳的處理粹胯,第二個注意點是chunk.new(pool)這行代碼蓖柔,也是需要大家注意,代碼注釋中也寫的比較清楚风纠。
3)Chunk Body的讀取處理况鸣,上文理論部分說過,Chunk header中當(dāng)fmt 為 0 的時候竹观,會有一個message length字段镐捧,這個字段會控制Chunk Body的大小,依據(jù)這個字段臭增,我們可以很輕松地讀取到Chunk body信息的讀取懂酱,整體邏輯如下。
到此為止誊抛,我們已經(jīng)成功解析了Chunk Header列牺,讀取了Chunk Body,注意我們只是讀取了Chunk Body還沒有按照AMF格式對Chunk Body進(jìn)行解析芍锚,針對Chunk Body部分的邏輯處理昔园,在下文會進(jìn)行詳細(xì)的源碼介紹,不過現(xiàn)在我們已經(jīng)解析到了一個連接發(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中也是這個RTMP服務(wù)端處理客戶端命令的代碼精髓了癌瘾,可以看出livego是支持AMF3和AMF0的觅丰,AMF3和AMF0的區(qū)別,上文也已經(jīng)介紹過了妨退,下文的代碼注釋寫的也比較清楚妇萄,然后就是解析AMF格式的Chunk Body的數(shù)據(jù)蜕企,解析出來的結(jié)果也是按照Slice格式進(jìn)行存儲。
解析好typeId和AMF冠句,接下來就是水到渠成的對各個命令進(jìn)行處理了轻掩。
接下來是針對每一個客戶端命令的處理了。
3.2.2.2 連接
連接(Connect)命令處理過程:連接過程客戶端和服務(wù)端會完成窗口大小懦底,傳輸塊大小和帶寬大小的確認(rèn)唇牧,RTMP 協(xié)議原文詳細(xì)介紹了連接過程,如下圖所示:
同樣聚唐,我們這里用 WireShark 抓包分析:
從抓包可以看出丐重,連接過程只用了3個包就完成了:
22 號包:客戶端告訴服務(wù)端,我想要設(shè)置 chunk size 為 4096拱层;
24 號包:客戶端告訴服務(wù)端弥臼,我想要連接叫 “l(fā)ive” 的應(yīng)用;
26 號包:服務(wù)端響應(yīng)客戶端的連接請求根灯,確定窗口大小径缅,帶寬大小和 chunk size,以及返回 “_result” 表示響應(yīng)成功烙肺。這些都是通過一個 TCP 包來完成的纳猪。
那么客戶端和服務(wù)端是如何知道這些包的含義的呢?這就是 RTMP 協(xié)議規(guī)范所制定的規(guī)則了桃笙,我們可以通過閱讀規(guī)范來了解氏堤,當(dāng)然也可以通過 wrieshark 來幫助我們快速解析。以下是 22 號包的詳細(xì)解析搏明,我們重點關(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,時間戳直接忽略膛锭。從 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俊马,因為真正起作用的是 Message Type ID丁存,服務(wù)端根據(jù) Message Type ID 來做相應(yīng)的響應(yīng)。連接過程發(fā)送的 AMF0 命令攜帶的是 Object 類型的數(shù)據(jù)柴我,會告訴服務(wù)端要連接的應(yīng)用名和播放地址等信息解寝。
以下代碼是 livego 處理客戶端請求連接的過程。
收到客戶端連接應(yīng)用的請求后艘儒,服務(wù)端需要作出相應(yīng)響應(yīng)給客戶端聋伦,也就是 WireShark 抓取的 26 號包的內(nèi)容,詳細(xì)內(nèi)容如下圖所示界睁,可以看到服務(wù)端在一個包里面做了好幾件事情觉增。
我們可以結(jié)合 livego 源碼來深入學(xué)習(xí)該過程。
3.2.2.3 createStream
連接完成后翻斟,就可以創(chuàng)建流了逾礁。創(chuàng)建流的過程相對來說比較簡單,只需要兩個包就能夠?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ì)介紹過了,我們重點看發(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)容监憎,該請求中需要帶上頻道名,在這個包里面就是"rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"婶溯。
服務(wù)端會首先會檢測這個頻道名是否存在以及檢查這個推流名是否被使用中鲸阔,如果不存在或者在使用的話就會拒絕客戶端的推流請求。由于我們在推流前已經(jīng)生成了該頻道名迄委,客戶端可以合法使用褐筛,于是服務(wù)端在38號包中回應(yīng)的是 "NetStream.Publish.Start",也就是告訴客戶端可以開始推流了叙身∮嬖客戶端在推流音視頻數(shù)據(jù)前需要先把音視頻的的元數(shù)據(jù)發(fā)給服務(wù)端,也就是40號包所做的事情信轿,我們可以看一下該包的詳細(xì)內(nèi)容晃痴。從下圖可以看出,發(fā)送元數(shù)據(jù)信息比較多财忽,包含有視頻分辨率愧旦,幀率,音頻采樣率和音頻聲道等關(guān)鍵信息定罢。
告訴服務(wù)端音視頻元數(shù)據(jù)后笤虫,客戶端就可以開始發(fā)送有效的音視頻數(shù)據(jù)了,服務(wù)端會一直接收這些數(shù)據(jù)祖凫,直到客戶端發(fā)出 FCUnpublish 和 deleteStream 命令為止琼蚯。stream.go 的 TransStart() 方法主要邏輯為接收推流客戶端的音視頻數(shù)據(jù),然后在本地緩存最新的一個數(shù)據(jù)包惠况,最后將音視頻數(shù)據(jù)發(fā)給各個拉流端遭庶。其中讀取推流客戶單音視頻數(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)講述過了龙屉,我們重點關(guān)注下 play 命令的過程就行。
同樣满俗,我們先用 WireShark 抓包來分析下转捕∽麽客戶端通過 640 號包告訴服務(wù)器,我想要播放叫 “movie” 的頻道五芝。
此處為什么是叫 “movie” 而不是推流時候用的“rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk”痘儡,其實這兩個指向的是同一個頻道,只不過一個用于推流一個用于拉流枢步,我們可以從 livego 的源碼來印證這一點谤辜。
服務(wù)端收到拉流客戶端的 play 請求后,會做出響應(yīng) "NetStream.Play.Reset"价捧,"NetStream.Play.Start" ,"NetStream.Play.PublishNotify" 和音視頻元數(shù)據(jù)涡戳。這些工作做完后结蟋,就可以持續(xù)發(fā)送音視頻數(shù)據(jù)給拉流客戶端了。我們可以通過 livego 源碼來加深一下對此過程的理解渔彰。
通過 chan 讀取推流數(shù)據(jù)嵌屎,然后發(fā)給拉流客戶端。
到此為止整個RTMP的主體流程就是這樣了恍涂,這邊不涉及FLV宝惰,HLS等具體傳輸協(xié)議或者格式轉(zhuǎn)換的源碼說明,也就是說RTMP服務(wù)器怎么收到推流客戶端的音視頻包也會原封不動地分發(fā)給拉流客戶端再沧,并沒有做額外的處理尼夺,不過現(xiàn)在各大云廠商拉流端都支持http-flv,hls等傳輸協(xié)議的支持炒瘸,并且也支持音視頻的錄制回放點播功能淤堵,這塊livego其實也是支持的。
因為篇幅限制顷扩,這邊就不再展開介紹拐邪,后續(xù)有機(jī)會,再單獨一起學(xué)習(xí)分享介紹livego關(guān)于這塊邏輯的處理隘截。
四扎阶、展望
目前基于RTMP協(xié)議的直播是國內(nèi)直播的基準(zhǔn)協(xié)議,也是各大云廠商都兼容的直播協(xié)議婶芭,它的多路復(fù)用东臀,分包等優(yōu)秀特性也是各大廠商選擇它的一個重要原因。在這個基礎(chǔ)之上犀农,也是因為它是應(yīng)用層協(xié)議啡邑,騰訊,阿里井赌,聲網(wǎng)等大型云廠商谤逼,也會對其協(xié)議的細(xì)節(jié)贵扰,進(jìn)行源碼的改造,例如實現(xiàn)多路音視頻流的混流流部,單路的錄制等功能戚绕。
但是RTMP也有它自己本身的缺點,時延較高就是RTMP一個最大的問題枝冀,在實際的生產(chǎn)過程中舞丛,即使在比較健康的網(wǎng)絡(luò)環(huán)境中,RTMP的時延也會有38s果漾,這與各大云廠商給出的13s理論時延值還是有較大出入的球切。那么時延會帶來哪些問題呢?我們可以想象如下的一些場景:
在線教育绒障,學(xué)生提問吨凑,老師都講到下一個知識點了,才看到學(xué)生上一個提問户辱。
電商直播鸵钝,詢問寶貝信息,主播“視而不理”庐镐。
打賞后遲遲聽不到主播的口播感謝恩商。
在別人的吶喊聲知道球進(jìn)了,你看的還是直播嗎必逆?
特別是現(xiàn)在直播已經(jīng)形成產(chǎn)業(yè)鏈的大環(huán)境下怠堪,很多主播都是將其作為一個職業(yè),很多主播使用在公司同一個網(wǎng)絡(luò)下進(jìn)行直播名眉,在公司網(wǎng)絡(luò)的出口帶寬有限的情況下研叫,RTMP和FLV格式的延遲會更加嚴(yán)重,高時延的直播影響了用戶和主播的實時互動璧针,也阻礙了一些特殊直播場景的落地嚷炉,例如帶貨直播,教育直播等探橱。
以下是使用RTMP協(xié)議常規(guī)的解決方案:
根據(jù)實際的網(wǎng)絡(luò)情況和推流的一些設(shè)置申屹,例如關(guān)鍵幀間隔,推流碼率等等隧膏,時延一般會在8秒左右哗讥,時延主要來自于2個大的方面:
CDN鏈路延遲, 這分為兩部分胞枕,一部分是網(wǎng)絡(luò)傳輸延遲杆煞。CDN內(nèi)部有四段網(wǎng)絡(luò)傳輸,假設(shè)每段網(wǎng)絡(luò)傳輸帶來的延遲是20ms,那這四段延遲便是100ms;此外,使用RTMP幀為傳輸單位慰技,意味著每個節(jié)點都要收滿一幀之后才能啟動向下游轉(zhuǎn)發(fā)的流程;CDN為了提升并發(fā)性能蚌斩,會有一定的優(yōu)化發(fā)包策略,會增加部分延遲范嘱。在網(wǎng)絡(luò)抖動的場景下送膳,延遲就更加無法控制了,可靠傳輸協(xié)議下丑蛤,一旦有網(wǎng)絡(luò)抖動叠聋,后續(xù)的發(fā)送流程都將阻塞,需要等待前序包的重傳受裹。
播放端buffer碌补,這個是延遲的主要來源。公網(wǎng)環(huán)境千差萬別名斟,推流、CDN傳輸魄眉、播放接收這幾個環(huán)節(jié)任何一個環(huán)節(jié)發(fā)生網(wǎng)絡(luò)抖動砰盐,都會影響到播放端。為了對抗前邊鏈路的抖動坑律,播放器的常規(guī)策略是保留6s 左右的媒體buffer岩梳。
通過上述說明,我們可以清楚的知道晃择,直播最大的延遲就是在于拉流端(播放端buffer)的時延冀值,所以如何快速地去消除這個階段的時延,就是各大云廠商亟待解決的問題宫屠,這就是后續(xù)各大云廠商推出消除RTMP協(xié)議時延的新的產(chǎn)品列疗,例如騰訊云的"快"直播,阿里云的超低時延RTS直播等等浪蹂,其實這些直播都引入了WebRTC技術(shù)抵栈,后續(xù)我們有機(jī)會可以一起學(xué)習(xí)相關(guān)知識。
五坤次、參考資料
2.AMF0
3.AMF3
4.FLV 官方文檔
作者:vivo互聯(lián)網(wǎng)服務(wù)器團(tuán)隊-Xiong Langyu