librtmp協議分析---RTMP_SendPacket函數

接下來我們分析RTMP_SendPacket函數。我們先了解一下rtmp的消息格式chunk。

RTMP的head組成

RTMP的head在協議中的表現形式是chunk head,前面已經說到一個Message + head可以分成一個和多個chunk,爲了區分這些chunk,肯定是需要一個chunk head的,具體的實現就把Message  head的信息和chunk head的信息合併在一起以chunk head的形式表現。 
        一個完整的chunk的組成如下圖所示


Chunk basic header:
該字段包含chunk的stream ID和 type 。chunk的Type決定了消息頭的編碼方式。該字段的長度完全依賴於stream ID,該字段是一個可變長的字段。 



Chunk Msg Header:0, 3 ,7, 11
該字段包含了將要發送的消息的信息(或者是一部分,一個消息拆成多個chunk的情況下是一部分)該字段的長度由chunk basic header中的type決定。



Extend Timestamp: 0 ,4 bytes
該字段發送的時候必須是正常的時間戳設置成0xffffff時,當正常時間戳不爲0xffffff時,該字段不發送。當時間戳比0xffffff小該字段不發送,當時間戳比0xffffff大時該字段必須發送,且正常時間戳設置成0xffffff。


Chunk Data
實際數據(Payload),可以是信令,也可以是媒體數據。


總結如下圖所示:




6.1.2 塊消息頭
有四種格式的塊消息ID,供塊流基本頭中的fmt 字段選擇。一個實現應該使用最緊緻的方式來表示塊消息頭。

6.1.2.1 類型0
0 類型的塊長度爲11 字節。在一個塊流的開始和時間戳返回的時候必須有這種塊。


時間戳:3 字節
對於0 類型的塊。消息的絕對時間戳在這裏發送。如果時間戳大於或等於16777215(16 進制0x00ffffff),該值必須爲16777215,並且擴展時間戳必須出現。否則該值就是整個的時間戳。


6.1.2.2. 類型1
類型1 的塊佔7 個字節長。消息流ID 不包含在本塊中。塊的消息流ID 與先前的塊相同。具有可變大小消息的流,在第一個消息之後的每個消息的第一個塊應該使用這個格式。


6.1.2.3. 類型2
類型2 的塊佔3 個字節。既不包含流ID 也不包含消息長度。本塊使用的流ID 和消息長度與先前的塊相同。具有固定大小消息的流,在第一個消息之後的每個消息的第一個塊應該使用這個格式。


6.1.2.4 類型3
類型3 的塊沒有頭。流ID,消息長度,時間戳都不出現。這種類型的塊使用與先前塊相同的數據。當一個消息被分成多個塊,除了第一塊以外,所有的塊都應使用這種類型。示例可參考6.2.2 節中的例2 。由相同大小,流ID,和時間間隔的流在類型2 的塊之後應使用這種塊。示例可參考6.2.1 節中的例1 。如果第一個消息和第二個消息的時間增量與第一個消息的時間戳相同,那麼0類型的塊之後必須是3 類型的塊而,不需要類型2 的塊來註冊時間增量。如果類型3 的塊在類型0 的塊之後,那麼類型3 的時間戳增量與0 類型的塊的時間戳相同。

時間戳增量:3 字節
對於類型1 的塊和類型2 的塊,本字段表示先前塊的時間戳與當前塊的時間戳的差值。如果增量大於等於1677215(16 進制0x00ffffff),這個值必須是16777215 ,並且擴展時間戳必須出現。否則這個值就是整個的增量。


消息長度:3 字節
對於類型0 或類型1 的塊本字段表示消息的長度。注意,這個值通常與負載長度是不相同的。The chunk payload length is the maximum chunk size for all but the last chunk, and the remainder (which may be the entire length, for small messages) for the last chunk.


消息類型ID:1 字節
對於0 類型和1 類型的塊,本字段發送消息類型。


消息流ID:4 字節
對於0 類型的塊,本字段存儲消息流ID。通常,在一個塊流中的消息來自於同一個消息流。雖然,由於不同的消息可能複用到一個塊流中而使頭壓縮無法有效實施。但是,如果一個消息流關閉而另一個消息流纔打開,那麼通過發送一個新的0 類型的塊重複使用一個存在的塊流也不是不可以。


6.1.3. 擴展時間戳
只有當塊消息頭中的普通時間戳設置爲0x00ffffff 時,本字段才被傳送。如果普通時間戳的值小於0x00ffffff,那麼本字段一定不能出現。如果時間戳字段不出現本字段也一定不能出現。類型3 的塊一定不能含有本字段。本字段在塊消息頭之後,塊時間之前。


代碼分析如下:

int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
{
	const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
	uint32_t last = 0;
	int nSize;
	int hSize, cSize;
	char *header, *hptr, *hend, hbuf[RTMP_MAX_HEADER_SIZE], c;
	uint32_t t;
	char *buffer, *tbuf = NULL, *toff = NULL;
	int nChunkSize;
	int tlen;

	// 前一個packet存在且不是完整的ChunkMsgHeader,因此有可能需要調整塊消息頭的類型
	//fmt字節
	/*case 0:chunk msg header 長度爲11
	 * case 1:chunk msg header 長度爲7
	 * case 2:chunk msg header 長度爲3
	 * case 3:chunk msg header 長度爲0
	*/
	if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
	{
		/* compress a bit by using the prev packet's attributes */
		// 獲取ChunkMsgHeader類型,前一個Chunk與當前Chunk比較
		// 如果前後兩個塊的大小、包類型及塊頭類型都相同,則將塊頭類型fmt設爲2,
		// 即可省略消息長度、消息類型id、消息流id
		// 可以參考官方協議:流的分塊 --- 6.1.2.3節
		if (prevPacket->m_nBodySize == packet->m_nBodySize&& prevPacket->m_packetType == packet->m_packetType
		&& packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)
			packet->m_headerType = RTMP_PACKET_SIZE_SMALL;

		// 前後兩個塊的時間戳相同,且塊頭類型fmt爲2,則相應的時間戳也可省略,因此將塊頭類型置爲3
		// 可以參考官方協議:流的分塊 --- 6.1.2.4節
		if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp && packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
			packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;
		last = prevPacket->m_nTimeStamp;// 前一個包的時間戳
	}

	// 塊頭類型fmt取值0、1、2、3,超過3就表示出錯(fmt佔二個字節)
	if (packet->m_headerType > 3) /* sanity */
	{
		RTMP_Log(RTMP_LOGERROR, "sanity failed!! trying to send header of type: 0x%02x.", (unsigned char)packet->m_headerType);
		return FALSE;
	}

	// 塊頭初始大小 = 基本頭(1字節) + 塊消息頭大小(11/7/3/0) = [12, 8, 4, 1]
	// 塊基本頭是1-3字節,因此用變量cSize來表示剩下的0-2字節
	// nSize 表示塊頭初始大小, hSize表示塊頭大小
	nSize = packetSize[packet->m_headerType];
	hSize = nSize;
	cSize = 0;
	// 時間戳增量
	t = packet->m_nTimeStamp - last;

	if (packet->m_body)
	{
		// m_body是指向負載數據首地址的指針;“-”號用於指針前移
		header = packet->m_body - nSize;
		// 塊頭的首指針
		hend = packet->m_body;
		// 塊頭的尾指針
	}
	else
	{
		header = hbuf + 6;
		hend = hbuf + sizeof(hbuf);
	}

	if (packet->m_nChannel > 319)// 塊流id(cs id)大於319,則塊基本頭佔3個字節
		cSize = 2;
	else if (packet->m_nChannel > 63)// 塊流id(cs id)在64與319之間,則塊基本頭佔2個字節
		cSize = 1;
	// ChunkBasicHeader的長度比初始長度還要長
	if (cSize)
	{
		header -= cSize;// header指向塊頭
		hSize += cSize;// hSize加上ChunkBasicHeader的長度(比初始長度多出來的長度)
	}

	// nSize > 1表示塊消息頭至少有3個字節,即存在timestamp字段
	// 相對TimeStamp大於0xffffff,此時需要使用ExtendTimeStamp
	if (nSize > 1 && t >= 0xffffff)
	{
		header -= 4;
		hSize += 4;
	}

	hptr = header;
	c = packet->m_headerType << 6;// 把ChunkBasicHeader的Fmt類型左移6位

	// 設置basic header的第一個字節值,前兩位爲fmt. 可以參考官方協議:流的分塊 --- 6.1.1節
	switch (cSize)
	{
	case 0:// 把ChunkBasicHeader的低6位設置成ChunkStreamID( cs id )
		c |= packet->m_nChannel;
		break;
	case 1:// 同理,但低6位設置成000000
		break;
	case 2:// 同理,但低6位設置成000001
		c |= 1;
		break;
	}
	*hptr++ = c;// 可以拆分成兩句*hptr=c; hptr++,此時hptr指向第2個字節

	// 設置basic header的第二(三)個字節值
	if (cSize)
	{
		int tmp = packet->m_nChannel - 64;// 將要放到第2字節的內容tmp
		*hptr++ = tmp & 0xff;// 獲取低位存儲與第2字節
		if (cSize == 2)// ChunkBasicHeader是最大的3字節時,獲取高位存儲於最後1個字節(注意:排序使用大端序列,和主機相反)
			*hptr++ = tmp >> 8;
	}

	if (nSize > 1)// ChunkMsgHeader長度爲11、7、3, 都含有timestamp(3字節)
	{
		// 將時間戳(相對或絕對)轉化爲3個字節存入hptr,如果時間戳超過0xffffff,則後面還要填入Extend Timestamp
		hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);
	}

	if (nSize > 4)// ChunkMsgHeader長度爲11、7,都含有 msg length + msg type id
	{
		// 將消息長度(msg length)轉化爲3個字節存入hptr
		hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
		*hptr++ = packet->m_packetType;
	}
	// ChunkMsgHeader長度爲11, 含有msg stream id( 小端)
	if (nSize > 8)
		hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);

	if (nSize > 1 && t >= 0xffffff)// 如果時間戳大於0xffffff,則需要寫入Extend Timestamp
		hptr = AMF_EncodeInt32(hptr, hend, t);

	// 到此爲止,已經將塊頭填寫好了
	// 此時nSize表示負載數據的長度, buffer是指向負載數據區的指針
	nSize = packet->m_nBodySize;
	buffer = packet->m_body;
	nChunkSize = r->m_outChunkSize;//Chunk大小,默認是128字節

	RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket, nSize);
	/* send all chunks in one HTTP request ,使用HTTP協議 */
	if (r->Link.protocol & RTMP_FEATURE_HTTP)
	{
		// nSize:Message負載長度;nChunkSize:Chunk長度;
		// 例nSize:307,nChunkSize:128;
		// 可分爲(307 + 128 - 1)/128 = 3個
		// 爲什麼加 nChunkSize - 1?因爲除法會只取整數部分!
		int chunks = (nSize + nChunkSize - 1) / nChunkSize;
		if (chunks > 1)// Chunk個數超過一個
		{
			// 注意:ChunkBasicHeader的長度 = cSize + 1
			// 消息分n塊後總的開銷:
			// n個ChunkBasicHeader,1個ChunkMsgHeader,1個Message負載
			// 實際上只有第一個Chunk是完整的,剩下的只有ChunkBasicHeader
			tlen = chunks * (cSize + 1) + nSize + hSize;
			tbuf = malloc(tlen);
			if (!tbuf)
				return FALSE;
			toff = tbuf;
		}
	}

	// 消息的負載 + 頭
	while (nSize + hSize)
	{
		int wrote;

		if (nSize < nChunkSize)// 消息負載大小 < Chunk大小(不用分塊)
			nChunkSize = nSize;// Chunk可能小於設定值

		RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)header, hSize);
		RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)buffer, nChunkSize);
		// 如果r->Link.protocol採用Http協議,則將RTMP包數據封裝成多個Chunk,然後一次性發送。
		// 否則每封裝成一個塊,就立即發送出去
		if (tbuf)
		{
			// 將從Chunk頭開始的nChunkSize + hSize個字節拷貝至toff中,
			// 這些拷貝的數據包括塊頭數據(hSize字節)和nChunkSize個負載數據
			memcpy(toff, header, nChunkSize + hSize);
			toff += nChunkSize + hSize;
		}
		else// 負載數據長度不超過設定的塊大小,不需要分塊,因此tbuf爲NULL;或者r->Link.protocol不採用Http
		{
			// 直接將負載數據和塊頭數據發送出去
			wrote = WriteN(r, header, nChunkSize + hSize);
			if (!wrote)
				return FALSE;
		}
		nSize -= nChunkSize;// 消息負載長度 - Chunk負載長度
		buffer += nChunkSize;// buffer指針前移1個Chunk負載長度
		hSize = 0;// 重置塊頭大小爲0,後續的塊只需要有基本頭(或加上擴展時間戳)即可

		// 如果消息負載數據還沒有發完,準備填充下一個塊的塊頭數據
		if (nSize > 0)
		{
			header = buffer - 1;
			hSize = 1;
			if (cSize)
			{
				header -= cSize;
				hSize += cSize;
			}
			*header = (0xc0 | c);
			if (cSize)
			{
				int tmp = packet->m_nChannel - 64;
				header[1] = tmp & 0xff;
				if (cSize == 2)
					header[2] = tmp >> 8;
			}
		}
	}
	if (tbuf)
	{
		int wrote = WriteN(r, tbuf, toff - tbuf);
		free(tbuf);
		tbuf = NULL;
		if (!wrote)
			return FALSE;
	}

	/* we invoked a remote method */
	if (packet->m_packetType == 0x14)
	{
		AVal method;
		char *ptr;
		ptr = packet->m_body + 1;
		AMF_DecodeString(ptr, &method);
		RTMP_Log(RTMP_LOGDEBUG, "Invoking %s", method.av_val);
		/* keep it in call queue till result arrives */
		if (queue)
		{
			int txn;
			ptr += 3 + method.av_len;
			txn = (int)AMF_DecodeNumber(ptr);
			AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
		}
	}

	if (!r->m_vecChannelsOut[packet->m_nChannel])
	r->m_vecChannelsOut[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
	memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));
	return TRUE;
}





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章