网上关于rtsp的文章很多,但大多是抽象的理论介绍,从理论学习到实际上手开发往往还有一段距离。然而,没有实际开发经验的支撑,理论又很难理解到位。
本系列文章将从流媒体协议的基础原理开始,通过抓包分析,并结合具体的代码例程,以[原理]->[抓包]->[代码]相结合的方式,循序渐进由浅入深的介绍rtsp/rtp/rtcp开发相关的内容。
希望通过本系列内容的学习,能让大家快速入门流媒体开发需要掌握的技能。
欢迎大家关注[断点实验室]流媒体开发系列文章。
rtsp协议开发指南
rtsp协议格式解析
rtsp协议报文解析-请求行解析
rtsp协议报文解析-首部字段解析
在上篇文章中我们介绍了rtsp协议报文解析原理,阐述了如何通过查表方法解析固定格式的报文内容,同时还给出了请求行报文解析的过程跟踪。
本次内容在上篇文章的基础上,继续讲述如何对rtsp报文首部字段内容进行解析。
让我们先来回顾下rtsp协议的报文格式,以及首部字段的使用场景。
与http协议类似,rtsp协议的首部字段是构成rtsp报文的要素之一,它为客户端和服务端提供报文交互所需的报文序列号、使用的语言、报文日期等内容。
support列中的"req."字段表示对应的首部行字段,在相关类型的报文中必须要包含的,而标记为"opt."的字段是可选的。
methods列中列出的方法为各个首部行字段实际使用的报文场景,如Range字段一般应用于PLAY, PAUSE, RECORD这三个报文中,表示视频流在PLAY, PAUSE, RECORD这三个操作中的作用范围。
Header | type | support | methods |
---|---|---|---|
Accept | R | opt. | entity |
Accept-Encoding | R | opt. | entity |
Accept-Language | R | opt. | all |
Allow | r | opt. | all |
Authorization | R | opt. | all |
Bandwidth | R | opt. | all |
Blocksize | R | opt. | all but OPTIONS, TEARDOWN |
Cache-Control | g | opt. | SETUP |
Conference | R | opt. | SETUP |
Connection | g | req. | all |
Content-Base | e | opt. | entity |
Content-Encoding | e | req. | SET_PARAMETER |
Content-Encoding | e | req. | DESCRIBE, ANNOUNCE |
Content-Language | e | req. | DESCRIBE, ANNOUNCE |
Content-Length | e | req. | SET_PARAMETER, ANNOUNCE |
Content-Length | e | req. | entity |
Content-Location | e | opt. | entity |
Content-Type | e | req. | SET_PARAMETER, ANNOUNCE |
Content-Type | r | req. | entity |
CSeq | g | req. | all |
Date | g | opt. | all |
Expires | e | opt. | DESCRIBE, ANNOUNCE |
From | R | opt. | all |
If-Modified-Since | R | opt. | DESCRIBE, SETUP |
Last-Modified | e | opt. | entity |
Proxy-Require | R | req. | all |
Public | r | opt. | all |
Range | R | opt. | PLAY, PAUSE, RECORD |
Range | r | opt. | PLAY, PAUSE, RECORD |
Referer | R | opt. | all |
Require | R | req. | all |
Retry-After | r | opt. | all |
RTP-Info | r | req. | PLAY |
Scale | Rr | opt. | PLAY, RECORD |
Session | Rr | req. | all but SETUP, OPTIONS |
Server | r | opt. | all |
Speed | Rr | opt. | PLAY |
Transport | Rr | req. | SETUP |
Unsupported | r | req. | all |
User-Agent | R | opt. | all |
Via | g | opt. | all |
WWW-Authenticate | r | opt. | all |
从首部字段报文格式可以看出,与请求行格式类似,首部字段也是由一系列固定字段组成,通过特殊字符分割首部字段键值对,最后以回车换行结尾。
因此,我们可以采取与请求行报文解析类似的方法,设计一个待解析首部字段数据结构描述表,描述表中预先包含支持的首部字段类型名称,内容长度以及对应的解析方法,通过查表比对报文内容与描述表字段名称,然后调用对应的解析方法处理。
下面我们来看下具体的实现过程。
我们定义一个待解析首部字段的数据结构描述表,将首部字段名长度、字段名以及该字段的解析方法关联起来,根据字段名匹配解析方法。
待解析字段数据结构描述表定义
//首部字段解析函数指针
typedef int (*rtsp_msg_line_parser) (rtsp_msg_s *msg, const char *line);
//关联rtsp首部字段类型名及其长度
typedef struct __rtsp_msg_str2parser_tbl_s{
int strsiz;//首部字段长度
const char *strval;//首部字段字符指针
rtsp_msg_line_parser parser;//rtsp首部段解析函数指针
} rtsp_msg_str2parser_tbl_s;
首部字段数据结构描述表实例,每个字段名包含了冒号、空格字符。
//关联rtsp请求报文首部字段类型及对应的处理函数
static const rtsp_msg_str2parser_tbl_s rtsp_msg_hdr_line_parse_tbl[] = {
{6, "CSeq: ", rtsp_msg_parse_cseq},
{6, "Date: ", rtsp_msg_parse_date},
{9, "Session: ", rtsp_msg_parse_session},
{11, "Transport: ", rtsp_msg_parse_transport},
{7, "Range: ", rtsp_msg_parse_range},
{8, "Accept: ", rtsp_msg_parse_accept},
{15, "Authorization: ", rtsp_msg_parse_authorization},
{12, "User-Agent: ", rtsp_msg_parse_user_agent},
{8, "Public: ", rtsp_msg_parse_public_},
{10, "RTP-Info: ", rtsp_msg_parse_rtp_info},
{8, "Server: ", rtsp_msg_parse_server},
{14, "Content-Type: ", rtsp_msg_parse_content_type},
{16, "Content-Length: ", rtsp_msg_parse_content_length},
};
与请求行报文解析过程类似,这里遍历描述表中所有首部字段类型,根据报文首部字段名查表返回解析函数指针地址。
static rtsp_msg_line_parser rtsp_msg_str2parser(const char *line) {
const rtsp_msg_str2parser_tbl_s *tbl = rtsp_msg_hdr_line_parse_tbl;
int num = ARRAY_SIZE(rtsp_msg_hdr_line_parse_tbl);
int i;
for (i = 0; i < num; i++) {//遍历所有首部字段类型,寻找对应的处理函数
if (strncmp(tbl[i].strval, line, tbl[i].strsiz) == 0)
return tbl[i].parser;//返回解析首部字段函数指针地址
}
return NULL;
}
首部字段解析函数查找过程示意图。
在找到解析函数地址后就可以开始解析过程了。
//遍历请求报文所有header field首部字段,根据首部字段名对其进行解析
while ((p = rtsp_msg_hdr_next_line(p, line, sizeof(line)))) {//读取首部行数据到line,返回下一行指针
rtsp_msg_line_parser parser;//首部段解析函数指针
if (strlen(line) == 0)//检查首部字段header field缓存长度是否为0
break;
//根据首部字段名获取对应的处理函数parse
parser = rtsp_msg_str2parser(line);//返回首部字段解析函数指针
if (!parser) {//检查首部段解析函数指针
warn("unknown line: %s\n", line);
continue;
}
ret = (*parser) (msg, line);//对首部字段进行解析,并返回结果
if (ret < 0) {//检查操作结果
err("parse failed. line: %s\n", line);
break;
}
}
在rtsp_msg_str2parser中查找解析函数地址,在(*parser) (msg, line)中解析首部字段。
下面我们来看下首部字段实际的解析过程。
这里引入宏定义方式(macro),将相同解析过程首部字段的解析方法放在一起实现。
CSeq/Session/Content-Length字段解析方法。
//link CSeq/Session int
#define DEFINE_PARSE_BUILD_LINK_CSEQ(_name, _type, _param, _fmt) \
static int rtsp_msg_parse_##_name (rtsp_msg_s *msg, const char *line) { \//解析CSeq/Session/Content-Length首部请求报文
printf("parsename=%s\n",_fmt);\//CSeq/Session/Content-Length首部类型名打印
rtsp_msg_hdr_s *hdrs = &msg->hdrs; \//取得报头结构实例指针
if (hdrs->_name) { \//检查_name表示的首部字段结构对象是否存在
rtsp_mem_free(hdrs->_name); \//释放相应的内存空间
hdrs->_name = NULL; \//指针置零
} \
hdrs->_name = (_type*)rtsp_mem_alloc(sizeof(_type)); \//为_name表示的首部结构对象创建内存空间
if (!hdrs->_name) { \//检查内存空间创建结果
err("rtsp_mem_alloc for %s failed\n", #_type); \
return -1; \
} \
if (sscanf(line, _fmt, &hdrs->_name->_param) != 1) { \//从line缓存中读取CSeq/Session/Content-Length到对应的结构体对象中
rtsp_mem_free(hdrs->_name); \
hdrs->_name = NULL; \
err("parse %s failed. line: %s\n", #_name, line); \
return -1; \
} \
return 0; \
} \//组装CSeq/Session/Content-Length应答报文
static int rtsp_msg_build_##_name (const rtsp_msg_s *msg, char *line, int size){ \
printf("name=%s\n",_fmt);\//CSeq/Session/Content-Length首部类型名打印
if (msg->hdrs._name) { \//检查CSeq/Session/Content-Length结构体对象相应字段是否存在
snprintf(line, size, _fmt "\r\n", msg->hdrs._name->_param); \//组装应答报文
return strlen(line); \//返回报文长度
} \
return 0; \
}
DEFINE_PARSE_BUILD_LINK_CSEQ(cseq, rtsp_msg_cseq_s, cseq, "CSeq: %u")
DEFINE_PARSE_BUILD_LINK_CSEQ(session, rtsp_msg_session_s, session, "Session: %08X")
DEFINE_PARSE_BUILD_LINK_CSEQ(content_length, rtsp_msg_content_length_s, length, "Content-Length: %u")
在宏定义DEFINE_PARSE_BUILD_LINK_CSEQ(_name, _type, _param, _fmt)中包含四个参数,这些参数在宏展开时被替换为实际输入的参数。
在DEFINE_PARSE_BUILD_LINK_CSEQ中根据具体解析的字段类型配置相应的参数,为宏在预编译阶段,展开成解析函数时生成对应的C代码。
这里需要注意的是,不像普通函数声明或实现中的形参,在宏中定义的参数是没有具体类型的,只是在宏展开时替换为输入的内容而已。
在rtsp_msg_parse_##_name函数实现中,msg和line分别为报文描述数据结构对象和报文缓存指针,解析过程先通过rtsp_mem_alloc为首部字段创建缓存空间,然后通过sscanf从报文中读数据,读取的报文内容最终保存在报文数据结构对象中msg。
类似的,下面是Server/User-Agent/Date字段解析方法。
//link Server/User-Agent char[]
#define DEFINE_PARSE_BUILD_LINK_SERVER(_name, _type, _param, _fmt) \
static int rtsp_msg_parse_##_name (rtsp_msg_s *msg, const char *line){ \//解析Server/User-Agent/Date首部请求报文
printf("parsename=%s\n",_fmt);\//打印Server/User-Agent/Date首部请求类型
rtsp_msg_hdr_s *hdrs = &msg->hdrs; \//取得报头结构实例指针
const char *p = line; \
unsigned int tmp = 0; \
if (hdrs->_name) { \//检查_name表示的首部字段结构对象是否存在
rtsp_mem_free(hdrs->_name); \//释放相应的内存空间
hdrs->_name = NULL; \//指针置零
} \
hdrs->_name = (_type*)rtsp_mem_alloc(sizeof(_type)); \//为_name表示的首部结构对象创建内存空间
if (!hdrs->_name) { \//检查内存空间创建结果
err("rtsp_mem_alloc for %s failed\n", #_type); \
return -1; \
} \
while (isgraph(*p) && *p != ':') p++; \//在报文中查找':'字符,指针跳转到对应的位置
if (*p != ':') { \//检查指针位置
rtsp_mem_free(hdrs->_name); \
hdrs->_name = NULL; \
err("parse %s failed. line: %s\n", #_name, line); \
return -1; \
} \
p++; \//指针后移
while (*p == ' ') p++; \//在报文中查找' '空格字符,指针跳转到对应的位置
while (isprint(*p) && tmp < sizeof(hdrs->_name->_param) - 1) { \
hdrs->_name->_param[tmp++] = *p++; \//将报文中内容复制到对应的结构体对象中
} \
hdrs->_name->_param[tmp] = 0; \//末尾置零
return 0; \
} \//组装Server/User-Agent/Date应答报文
static int rtsp_msg_build_##_name (const rtsp_msg_s *msg, char *line, int size){ \
printf("name=%s\n",_fmt);\//打印Server/User-Agent/Date首部请求类型
if (msg->hdrs._name) { \//检查_name表示的首部字段结构对象是否存在
snprintf(line, size, _fmt "\r\n", msg->hdrs._name->_param); \//组装应答报文
return strlen(line); \//返回报文长度
} \
return 0; \
}
DEFINE_PARSE_BUILD_LINK_SERVER(server, rtsp_msg_server_s, server, "Server: %s")
DEFINE_PARSE_BUILD_LINK_SERVER(user_agent, rtsp_msg_user_agent_s, user_agent, "User-Agent: %s")
DEFINE_PARSE_BUILD_LINK_SERVER(date, rtsp_msg_date_s, http_date, "Date: %s")
Public/Accept字段解析方法。
#define DEFINE_PARSE_BUILD_LINK_PUBLIC(_name, _type, _param, _fmt, _tbl) \
static int rtsp_msg_parse_##_name (rtsp_msg_s *msg, const char *line) { \//解析Public/Accept首部请求报文
printf("parsename=%s\n",_fmt);\//打印Public/Accept首部请求类型
rtsp_msg_hdr_s *hdrs = &msg->hdrs; \//取得报头结构实例指针
const char *p = line; \
int num = ARRAY_SIZE(_tbl); \
int i = 0; \
if (hdrs->_name) { \//检查_name表示的首部字段结构对象是否存在
rtsp_mem_free(hdrs->_name); \//释放相应的内存空间
hdrs->_name = NULL; \//指针置零
} \
hdrs->_name = (_type*)rtsp_mem_alloc(sizeof(_type)); \//为_name表示的首部结构对象创建内存空间
if (!hdrs->_name) { \//检查内存空间创建结果
err("rtsp_mem_alloc for %s failed\n", #_type); \
return -1; \
} \
while (isgraph(*p) && *p != ':') p++; \//在报文中查找':'字符,指针跳转到对应的位置
if (*p != ':') { \//检查指针位置
rtsp_mem_free(hdrs->_name); \
hdrs->_name = NULL; \
err("parse %s failed. line: %s\n", #_name, line); \
return -1; \
} \
p++; \//指针后移
while (*p == ' ') p++; \//在报文中查找' '空格字符,指针跳转到对应的位置
for (i = 0; i < num; i++) { \//遍历有Public/Accept类型
if (_tbl[i].strsiz && strstr(p, _tbl[i].strval)) \//在请求报文中查找请求类型名
hdrs->_name->_param |= 1 << _tbl[i].intval; \//将字段类型枚举名传入结构体对象
} \
return 0; \
} \//Public/Accept应答报文
static int rtsp_msg_build_##_name (const rtsp_msg_s *msg, char *line, int size){ \
printf("name=%s\n",_fmt);\//打印Public/Accept首部请求类型
if (msg->hdrs._name) { \
char *p = line; \
int len, i, flag = 0; \
int num = ARRAY_SIZE(_tbl); \
snprintf(p, size, _fmt, ""); \
len = strlen(p); \
p += len; \
size -= len; \
if (size <= 1) { \
return (p - line); \
} \
for (i = 0; i < num; i++) { \
if (msg->hdrs._name->_param & (1 << _tbl[i].intval)) { \
if (flag) { \
snprintf(p, size, ", %s", _tbl[i].strval); \
} else { \
snprintf(p, size, "%s", _tbl[i].strval); \
flag = 1; \
} \
len = strlen(p); \
p += len; \
size -= len; \
if (size <= 1) { \
return (p - line); \
} \
} \
} \
snprintf(p, size, "\r\n"); \
len = strlen(p); \
p += len; \
return (p - line); \
} \
return 0; \
}
DEFINE_PARSE_BUILD_LINK_PUBLIC(public_, rtsp_msg_public_s, public_, "Public: %s", rtsp_msg_method_tbl)
DEFINE_PARSE_BUILD_LINK_PUBLIC(accept, rtsp_msg_accept_s, accept, "Accept: %s", rtsp_msg_content_type_tbl)
Transport首部字段的报文内容相对特殊,因此解析方法单独给出。
//Transport请求解析
static int rtsp_msg_parse_transport(rtsp_msg_s *msg, const char *line) {
rtsp_msg_hdr_s *hdrs = &msg->hdrs;//取得报头结构实例指针
const char *p;
unsigned int tmp;
if (hdrs->transport) {//检查header Transport字段指针是否初始化
rtsp_mem_free(hdrs->transport);//释放header Transport字段缓存空间
hdrs->transport = NULL;//指针置零
}
//初始化header Transport字段结构缓存空间
hdrs->transport = (rtsp_msg_transport_s *)rtsp_mem_alloc(sizeof(rtsp_msg_transport_s));
if (!hdrs->transport) {//检查缓存空间是否创建成功
err("rtsp_mem_alloc for %s failed\n", "rtsp_msg_transport_s");
return -1;
}
p = strstr(line, "RTP/AVP");//查找在line中首次出现"RTP/AVP"的地方,并返回指针位置
if (!p) {//检查返回指针位置
err("parse transport failed. line: %s\n", line);
rtsp_mem_free(hdrs->transport);
hdrs->transport = NULL;
return -1;
}
//从请求报文缓存中解析出rtsp负载类型,并存储到rtsp_msg_transport_type_e结构对象中
hdrs->transport->type = rtsp_msg_str2int(rtsp_msg_transport_type_tbl,
ARRAY_SIZE(rtsp_msg_transport_type_tbl), p);
//查找在line中首次出现"ssrc"的地方,并返回指针位置
if ((p = strstr(line, "ssrc="))) {
if (sscanf(p, "ssrc=%X", &tmp) == 1) {//从请求报文缓存中读ssrc到tmp中
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_SSRC;//更新flags取值
hdrs->transport->ssrc = tmp;//存档ssrc值到rtsp_msg_transport_s的ssrc字段中
}
}
//查找在line中首次出现"unicast"的地方,并返回指针位置
if ((p = strstr(line, "unicast"))) {
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_UNICAST;//更新flags取值
}
//查找在line中首次出现"multicast"的地方,并返回指针位置
if ((p = strstr(line, "multicast"))) {
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_MULTICAST;//更新flags取值
}
//查找在line中首次出现"client_port="的地方,并返回指针位置
if ((p = strstr(line, "client_port="))) {
if (sscanf(p, "client_port=%u-%*u", &tmp) == 1) {//从请求报文缓存中读client_port到tmp中
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_CLIENT_PORT;//更新flags取值
hdrs->transport->client_port = tmp;//存档client_port值到rtsp_msg_transport_s的client_port字段中
}
}
//查找在line中首次出现"server_port="的地方,并返回指针位置
if ((p = strstr(line, "server_port="))) {
if (sscanf(p, "server_port=%u-%*u", &tmp) == 1) {//从请求报文缓存中读server_port到tmp中
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_SERVER_PORT;//更新flags取值
hdrs->transport->server_port = tmp;//存档server_port值到rtsp_msg_transport_s的server_port字段中
}
}
//查找在line中首次出现"interleaved="的地方,并返回指针位置
if ((p = strstr(line, "interleaved="))) {
if (sscanf(p, "interleaved=%u-%*u", &tmp) == 1) {//从请求报文缓存中读interleaved=到tmp中
hdrs->transport->flags |= RTSP_MSG_TRANSPORT_FLAG_INTERLEAVED;//更新flags取值
hdrs->transport->interleaved = tmp;//存档interleaved值到rtsp_msg_transport_s的interleaved字段中
}
}
return 0;
}
此外,部分字段如Range/AuthorizationRTP-Info的解析函数在demo中缺少实现过程,函数直接返回0,这里不影响rtsp服务端demo的正常工作,感兴趣的读者可以研究下如何实现。
为了更加深入的理解上述报文解析,这里开启gdb调试模式,对解析过程进行跟踪调试。
首先在root模式下进入调试模式(这里涉及网络操作,因此需要root权限),在首部字段解析入口函数rtsp_msg_str2parser中,以及报文解析函数rtsp_msg_parse_from_array中插入断点,单步执行观察每步的结果。
sudo su//进入root模式
gdb ./demo//开启调试跟踪
b rtsp_msg.c:763//设置断点
r//开始运行程序
接着启动vlc播放器,打开网络串流选项,输入url地址[rtsp://127.0.0.1/live/chn0]启动视频流播放,下面的截图中可以看到断点已经被捕获。
在rtsp_msg_str2parser中找到首部字段解析函数的入口地址并返回。
在rtsp_msg_parse_from_array中,根据得到的parser解析函数地址,对首部字段进行解析。打印parser可得到当前解析函数的入口地址。
查找User-Agent字段解析函数入口地址。
在rtsp_msg_parse_from_array中解析User-Agent首部字段。
另外,由于解析函数本身是通过宏定义方式实现的,调试模式下可能跟踪不到具体的执行过程,感兴趣的读者可以在代码中用宏展开后的代码替代宏定义代码进行调试。
本篇为流媒体开发系列文章的第四篇,本次内容中,我们回顾了首部字段的报文格式,介绍了首部字段的解析原理,通过代码展示了首部字段的实际解析过程,最后对解析过程的关键内容进行了跟踪调试。
欢迎大家继续关注[断点实验室]流媒体开发系列后续文章。
ffmpeg播放器实现详解 - FFmpeg编译安装
ffmpeg播放器实现详解 - FFPlay源码编译
ffmpeg播放器实现详解 - 框架搭建
ffmpeg播放器实现详解 - 视频显示
ffmpeg播放器实现详解 - 音频播放
ffmpeg播放器实现详解 - 创建线程
ffmpeg播放器实现详解 - 视频同步控制
ffmpeg播放器实现详解 - 音频同步控制
ffmpeg播放器实现详解 - 快进快退控制
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。