RTMP詳細分析(三次握手)
librtmp分析(發送數據包處理)
librtmp分析(接收數據包處理)
目錄
- 1、Message(消息)
- 2、Chunking(Message 分塊)
- 2.1、 Basic Header(基本的頭信息)
- 2.1.1、Basic Header為1個字節時
- 2.1.2、Basic Header為2個字節時
- 2.1.3、Basic Header為3個字節時
- 2.2、Message Header(消息的頭信息)
- 2.2.1、Type(fmt) = 0:占用11個字節
- 2.2.2、Type(fmt) = 1:占用7個字節
- 2.2.3、Type(fmt) = 2:占用3個字節
- 2.2.4、Type(fmt) = 3:占用0個字節
- 2.3、Extended Timestamp(擴展時間戳)
- 2.4、Chunk Data(塊數據)
- 3、協議控制消息(Protocol Control Message)
- 3.1、Set Chunk Size(ID=1)
- 3.2、Abort Message (ID=2)
- 3.3、Acknowledgement (ID=3)、Window Acknowledgement Size (ID=5)
- 3.4、Set Peer Bandwidth (ID=6)
- 4、用戶控制消息(User Control Message Events ID=4)
- 5、命令消息(Command Message Message ID=17或20)
- 5.1、NetConnection Commands(連接層的命令)
- 5.1.1、connect:用于客戶端向服務器發送連接請求
- 5.1.2、Call:用于在對端執行某函數
- 5.1.3、Create Stream:創建傳遞具體信息的通道
- 5.2、NetStream Commands(流連接上的命令)
- 5.2.1、play(播放)
- 5.2.2、play2(播放)
- 5.2.3、deleteStream(刪除流)
- 5.2.4、 receiveAudio(接收音頻)
- 5.2.5、 receiveVideo(接收視頻)
- 5.2.6、 publish(推送數據)
- 5.2.7、 seek(定位流的位置)
- 5.2.8、pause(暫停)
- 6、數據消息(Data Message ID=15或18)(也是對應flv中的script data的tag type = 18 )
- 7、共享消息(Shared Object Message ID=16或19)
- 8、音頻消息(Audio Message ID=8)(也是對應flv中的audio data的tag type = 8 )
- 9、視頻消息(Video Message ID=9)(也是對應flv中的video data的tag type = 9 )
- 10、聚集消息(Aggregate Message ID=22)
- 11、推流流程
- 12、拉流流程
RTMP Chunk Stream
Chunk Stream是對傳輸RTMP Chunk的流的邏輯上的抽象,客戶端和服務器之間有關RTMP的信息都在
這個流上通信。這個流上的操作也是我們關注RTMP協議的重點。
1、Message(消息)
這里的Message是指滿足該協議格式的、可以切分成Chunk發送的消息,消息包含的字段如下:
Timestamp(時間戳):消息的時間戳(但不一定是當前時間),4個字節。
Length(長度):是指Message Payload(消息負載)即音視頻等信息的數據的長度,3個字節。
TypeId(類型Id):消息的類型Id,1個字節。
Message Stream ID(消息的流ID,應該叫Chunk Stream ID更準確):每個消息的唯一標識,劃分成Chunk和還原Chunk為Message的時候都是根據這個ID來辨識是否是同一個消息的Chunk的,4個字節,并且以小端格式存儲。
(Message Stream ID如何產生?audio和video使用不同的Message Stream ID)
2、Chunking(Message 分塊)
RTMP在收發數據的時候并不是以Message為單位的,而是把Message拆分成Chunk發送,而且必須
在一個Chunk發送完成之后才能開始發送下一個Chunk。每個Chunk中帶有MessageID(Chunk Stream ID)代表屬于哪個Message,接受端也會按照這個id來將chunk組裝成Message。
為什么RTMP要將Message拆分成不同的Chunk呢?通過拆分,數據量較大的Message可以被拆分成較小的“Message”,這樣就可以避免優先級低的消息持續發送阻塞優先級高的數據,比如在視頻的傳輸過程中,會包括視頻幀,音頻幀和RTMP控制信息,如果持續發送音頻數據或者控制數據的話可能就會造成視頻幀的阻塞,然后就會造成看視頻時最煩人的卡頓現象。同時對于數據量較大的Message,可以通過對
Chunk Header的字段來壓縮信息,從而減少信息的傳輸量。
Chunk的默認大小是128字節,在傳輸過程中,通過一個叫做Set Chunk Size的控制信息可以設置Chunk數據量的最大值,在發送端和接受端會各自維護一個Chunk Size(srs流媒體服務器默認是60000),可以分別設置這個值來改變這一方發送的Chunk的最大值。大一點的Chunk減少了計算每個chunk的時間從而減少了CPU的占用率,但是它會占用更多的時間在發送上,尤其是在低帶寬的網絡情況下,很可能會阻塞后面更重要信息的傳輸。小一點的Chunk可以減少這種阻塞問題,但小的Chunk會引起過多額外的信息(Chunk中的Header),少量多次的傳輸也可能會造成發送的間斷導致不能充分利用高帶寬的優勢,因此并不適合在高比特率的流中傳輸。在實際發送時應對要發送的數據用不同的Chunk Size去嘗試,通過抓包分析等手段得出合適的Chunk大小,并且在傳輸過程中可以根據當前的帶寬信息和實際信息的大小動態調Chunk的大小,從而盡量提高CPU的利用率并減少信息的阻塞機率。
Chunk Format(塊格式):
2.1、 Basic Header(基本的頭信息)
包含了chunk stream ID(流通道Id)和chunk type(chunk的類型),chunk stream id一般被簡寫為CSID,用來唯一標識一個特定的流通道,chunk type決定了后面Message Header的格式。Basic Header的長度可能是1,2,或3個字節,其中chunk type的長度是固定的(占2位,注意單位是位,bit),Basic Header是變長的,其長度取決于CSID的大小,在足夠存儲這兩個字段的前提下最好用盡量少的字節從而減少由于引入Header增加的數據量。
RTMP協議最多支持65597個用戶自定義chunk stream ID,范圍為[3,65599] ,ID 0, 1, 2被協議規范直接使用,其中ID值為0, 1分表表示了Basic Header占用2個字節和3個字節:
ID值0:代表Basic Header占用2個字節,CSID在 [64,319] 之間;
ID值1:代表Basic Header占用3個字節,CSID在 [64,65599] 之間;
ID值2:代表該chunk是控制信息和一些命令信息,后面會有詳細的介紹。
2.1.1、Basic Header為1個字節時
CSID占6位,6位最多可以表示64個數,因此這種情況下CSID在 [0,
63] 之間,其中用戶可以定義的范圍為 [3,63] 。
2.1.2、Basic Header為2個字節時
CSID占只占8位,第一個字節除chunk type占用的bit都置為0,第二
個字節用來表示CSID-64,8位可以表示 [0, 255] 共256個數,ID的計算方法為(第二個字節+64),范圍為 [64,319]。
2.1.3、Basic Header為3個字節時
在此字段用3字節版本編碼。ID的計算方法為(第三字節*256+第二字節+64)(Basic Header是采用小端存儲的方式),范圍為 [64,65599]。
可以看到2個字節和3個字節的Basic Header所能表示的CSID是有交集的 [64,319],但實際實現時還是應該秉著最少字節的原則使用2個字節的表示方式來表示 [64,319] 的CSID。
2.2、Message Header(消息的頭信息)
包含了要發送的實際信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和長度取決于Basic Header的chunk type,共有4種不同的格式,由上面所提到的Basic Header中的fmt 字段控制。其中第一種格式可以表示其他三種表示的所有數據,但由于其他三種格式是基于對之前chunk的差量化的表示,因此可以更簡潔地表示相同的數據,實際使用的時候還是應該采樣盡量少的字節表示相同意義的數據。
以下按照字節數從多到少的順序分別介紹這4種格式的Message Header。
2.2.1、Type(fmt) = 0:占用11個字節
Type(fmg) = 0時,Message Header占用11個字節,其他三種能表示的數據它都能表示,但在chunk stream的開始的第一個chunk和頭信息中的時間戳后退(即值與上一個chunk相比減小,通常在回退播放的時候會出現這種情況)的時候必須采用這種格式。
timestamp(時間戳):占用3個字節,因此它最多能表示到16777215=0xFFFFFF, 當它的值超過這個最大值時,這三個字節都置為1,這樣實際的timestamp會轉存到Extended Timestamp字段中,接受端在判斷timestamp字段24個位都為1時就會去Extended timestamp中解析實際的時間戳。
message length(消息數據的長度):占用3個字節,表示實際發送的消息的數據如音頻幀、視頻幀等數據的長度,單位是字節。注意這里是Message的長度,也就是chunk屬于的Message的總數據長度,而不是chunk本身Data的數據的長度。
message type id(消息的類型id):占用1個字節,表示實際發送的數據的類型,如8代表音頻數據、9代表視頻數據。
msg stream id(消息的流id):占用4個字節,表示該chunk所在的流的ID,和Basic Header的CSID一樣,它采用小端存儲的方式。
2.2.2、Type(fmt) = 1:占用7個字節
Type(fmg) = 1時,Message Header占用7個字節,省去了表示msg stream id的4個字節,表示此chunk和上一次發的chunk所在的流相同,如果在發送端只和對端有一個流鏈接的時候可以盡量去采取這種格式。
timestamp delta:占用3個字節,注意這個和type=0時不同,存儲的是和上一個chunk的時間差。類似上面提到的timestamp,當它的值超過3個字節所能表示的最大值時,三個字節都置為1,實際的時間戳差值就會轉存到Extended Timestamp字段中,接受端在判斷timestamp delta字段24個位都為1時就會去Extended timestamp中解析時機的與上次時間戳的差值。
2.2.3、Type(fmt) = 2:占用3個字節
Type(fmg) = 2時,Message Header占用3個字節,相對于type=1格式又省去了表示消息長度的3個字節和表示消息類型的1個字節,表示此chunk和上一次發送的chunk所在的流、消息的長度和消息的類型都相同。余下的這三個字節表示timestamp delta,使用同type=1。
2.2.4、Type(fmt) = 3:占用0個字節
0字節!!!它表示這個chunk的Message Header和上一個是完全相同的,自然就不用再傳輸一遍了。當它跟在Type=0的chunk后面時,表示和前一個chunk的時間戳都是相同的。什么時候連時間戳都相同呢?就是一個Message拆分成了多個chunk,這個chunk和上一個chunk同屬于一個Message。而當它跟在Type=1或者Type=2的chunk后面時,表示和前一個chunk的時間戳的差是相同的。比如第一個chunk的Type=0,timestamp=100,第二個chunk的Type=2,timestamp delta=20,表示時間戳為
100+20=120,第三個chunk的Type=3,表示timestamp delta=20,時間戳為120+20=140。
以上幾種Type的Message Header對比:
2.3、Extended Timestamp(擴展時間戳)
在chunk中會有時間戳timestamp和時間戳差timestamp delta,并且它們不會同時存在,只有這兩者之一大于3個字節能表示的最大數值0xFFFFFF=16777215時,才會用這個字段來表示真正的時間戳,否則這個字段為0。擴展時間戳占4個字節,能表示的最大數值就是0xFFFFFFFF=4294967295。當擴展時間戳啟用時,timestamp字段或者timestamp delta要全置為0xFFFFFF,表示應該去擴展時間戳字段來提取真正的時間戳或者時間戳差。
2.4、Chunk Data(塊數據)
用戶層面上真正想要發送的與協議無關的數據,長度在(0,chunkSize]之間。
3、協議控制消息(Protocol Control Message)
在RTMP的chunk流會有一些特殊的值來代表協議的控制消息,它們的Message Stream ID必須為0(代表控制流信息),CSID必須為2,Message Type ID可以為1,2,3,5,6,具體代表的消息會在下面依次說明。控制消息的接受端會忽略掉chunk中的時間戳,收到后立即生效。
3.1、Set Chunk Size(ID=1)
設置chunk中Data字段所能承載的最大字節數,默認為128B,通信過程中可以通過發送該消息來設置chunk Size的大小(不得小于128B),而且通信雙方會各自維護一個chunkSize,兩端的chunkSize是獨立的。比如當A想向B發送一個200B的Message,但默認的chunkSize是128B,因此就要將該消息拆分為Data分別為128B和72B的兩個chunk發送,如果此時先發送一個設置chunkSize為256B的消息,再發送Data為200B的chunk,本地不再劃分
Message,B接受到Set Chunk Size的協議控制消息時會調整的接受chunk的Data的大小,也不用再將兩個chunk組成為一個Message。在實際寫代碼的時候一般會把chunk size設置的很大,有的會設置為4096,FFMPEG推流的時候設置的是 60*1000,這樣設置的好處是避免了頻繁的拆包組包,占
用過多的CPU。
以下為代表Set Chunk Size消息的chunk的Data:
其中第一位必須為0,chunk Size占31個位,最大可代表2147483647=0x7FFFFFFF=2 -1,但實際上所有大于16777215=0xFFFFFF的值都用不上,因為chunk size不能大于Message的長度,表示Message的長度字段是用3個字節表示的,最大只能為0xFFFFFF。
3.2、Abort Message (ID=2)
當一個Message被切分為多個chunk,接受端只接收到了部分chunk時,發送該控制消息表示發送端不再傳輸同Message的chunk,接受端接收到這個消息后要丟棄這些不完整的chunk。Data數據中只需要一個CSID,表示丟棄該CSID的所有已接收到的chunk。
3.3、Acknowledgement (ID=3)、Window Acknowledgement Size (ID=5)
Window Acknowledgement Size用于設置窗口確認大小,Acknowledgement是窗口確認消息。
會話開始時,雙方都要先對端發送Window Acknowledgement Size,用于指明期望獲得確認的大小。當一端收到內容大小超過Window Acknowledgement Size,就要像對方發送Acknowledgement。
1、會話開始計算收到byte個數的時間點是收到Window Acknowledgement Size消息開始。
2、byte size不包括tcp包頭,應該是chunk的大小,即從tcp 的recv函數中獲得的內容大小。
3、雙方都要向對方發送Window Acknowledgement Size和Acknowledgement。
4、發送端發送完Window Acknowledgement Size消息后,沒有收到Acknowledgement是不再發送進行步的消息的——這樣會容易引起錯誤,導致再也發送不出消息了。
Acknowledgement (ID=3):
客戶端或者服務器在接收到等同于窗口大小的字節之后必須要發送給對端一個確認。窗
口大小是指發送者在沒有收到接收者確認之前發送的最大數量的字節。這個消息定義了序列
號,也就是目前接收到的字節數。
Window Acknowledgement Size (ID=5):
‘客戶端或者服務器端發送這條消息來通知對端發送和應答之間的窗口大小。發送者在發
送完窗口大小字節之后期望對端的確認。接收端在上次確認發送后接收到的指示數值后,或
者會話建立之后尚未發送確認,必須發送一個確認。
對于拉流端,一般在收到av_createStream后,接著play,然后發送Acknowledgement 以讓服務器繼續發送數據。
3.4、Set Peer Bandwidth (ID=6)
限制對端的輸出帶寬。接受端接收到該消息后會通過設置消息中的Window ACK Size來限制已發送但未接受到反饋的消息的大小來限制發送端的發送帶寬。如果消息中的Window ACK Size與上一次發送給發送端的size不同的話要回饋一個Window Acknowledgement Size的控制消息。
1、Hard(Limit Type=0):接受端應該將Window Ack Size設置為消息中的值。
2、Soft(Limit Type=1):接受端可以講Window Ack Size設為消息中的值,也可以保存原來的值(前提是原來的Size小與該控制消息中的Window Ack Size)。
3、Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type為0,本次也按Hard處理,否則忽略本消息,不去設置Window Ack Size。
4、用戶控制消息(User Control Message Events ID=4)
(用戶控制消息,Message Type ID=4):告知對方執行該信息中包含的用戶控制事件,比如Stream Begin事件告知對方流信息開始傳輸。和前面提到的協議控制信息(Protocol Control Message)不同,這是在RTMP協議層的,而不是在RTMP chunk流協議層的,這個很容易弄混。該信息在chunk流中發送時,Message Stream ID=0,Chunk Stream
Id=2,Message Type Id=4。
支持以下用戶控制事件類型:
5、命令消息(Command Message Message ID=17或20)
(命令消息,Message Type ID=17或20):表示在客戶端盒服務器間傳遞的在對端執行某些操作的命令消息,如connect表示連接對端,對端如果同意連接的話會記錄發送端信息并返回連接成功消息,publish表示開始向對方推流,接受端接到命令后準備好接受對端發送的流信息,后面會對比較常見的Command Message具體介紹。當信息使用AMF0編碼時,Message Type ID=20,AMF3編碼時Message Type ID=17。
發送端發送時會帶有命令的名字,如connect,TransactionID表示此次命
令的標識,Command Object表示相關參數。接受端收到命令后,會返回以下三種消息中的一種:
_result消息表示接受該命令,對端可以繼續往下執行流程。
_error消息代表拒絕該命令要執行的操作。
method name消息代表要在之前命令的發送端執行的函數名稱。這三種回應的消息都要帶有收到的命令消息中的TransactionId來表示本次的回應作用于哪個命令。
可以認為發送命令消息的對象有兩種:
一種是NetConnection,表示雙端的上層連接。
一種是NetStream,表示流信息的傳輸通道,控制流信息的狀態,如Play播放流,Pause暫停。
5.1、NetConnection Commands(連接層的命令)
用來管理雙端之間的連接狀態,同時也提供了異步遠程方法調用(RPC)在對端執行某方法,以下是常見的連接層的命令:
5.1.1、connect:用于客戶端向服務器發送連接請求
握手之后先發送一個connect 命令消息,這些信息是以AMF格式發送的,消息的結構如下:
第三個字段中的Command Object中會涉及到很多鍵值對,使用時可以參考協議的官方文檔。
消息的回應有兩種,_result表示接受連接,_error表示連接失敗。
以下是連接命令對象中使用的名稱-值對的描述:
命令執行時消息流動如下:
1、 客戶端發送 connect 命令到服務器端以請求對服務器端應用實例的連接。
2、 收到 connect 命令后,服務器端發送協議消息 ‘窗口確認大小’ 到客戶端。服務器端也會連接到 connect 命令中提到的應用。
3、 服務器端發送協議消息 ‘設置對端帶寬’ 到客戶端。
4、在處理完協議消息 ‘設置對端帶寬’ 之后客戶端發送協議消息 '窗口確認大小’到服務器端。
5、 服務器端發送另一個用戶控制消息 (StreamBegin) 類型的協議消息到客戶端。
6、 服務器端發送結果命令消息告知客戶端連接狀態 (success/fail)。這一命令定義了事務ID(常常為 connect 命令設置為 1)。這一消息也定義了一些屬性,比如 FMS 服務器版本 (字符串)。之外,它還定義了?其他連接關聯到的信息,比如 level (字符串)、code (字符串)、 description (字符串)、objectencoding (數字) 等等。
5.1.2、Call:用于在對端執行某函數
即常說的RPC:遠程進程調用,消息的結構如下:
如果消息中的TransactionID不為0的話,對端需要對該命令做出響應,響應的消息結構如下:
5.1.3、Create Stream:創建傳遞具體信息的通道
從而可以在這個流中傳遞具體信息,傳輸信息單元為Chunk。
當發送完createStream消息之后,解析服務器返回的消息會得到一個stream ID, 這個ID也就是以后和服務器通信的 message stream ID, 一般返回的是1,不固定。
5.2、NetStream Commands(流連接上的命令)
Netstream建立在NetConnection之上,通過NetConnection的createStream命令創建,用于傳輸具體的音頻、視頻等信息。在傳輸層協議之上只能連接一個NetConnection,但一個NetConnection可以建立多個NetStream來建立不同的流通道傳輸數據。
以下會列出一些常用的NetStream Commands,服務端收到命令后會通過onStatus的命令來響應客戶端,表示當前NetStream的狀態。
onStatus命令的消息結構如下:
5.2.1、play(播放)
由客戶端向服務器發起請求從服務器端接受數據(如果傳輸的信息是視頻的話就是請求開始播流),可以多次調用,這樣本地就會形成一組數據流的接收者(創建一個播放列表)。注意其中有一個reset字段,表示是覆蓋之前的播流(設為true)還是重新開始一路播放(設為false)。
play命令的結構如下:
命令執行時的消息流動:
1、 當客戶端從服務器端接收到 createStream 命令的結果是為 success 時,發送play 命令。
2、 一旦接收到 play 命令,服務器端發送一個協議消息來設置塊大小。
3、 服務器端發送另一個協議消息( 用戶控制 ) , 這個消息中定義了
‘StreamIsRecorded’ 事件和流 ID。消息在前兩個字節中保存事件類型,在后四個字節中保存流 ID。
4、服務器端發送另一個協議消息 (用戶控制),這一消息包含 ‘StreamBegin’ 事件,來指示發送給客戶端的流的起點。
5、 如果客戶端發送的 play 命令成功,服務器端發送一個 onStatus 命令消息NetStream.Play.Start & NetStream.Play.Reset。只有當客戶端發送的 play 命令設置了 reset 時,服務器端才會發送 NetStream.Play.Reset。如果要播放的流沒有找到,服務器端發送 onStatus消息 NetStream.Play.StreamNotFound。
之后,服務器端發送視頻和音頻數據,客戶端對?其進行播放。
5.2.2、play2(播放)
和上面的play命令不同的是,play2命令可以在不改變播放內容時間軸的情況下切換到不同的比特率,服務器端會維護多種比特率的文件來供客戶端使用play2命令來切換。
5.2.3、deleteStream(刪除流)
用于客戶端告知服務器端本地的某個流對象已被刪除,不需要再傳輸此
路流。
5.2.4、 receiveAudio(接收音頻)
NetStream 通過發送 receiveAudio 消息來通知服務器端是否發送音頻到客戶端。
receiveAudio命令結構如下:
如果發送來的 receiveAudio 命令布爾字段被設為 false 時服務器端不發送任何回復。
如果這一標識被設為 true, 服務器端發送狀態消息NetStream.Seek.Notify 和
NetStream.Play.Start 進行回復。
5.2.5、 receiveVideo(接收視頻)
NetStream 通過發送receiveVideo 消息來通知服務器端是否發送視頻到客戶端。
receiveVideo命令結構如下:
如果發送來的 receiveVideo 命令布爾字段被設為 false 時服務器端不發送任何回復。
如果這一標識被設為 true, 服務器端發送狀態消息NetStream.Seek.Notify 和
NetStream.Play.Start 進行回復。
5.2.6、 publish(推送數據)
由客戶端向服務器發起請求推流到服務器。
publish命令結構如下:
5.2.7、 seek(定位流的位置)
定位到視頻或音頻的某個位置,以毫秒為單位。
seek命令的結構如下:
5.2.8、pause(暫停)
客戶端告知服務端停止或恢復播放。
pause命令的結構如下:
如果Pause為true即表示客戶端請求暫停的話,服務端暫停對應的流會返回NetStream.Pause.Notify的onStatus命令來告知客戶端當前流處于暫停的狀態,當Pause為false時,服務端會返回NetStream.Unpause.Notify的命令來告知客戶端當前流恢復。如果服務端對該命令響應失敗,返回_error信息。
6、數據消息(Data Message ID=15或18)(也是對應flv中的script data的tag type = 18 )
(數據消息,Message Type ID=15或18):傳遞一些元數據(MetaData,比如視頻名,分辨率等等)或者用戶自定義的一些消息。當信息使用AMF0編碼時,Message Type ID=18,AMF3編碼時Message Type ID=15。
7、共享消息(Shared Object Message ID=16或19)
(共享消息,Message Type ID=16或19):表示一個Flash類型的對象,由鍵值對的集合組成,用于多客戶端,多實例時使用。當信息使用AMF0編碼時,Message Type ID=19,AMF3編碼時Message Type ID=16。
每個消息可以包含有不同事件。
支持以下事件:
8、音頻消息(Audio Message ID=8)(也是對應flv中的audio data的tag type = 8 )
(音頻信息,Message Type ID=8):音頻數據。
9、視頻消息(Video Message ID=9)(也是對應flv中的video data的tag type = 9 )
(視頻信息,Message Type ID=9):視頻數據。
10、聚集消息(Aggregate Message ID=22)
(聚集信息,Message Type ID=22):多個RTMP子消息的集合。