认识流媒体协议,从 RTSP 协议解析开始!

认识流媒体协议,从RTSP协议解析开始!

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 官方文档,也是最权威的文档。

推荐阅读
相关专栏
开源技术
106 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。