RTSP 是 Internet 協議規範,是 TCP/IP 協議體系中的一個應用層協議級網路通訊系統。專為娛樂(如音訊和視訊)和通訊系統的使用,以控制流媒體伺服器。該協議用於在端點之間建立和控制媒體會話。媒體伺服器的客戶端發出 VHS 樣式的命令,例如:PLAY、PAUSE、SETUP、DESCRIBE、RECORD 等等。以促進對從伺服器到客戶端或從客戶端到伺服器的媒體流進行實時控制。
RTSP 傳輸過程
- 當使用者或應用程式嘗試從遠端源流式傳輸視訊時,客戶端裝置會向伺服器傳送 RTSP 請求,以確定可用選項,例如 PLAY,PAUSE、SETUP。。。
- 然後,伺服器返回它可以通過 RTSP 接受的請求型別的列表。
- 客戶端知道如何發出請求後,便將媒體描述請求傳送到流伺服器。
- 伺服器以媒體描述作為響應。
- 客戶端從那裡傳送設定請求,伺服器以有關傳輸機制的資訊作為響應。
- 設定過程完成後,客戶端將通過告訴伺服器使用設定請求中指定的傳輸機制傳送位流(二進位制序列)來啟動流傳輸過程。
客戶端 ->伺服器:DESCRIBE
伺服器 ->客戶端: 200 OK (SDP)
客戶端 ->伺服器:SETUP
伺服器 ->客戶端: 200 OK
客戶端 ->伺服器:PAUSE
...
協議的分析和學習少不了抓包,截圖個 RTSP 協議抓包的圖:
為什麼 RTS 協議那麼重要
- RTSP 最初是一種允許使用者直接從 Internet 播放音訊和視訊,而不必將媒體檔案下載到其裝置的方法。該協議已被應用於多種用途,包括網際網路攝像機站點,線上教育和網際網路廣播。
- RTSP 使用與基本 HTTP 相同的概念,在很大程度上是為了相容現有的 Web 基礎結構。正因如此,HTTP 的擴充套件機制大都可以直接引入到 RTSP 中。
- RTSP 協議還具有很大的靈活性。客戶端可以請求他們要使用的功能,以找出媒體伺服器是否支援它們。同樣,擁有媒體的任何人都可以從多個伺服器傳遞媒體流。該協議還旨在適應媒體的未來發展,以便媒體建立者可以在必要時修改協議。
RTSP 協議指令
儘管 RTSP 在某些方面類似於 HTTP,但它定義了可用於控制多媒體播放的控制序列。儘管 HTTP 是無狀態的,但 RTSP 卻具有狀態。
在需要跟蹤併發會話時使用識別符號。像 HTTP 一樣,RTSP 使用 TCP 來維護端到端連線,埠號為 554。
儘管大多數 RTSP 控制訊息是由客戶端傳送到伺服器的,但是某些命令卻是朝著另一個方向(即從伺服器到客戶端)傳遞的。
下面我們來介紹基本的 RTSP 請求:
SETUP
SETUP 請求指定必須如何傳輸單個媒體流。必須在傳送 PLAY 請求之前完成此操作。
該請求包含媒體流 URL 和傳輸說明符。
該說明符通常包括一個本地埠,用於接收 RTP 資料(音訊或視訊),另一個用於 RTCP 資料(元資訊)。
伺服器答覆通常會確認選定的引數,並填寫缺少的部分,例如伺服器的選定埠。必須先使用 SETUP 配置每個媒體流,然後才能傳送聚合播放請求。
PLAY
PLAY 請求將導致播放一個或所有媒體流。可以通過傳送多個 PLAY 請求來堆疊播放請求。該 URL 可以是聚合 URL(以播放所有媒體流),也可以是單個媒體流 URL(僅播放該流)。
可以指定範圍。如果未指定範圍,則從頭開始播放並播放到結尾,或者,如果流已暫停,則在暫停點恢復播放。
PAUSE
PAUSE 請求會暫時中止一個或所有媒體流,因此稍後可以通過 PLAY 請求將其恢復。該請求包含聚合或媒體流 URL。
PAUSE 請求上的 range 引數指定何時暫停。如果省略 range 引數,則暫停將立即無限期地發生。
RECORD
此方法根據演示說明開始記錄一系列媒體資料。時間戳反映開始時間和結束時間(UTC)。如果沒有給出時間範圍,請使用演示說明中提供的開始時間或結束時間。
如果會話已經開始,請立即開始錄製。伺服器決定是否將記錄的資料儲存在請求 URl 或其他 URI 下。
如果伺服器未使用請求 URI,則響應應為 201,幷包含描述請求狀態並引用新資源的實體和位置標頭。
ANNOUNCE
當從客戶端傳送到伺服器時,ANNOUNCE 將請求 URL 標識的演示或媒體物件的描述釋出到伺服器。ANNOUNCE 會實時更新會話描述。
如果將新的媒體流新增到簡報中(例如,在現場簡報中),則應再次傳送整個簡報說明,而不僅僅是其他元件,以便可以刪除這些元件。
TEARDOWN
TEARDOWN 請求用於終止會話。它停止所有媒體流並釋放伺服器上所有與會話相關的資料。
GET_PARAMETER
GET_PARAMETER 請求檢索 URI 中指定的表示形式或流的引數值。答覆和響應的內容留給實現。
SET_PARAMETER
此方法要求為 URI 指定的表示或流設定引數值。
Wireshark RTSP 協議解析實現
對 RTSP 協議的使用有了一個大概的瞭解之後,我們來解析實現一下 RTSP 協議。
#include <sys/stat.h>
#include <sys/types.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <net/ethernet.h>
#include <pcap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/*RTSP 埠*/
#define RTSP_TCP_PORT_RANGE 554
typedef enum {
RTSP_REQUEST,
RTSP_REPLY,
RTSP_NOT_FIRST_LINE
} rtsp_type_t;
static const char *rtsp_methods[] = {
"DESCRIBE",
"ANNOUNCE",
"GET_PARAMETER",
"OPTIONS",
"PAUSE",
"PLAY",
"RECORD",
"REDIRECT",
"SETUP",
"SET_PARAMETER",
"TEARDOWN"
};
/* 用於RTSP統計 */
struct rtsp_info_value_t {
char *request_method;
unsigned long int response_code;
};
/*
假定一個位元組陣列(假定包含一個以空值結尾的字串)作為引數,
並返回字串的長度-即該陣列的大小,對於空終止符的值減去1。
*/
#define STRLEN_CONST(str) (sizeof (str) - 1)
static const char rtsp_content_type[] = "Content-Type:";
static const char rtsp_transport[] = "Transport:";
static const char rtsp_sps_server_port[] = "server_port=";
static const char rtsp_cps_server_port[] = "client_port=";
static const char rtsp_sps_dest_addr[] = "dest_addr=";
static const char rtsp_cps_src_addr[] = "src_addr=";
static const char rtsp_rtp_udp_default[] = "rtp/avp";
static const char rtsp_rtp_udp[] = "rtp/avp/udp";
static const char rtsp_rtp_tcp[] = "rtp/avp/tcp";
static const char rtsp_rdt_feature_level[] = "RDTFeatureLevel";
static const char rtsp_real_rdt[] = "x-real-rdt/";
static const char rtsp_real_tng[] = "x-pn-tng/"; /* synonym for x-real-rdt */
static const char rtsp_inter[] = "interleaved=";
static const char rtsp_content_length[] = "Content-Length:";
static void rtsp_create_conversation(u_char *line_begin, size_t line_len,rtsp_type_t rtsp_type_packet)
{
char buf[256];
char *tmp;
bool rtp_udp_transport = false;
bool rtp_tcp_transport = false;
bool rdt_transport = false;
//bool is_video = false; /* 是否需要顯示視訊 */
unsigned int c_data_port, c_mon_port;
unsigned int s_data_port, s_mon_port;
unsigned int ipv4_1, ipv4_2, ipv4_3, ipv4_4;
if (rtsp_type_packet != RTSP_REPLY) {
return;
}
/* 將行復制到buf */
if (line_len > sizeof(buf) - 1)
{
/* 避免溢位緩衝區。 */
line_len = sizeof(buf) - 1;
}
memcpy(buf, line_begin, line_len);
buf[line_len] = '\0';
printf("%s\n",buf);
/* Get past "Transport:" and spaces */
tmp = buf + STRLEN_CONST(rtsp_transport);
//printf("tmp %s\n",tmp);
while (*tmp && isspace(*tmp))
tmp++;
if ((tmp = strstr(buf, rtsp_cps_src_addr)))
{
tmp += strlen(rtsp_cps_src_addr);
//printf("tmp ====== %s\n",tmp);
if (sscanf(tmp, "\"%u.%u.%u.%u:%u\"", &ipv4_1, &ipv4_2, &ipv4_3, &ipv4_4, &c_data_port) == 5)
{
char *tmp2;
char *tmp3;
//printf("ipv4_1 %d\n",ipv4_1);
//printf("ipv4_2 %d\n",ipv4_2);
//printf("ipv4_3 %d\n",ipv4_3);
//printf("ipv4_4 %d\n",ipv4_4);
printf("c_data_port %d\n",c_data_port);
//Skip leading
tmp++;
tmp2=strstr(tmp,":");
tmp3=strndup(tmp,tmp2-tmp);
printf("src_addr %s\n",tmp3);
free(tmp3);
}
}
if ((tmp = strstr(buf, rtsp_sps_dest_addr)))
{
tmp += strlen(rtsp_sps_dest_addr);
if (sscanf(tmp, "\":%u\"", &s_data_port) == 1)
{
/* :9 mean ignore */
if (s_data_port == 9) {
s_data_port = 0;
}
printf("s_data_port %d\n",s_data_port);
}
}
if ((tmp = strstr(buf, rtsp_sps_server_port))) {
tmp += strlen(rtsp_sps_server_port);
if (sscanf(tmp, "%u", &s_mon_port) == 1) {
printf("s_mon_port %d\n",s_mon_port);
}
}
}
static bool is_rtsp_request_or_reply( unsigned char *line, int offset, rtsp_type_t *type)
{
unsigned int ii = 0;
char *data = reinterpret_cast<char *>(line);
int tokenlen;
char response_chars[4];
struct rtsp_info_value_t rtsp_info;
char *token, *next_token;
/*這是RTSP的回覆 ? */
if ( strncasecmp("RTSP/", data, 5) == 0) {
/*
* Yes.
*/
*type = RTSP_REPLY;
/* 第一個標記是版本。 */
offset += 9;
memcpy(response_chars, data + offset, 3);
response_chars[3] = '\0';
rtsp_info.response_code = strtoul(response_chars, NULL, 10);
//printf("rtsp_info.response_code %d\n",rtsp_info.response_code);
return true;
}
/*
這是RTSP請求嗎?
檢查該行是否以RTSP請求方法之一開頭。
*/
for (ii = 0; ii < sizeof rtsp_methods / sizeof rtsp_methods[0]; ii++) {
size_t len = strlen(rtsp_methods[ii]);
if (strncasecmp(rtsp_methods[ii], data, len) == 0 &&(isspace(data[len])))
{
*type = RTSP_REQUEST;
rtsp_info.request_method = strndupa(rtsp_methods[ii], len+1);
//printf("request_method: %s\n",rtsp_info.request_method);
return true;
}
}
/* 既不是請求也不是回應 */
*type = RTSP_NOT_FIRST_LINE;
return false;
}
/* 閱讀回覆訊息的第一行 */
static void process_rtsp_reply(u_char *rtsp_data, int offset,rtsp_type_t rtsp_type_packet)
{
char *lineend = reinterpret_cast<char *>(rtsp_data + offset);
char *status = reinterpret_cast<char *>(rtsp_data );
char *status_start;
unsigned int status_i;
/* status code */
/* Skip protocol/version */
while (status < lineend && !isspace(*status))
status++;
/* Skip spaces */
while (status < lineend && isspace(*status))
status++;
/* Actual code number now */
status_start = status;
//printf("status_start %s\n",status_start);
status_i = 0;
while (status < lineend && isdigit(*status))
status_i = status_i * 10 + *status++ - '0';
//printf("status_i %d\n",status_i);
offset += strlen(lineend);
rtsp_create_conversation(rtsp_data,offset,rtsp_type_packet);
}
static void process_rtsp_request(u_char *rtsp_data, int offset,rtsp_type_t rtsp_type_packet)
{
char *lineend = reinterpret_cast<char *>(rtsp_data + offset);
// u_char *lineend = rtsp_data + offset;
unsigned int ii = 0;
char *url;
char *url_start;
char buf[256];
char *tmp;
int content_length = 0;
char content_type[256];
/* Request Methods */
for (ii = 0; ii < sizeof rtsp_methods / sizeof rtsp_methods[0]; ii++) {
size_t len = strlen(rtsp_methods[ii]);
if (strncasecmp(rtsp_methods[ii], lineend, len) == 0 &&(isspace(lineend[len])))
break;
}
//printf("process_rtsp_request 0x%.2X,0x%.2X,0x%.2X,0x%.2X\n",lineend[0],lineend[1],lineend[2],lineend[3]);
/* URL */
url = lineend;
/* Skip method name again */
while (url < lineend && !isspace(*url))
url++;
/* Skip spaces */
while (url < lineend && isspace(*url))
url++;
/* URL starts here */
url_start = url;
/* Scan to end of URL */
while (url < lineend && !isspace(*url))
url++;
printf("%s\n",url_start);
printf("111url %s\n",url);
if ((tmp = strstr(url_start, rtsp_content_type)))
{
tmp += strlen(rtsp_content_type);
if (sscanf(tmp, "%s", content_type) == 1)
{
//printf("content_type %s\n",content_type);
}
}
//Content-Length
if ((tmp = strstr(url_start, rtsp_content_length)))
{
tmp += strlen(rtsp_content_length);
if (sscanf(tmp, "%u", &content_length) == 1)
{
//printf("content_length %d\n",content_length);
}
}
}
void dissect_rtsp(u_char *rtsp_data)
{
int offset = 0;
rtsp_type_t rtsp_type_packet;
bool is_request_or_reply;
u_char *linep, *lineend;
u_char c;
//bool is_header = false;
is_request_or_reply = is_rtsp_request_or_reply(rtsp_data, offset, &rtsp_type_packet);
if (is_request_or_reply)
goto is_rtsp;
is_rtsp:
switch(rtsp_type_packet)
{
case RTSP_REQUEST:
process_rtsp_request(rtsp_data, offset,rtsp_type_packet);
break;
case RTSP_REPLY:
process_rtsp_reply(rtsp_data, offset,rtsp_type_packet);
break;
case RTSP_NOT_FIRST_LINE:
/* Drop through, it may well be a header line */
break;
default:
break;
}
}
static void dissect_rtsp_tcp(struct ip *pIp)
{
int iHeadLen = pIp->ip_hl*4;
int iPacketLen = ntohs(pIp->ip_len) - iHeadLen;
int offset = 0;
int nFragSeq = 0;
struct tcphdr *pTcpHdr = (struct tcphdr *)(((char *)pIp) + iHeadLen);
if (pIp->ip_p == IPPROTO_TCP && (ntohs(pTcpHdr->dest) == RTSP_TCP_PORT_RANGE)
|| (ntohs(pTcpHdr->source) == RTSP_TCP_PORT_RANGE) )/*僅處理TCP協議*/
{
int iPayloadLen = iPacketLen - pTcpHdr->doff*4;
//printf("TCP Payload Len %d\n",iPayloadLen);
u_char *RtspHdr = (u_char*)(pTcpHdr+1);
if (RtspHdr == NULL)
return;
u_char *RtspData = RtspHdr + 12; /*skip OPtions */
//printf("NtpHdr 0x%.2X,0x%.2X,0x%.2X,0x%.2X\n",RtspData[0],RtspData[1],RtspData[2],RtspData[3]);
dissect_rtsp(RtspData);
}
}
編譯執行
RTSP 是一種基於文字的協議,用回車換行(\r\n)作為每一行的結束符,其好處是,在使用過程中可以方便地增加自定義引數,也方便抓包分析。
從訊息傳送方向上來分,RTSP 的報文有兩類:請求報文和響應報文。請求報文是指從客戶端向伺服器傳送的請求,響應報文是指從伺服器到客戶端的回應。
總結
RTSP 對流媒體提供了諸如 PLAY,PAUSE、SETUP 等控制,但它本身並不傳輸資料,RTSP 的作用相當於流媒體伺服器的遠端控制。
伺服器端可以自行選擇使用 TCP 或 UDP 來傳送串流內容,它的語法和運作跟 HTTP 類似。更多解析請參考 RFC 官方文件,也是最權威的文件。