rtmp協議學習筆記
來自專欄 編程那些事兒
上一篇文章大概梳理了一下流媒體的基本傳輸過程,現階段學習的重點是rtmp、H.264、AAC等協議的原理。通讀了官方文檔後,這篇文章先寫寫對rtmp的理解。
協議分析
首先rtmp位於TCP/IP協議族中的應用層,與HTTP協議並列於第3層,默認使用1935埠。如下圖所示。
由此可見,RTMP和HTTP一樣,本身是不負責安全性的,而依靠下層TCP協議來保證數據安全有序到達。但RTMP協議在播放時會發送帶命令的消息給伺服器,從而控制流的播放、暫停、點播等。
rtmpdump是一個包含了librtmp庫和兩個伺服器程序示例的開源庫。可以用來很好地學習和進行基於rtmp的開發。其中rtmpsrc.c文件中main函數代碼如下:
intmain(int argc, char **argv){ int nStatus = RD_SUCCESS; int i; // http streaming server char DEFAULT_HTTP_STREAMING_DEVICE[] = "0.0.0.0"; // 0.0.0.0 is any device char *rtmpStreamingDevice = DEFAULT_HTTP_STREAMING_DEVICE; // streaming device, default 0.0.0.0 int nRtmpStreamingPort = 1935; // port char *cert = NULL, *key = NULL; RTMP_LogPrintf("RTMP Server %s
", RTMPDUMP_VERSION); RTMP_LogPrintf("(c) 2010 Andrej Stepanchuk, Howard Chu; license: GPL
"); RTMP_debuglevel = RTMP_LOGINFO; for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "-z")) RTMP_debuglevel = RTMP_LOGALL; else if (!strcmp(argv[i], "-c") && i + 1 < argc) cert = argv[++i]; else if (!strcmp(argv[i], "-k") && i + 1 < argc) key = argv[++i]; } if (cert && key) sslCtx = RTMP_TLS_AllocServerContext(cert, key); // init request memset(&defaultRTMPRequest, 0, sizeof(RTMP_REQUEST)); defaultRTMPRequest.rtmpport = -1; defaultRTMPRequest.protocol = RTMP_PROTOCOL_UNDEFINED; defaultRTMPRequest.bLiveStream = FALSE; // is it a live stream? then we cant seek/resume defaultRTMPRequest.timeout = 300; // timeout connection afte 300 seconds defaultRTMPRequest.bufferTime = 20 * 1000; signal(SIGINT, sigIntHandler);#ifndef WIN32 signal(SIGPIPE, SIG_IGN);#endif#ifdef _DEBUG netstackdump = fopen("netstackdump", "wb"); netstackdump_read = fopen("netstackdump_read", "wb");#endif InitSockets(); // start text UI ThreadCreate(controlServerThread, 0); // start http streaming if ((rtmpServer = startStreaming(rtmpStreamingDevice, nRtmpStreamingPort)) == 0) { RTMP_Log(RTMP_LOGERROR, "Failed to start RTMP server, exiting!"); return RD_FAILED; } RTMP_LogPrintf("Streaming on rtmp://%s:%d
", rtmpStreamingDevice, nRtmpStreamingPort); while (rtmpServer->state != STREAMING_STOPPED) { sleep(1); } RTMP_Log(RTMP_LOGDEBUG, "Done, exiting..."); if (sslCtx) RTMP_TLS_FreeServerContext(sslCtx); CleanupSockets();#ifdef _DEBUG if (netstackdump != 0) fclose(netstackdump); if (netstackdump_read != 0) fclose(netstackdump_read);#endif return nStatus;}
可以看出,播放一個RTMP協議的流媒體需要經過以下幾個過程:握手、建立連接、建立流、播放、斷開流、斷開連接。需要調用的函數為:
- InitSockets()
- StartStreaming()
- RTMP_TLS_FreeServerContext()
- CleanupSockets()
握手
要建立一個有效的RTMP connection連接,首先需要進行握手。客戶端和伺服器分別向對端發送3個固定長度的數據塊(chunk),稱為C0~2,S0~2。協議本身並沒有規定這6個數據塊要如何進行傳輸,但必須保證以下幾點:
- 握手以客戶端發送C0和C1開始;
- 伺服器必須收到C0才能發送S0和S1,必須收到C1才能發送S2;
- 客戶端必須收到S1才能發送C2;
- 客戶端和伺服器必須分別收到S2和C2後,才能發送其他數據,此時握手完成。
握手數據包的格式
C0和S0的數據包都是一個位元組長度,代表當前使用的協議版本號。
在C0中,version表示客戶端自己所使用的協議版本號。而在S0中表示伺服器所選擇的版本號,可選值為0,1,2,3.現階段應該使用3,因為前面幾個已經被不再使用。4-31保留給將來使用,32-255不可用。
C1和S1的長度都是1536位元組,包含以下欄位:
其中timestamp是一個時間戳,表明發送當前報文時的時間,用來同步各個報文。佔4位元組。
zero欄位全部用0填充,佔4位元組。
因為RTMP命令既可以有客戶端發送給伺服器,也可以從伺服器發到客戶端。所以random bytes包含一個隨機數,表明該報文是在哪個方向上傳輸的。
C2和S2的報文格式和C1/S1很相似,不同之處是沒有zero欄位,而改成了time2欄位,包含自己上次發送的相應欄位數據。其中timestamp欄位和random欄位必須和自己上次收到報文中的相應欄位一致,以唯一確定當前報文是哪個方向上的哪一個包,便於對端組裝。
可以看出,重要信息如version、timestamp和random都在前兩次握手中完成傳輸,C2和S2隻是對這些信息的在此確認,所以只要可以發送數據,表明握手可靠完成。
消息格式
「消息」是RTMP協議中基本的數據單元,用來在一個單向的邏輯通道上傳輸數據。消息頭的格式如下:
頭部包含以下信息:
- Message Type:消息類型,佔1個位元組,1-7用於協議控制,8、9分別用於傳輸音頻和視頻數據,15-20用於發送AMF編碼的指令。
- Payload Length:負載長度,佔3個位元組。採用大端位元組序。
- Timestamp:時間戳,佔4個位元組。採用大端位元組序。
- Message Stream ID:消息流ID,表示消息所使用的流。採用大端位元組序。
塊流格式
當消息在網路上傳輸時,需要進行分塊,形成塊流。原理就是將消息中的負載部分按照一定的規則分成多個部分,每個部分加上各自的塊頭。每個塊的格式如下:
可見,塊頭部包含以下欄位:
- Basic Header:表示塊流的ID和類型,長度為1-3位元組,取決於塊流ID。
- Message Header:表示所發送消息的描述信息,長度為0,3,7或11位元組,取決於Basic Header中指定的塊類型。
- Extend TimeStamp:表示擴展時間戳,長度為0或4位元組,取決於Message Header中的時間戳。
Basic Header
每個分塊都有自己所屬的塊流ID,取值區間為3~65599之間,共65597個值。當ID在3~63之間時,Basic Header佔一個位元組,第1,2兩個比特位表示塊類型,之後6個比特位表示ID,如下:
當ID在64~319之間時,Basic Header佔兩個位元組,此時第1,2兩個比特仍然表示類型,第一個位元組的其餘6個比特為0,這也是塊流ID不取0值得原因。第二個位元組表示真正的ID,即ID=第二個位元組值+64.如下:
當ID在64~65599之間時,Basic Header佔三個位元組,此時第1,2兩個比特仍然表示塊類型,第一個位元組其餘比特數值為1,這也是塊流ID不取1值得原因。ID=第二個位元組值*255+第三個位元組值+64.如下:
塊流ID值2是為低版本協議保留的,用於協議控制消息和命令。ID值在64~319之間時,既可以用兩個位元組來編碼,也可以用3個位元組編碼。
Message Header
Message Header根據Basic Header中fmt值的不同而有不同長度。具體如下:若fmt==0,Message Header為11位元組,該類型必須用在一個塊流的開頭,和每當塊流時間戳後退的時候,因為四種Message Header中只有它精確指定了時間戳。此時Message Header格式如下:
- timestamp:絕對時間戳,如果其大於0xFFFFFF,則必須改為0xFFFFFF,並啟用擴展時間戳。
- message length:消息長度。
- message type id:消息類型id,表示消息的類型。
- message stream id:消息流id,表示該消息所屬的流。
fmt>0,Message Header中的第一個欄位應為timestamp delta欄位,而不是絕對timestamp,表示相對於上一個時間戳的增量。
若fmt==1,Message Header為7位元組長度,表示沿用上一個分塊的消息流id,因而就可以不用發送這個欄位。對於同一個流且大小可變的塊,應該從第二個塊開始使用這個格式。
若fmt==2,Message Header為3位元組長度,表示沿用上一個分塊的消息流id和消息長度,因而不用重複發送這兩個欄位。對於傳輸固定大小的塊流,應該從第二個塊開始使用這個格式。
若fmt==3,Message Header為0位元組,即沒有這個欄位,表示消息流id、消息長度和時間戳增量都沿用上一個。
歡迎關注我的公眾號↓
推薦閱讀: