rtsp协议报文解析-请求行解析

前言

网上关于rtsp的文章很多,但大多是抽象的理论介绍,从理论学习到实际上手开发往往还有一段距离。然而,没有实际开发经验的支撑,理论又很难理解到位。

本系列文章将从流媒体协议的基础原理开始,通过抓包分析,并结合具体的代码例程,以[原理]->[抓包]->[代码]相结合的方式,循序渐进由浅入深的介绍rtsp/rtp/rtcp开发相关的内容。

希望通过本系列内容的学习,能让大家快速入门流媒体开发需要掌握的技能。

欢迎大家关注[公众号:断点实验室]流媒体开发系列文章。
rtsp协议开发指南
rtsp协议格式解析
rtsp协议报文解析-请求行解析
rtsp协议报文解析-首部字段解析

在上篇文章中我们介绍了rtsp协议报文格式,通过抓包分析了报文交互,最后引入一系列的结构体组件,得到了用于描述rtsp报文信息的数据结构。

本次内容在上篇文章的基础上,讲述如何对rtsp报文内容进行解析。

1 RTSP协议报文解析原理

这里我们以rtsp请求行报文解析为例进行介绍,其它报文字段解析原理类似。

请求行报文一般由"方法"、"URL"以及"版本"三个固定字段组成,以空格符分隔开,最后以回车换行结尾。

如果要对请求行进行解析,我们先定位到请求行的空格与回车换行符位置,然后截取出"方法"、"URL"以及"版本"三个字段的报文内容,再根据报文内容返回对应的结果

例如,根据空格位置截取出请求行"方法"字段报文[4f 50 54 49 4f 4e 53],根据ascii码表可以推断出这段报文信息表示协议"OPTIONS"类型请求方法,在解析结果中返回rtsp_msg_method_e结构中的RTSP_MSG_METHOD_OPTIONS描述类型。

下图为rtsp报文格式原理图与实际的抓包截图,可以看到wireshark已经自动帮我们解析出了报文内容并排好了版。

wireshark通过解析结果与原始码流的对比,向我们直观的展现出它们之间的对照关系。

2 RTSP协议请求行报文解析实现

按照协议约定,请求行中的"方法"字段,一般是OPTIONS,DESCRIBE,SETUP,PLAY,TEARDOWN中的某一个。

协议中这些字段内容都是固定不变的,因此,我们可以考虑采用查表的方法来解析报文。即设计一个待解析字段的数据结构描述表,描述表中预先包含该字段所有类型的报文内容,内容长度及描述类型(枚举类型),通过将接收到某个字段的报文码流,与描述表中该字段预置报文内容进行比对,若比对成功则返回该字段描述类型值

下面我们来看下具体的实现过程。

我们定义一个待解析字段的数据结构描述表,将协议某个字段类型(枚举类型)、该字段的报文内容及报文长度关联起来,根据报文内容匹配字段类型。

待解析字段数据结构描述表

typedef struct __rtsp_msg_int2str_tbl_s {
	int intval;//字段类型
	int strsiz;//字段类型名长度
	const char *strval;//字段类型名指针
} rtsp_msg_int2str_tbl_s;

例如,这里创建请求行"方法"字段数据结构描述表,将"方法"字段类型、类型名长度及类型名内容关联到一起。

请求行"方法"字段数据结构描述表

static const rtsp_msg_int2str_tbl_s rtsp_msg_method_tbl[] = {
	{ RTSP_MSG_METHOD_OPTIONS, 7, "OPTIONS", },
	{ RTSP_MSG_METHOD_DESCRIBE, 8, "DESCRIBE", },
	{ RTSP_MSG_METHOD_SETUP, 5, "SETUP", },
	{ RTSP_MSG_METHOD_PLAY, 4, "PLAY", },
	{ RTSP_MSG_METHOD_RECORD, 6, "RECORD", },
	{ RTSP_MSG_METHOD_PAUSE, 5, "PAUSE", },
	{ RTSP_MSG_METHOD_TEARDOWN, 8, "TEARDOWN", },
	{ RTSP_MSG_METHOD_ANNOUNCE, 8, "ANNOUNCE", },
	{ RTSP_MSG_METHOD_SET_PARAMETER, 13, "SET_PARAMETER", },
	{ RTSP_MSG_METHOD_GET_PARAMETER, 13, "GET_PARAMETER", },
	{ RTSP_MSG_METHOD_REDIRECT, 8, "REDIRECT", },
	{ RTSP_MSG_METHOD_BUTT, 0, "", },//未匹配到的类型(缺省类型)
};

有了对比的数据结构模板,下面我们来看下如何对协议报文进行解析。

将接收到的请求行内容与"方法"字段数据结构描述表进行对比,返回匹配到的字段类型,即根据报文码流内容返回类型枚举值。

static int rtsp_msg_str2int(const rtsp_msg_int2str_tbl_s *tbl, int num, const char *str) {
	int i;
	for (i = 0; i < num; i++) {//遍历描述类型列表,与请求行报文进行对比
		if (strncmp(tbl[i].strval, str, tbl[i].strsiz) == 0) {//报文码流对比
			return tbl[i].intval;//返回字段类型
		}
	}
	return tbl[num-1].intval;//未匹配到的类型(缺省类型)
}

其中,str为报文码流当前位置指针,num为需要遍历的数据结构描述表长度,tbl为待解析的字段描述表数据结构。

准备工作都做好了,下面看下对请求行报文的解析过程。

我们在rtsp_msg_parse_startline对请求行报文进行解析,line为接收到的报文缓存指针,rtsp_msg_s为报文数据组件,对这块不熟悉的读者可以回顾下[断点实验室]系列文章[rtsp协议格式解析]。

函数一开始保存了报文缓存指针,在rtsp_msg_str2int中对报文内容与预置的报文类型名进行比对,并返回匹配到的字段类型,最后将自读类型保存到msg->hdrs.startline.reqline.method中,这样就完了了对请求行"方法"字段的解析过程。

static int rtsp_msg_parse_startline(rtsp_msg_s *msg, const char *line) {
	const char *p = line;//传递待解析数据指针
	int ret;//操作结果
	//将接收到的请求行内容与rtsp各种相关操作类型名进行比对,返回匹配到的类型名长度
	ret = rtsp_msg_str2int(rtsp_msg_method_tbl, ARRAY_SIZE(rtsp_msg_method_tbl), p);//匹配rtsp请求报文类型
	if (ret != RTSP_MSG_METHOD_BUTT) {//检查返回请求报文类型,是否匹配到rtsp_msg_method_tbl中的某个类型
		msg->type = RTSP_MSG_TYPE_REQUEST;//将报文类型置为请求报文类型
		msg->hdrs.startline.reqline.method = ret;//保存请求类型

3 RTSP协议请求行报文解析过程调试

为了更加深入的理解上述报文解析,这里开启gdb调试模式,对解析过程进行跟踪调试。

首先在root模式下进入调试模式(这里涉及网络操作,因此需要root权限),在请求行解析函数rtsp_msg_parse_startline与报文字段类型解析函数rtsp_msg_str2int中插入断点,接着单步执行观察每步的结果。

sudo su//进入root模式
gdb ./demo//开启调试跟踪
b rtsp_msg.c:248//设置断点
r//开始运行程序

接着启动vlc播放器,打开网络串流选项,输入url地址[rtsp://127.0.0.1/live/chn0]启动视频流播放,下面的截图中可以看到断点已经被捕获。

接着我们以单步方式运行程序,可以看到rtsp_msg_str2int函数的三个输入参数,分别是0x40ade0地址下的rtsp_msg_method_tbl描述表,描述表长度12,以及待解析报文内容指针。

单步到strncmp函数所在行,通过查表方式对报文内容与描述表预置的类型名逐个比对,若比对成功则返回报文对应的字段类型,这里返回0,即rtsp_msg_method_e中的RTSP_MSG_METHOD_OPTIONS类型。

类似的,我们可以通过定义不同的rtsp_msg_int2str_tbl_s类型描述表,对协议中有固定报文内容的字段进行解析,如:

rtsp uri类型

static const rtsp_msg_int2str_tbl_s rtsp_msg_uri_scheme_tbl[] = {
	{RTSP_MSG_URI_SCHEME_RTSPU, 6, "rtspu:"},
	{RTSP_MSG_URI_SCHEME_RTSP, 5, "rtsp:"},
	{RTSP_MSG_URI_SCHEME_BUTT, 0, ""}, //未匹配到的类型(缺省类型)
};

rtsp版本号类型

static const rtsp_msg_int2str_tbl_s rtsp_msg_version_tbl[] = {
	{RTSP_MSG_VERSION_1_0, 8, "RTSP/1.0"},
	{RTSP_MSG_VERSION_BUTT, 0, ""},//未匹配到的类型(缺省类型)
};

rtsp负载类型

static const rtsp_msg_int2str_tbl_s rtsp_msg_transport_type_tbl[] = {
	{RTSP_MSG_TRANSPORT_TYPE_RTP_AVP_TCP, 11, "RTP/AVP/TCP"},
	{RTSP_MSG_TRANSPORT_TYPE_RTP_AVP, 7, "RTP/AVP"},
	{RTSP_MSG_TRANSPORT_TYPE_BUTT, 0, ""},//未匹配到的类型(缺省类型)
};

与请求行"方法"字段解析原理类似,其他字段通过查表方式对比报文内容与描述表预置的类型名,返回对应的结果。

4 RTSP协议请求行URL字段解析

与协议中有固定报文内容字段不同,一个完整的url字段包含ip地址,端口号以及资源路径等信息,下面我们在rtsp_msg_parse_uri中分别对它们进行解析。

//解析请求报文uri资源信息,包括uri中的ip地址,端口号及资源绝对路径,并保存到rtsp_msg_uri_s结构对象中
static int rtsp_msg_parse_uri(const char *line, rtsp_msg_uri_s *uri) {
	const char *p = line, *q;
	unsigned int tmp;

	//将输入的header一行内容与rtsp各种相关操作类型名进行比对,返回匹配到的类型名长度
	//检查uri类型名长度(rtsp/rtspu)
	uri->scheme = rtsp_msg_str2int(rtsp_msg_uri_scheme_tbl, ARRAY_SIZE(rtsp_msg_uri_scheme_tbl), line);
	if (uri->scheme == RTSP_MSG_URI_SCHEME_BUTT) {//检查uri类型名匹配结果,若结果为RTSP_MSG_URI_SCHEME_BUTT则表示未匹配到
		err("parse scheme failed. line: %s\n", line);
		return -1;
	}
	uri->port = 0; //初始端口号
	uri->ipaddr[0] = 0;//初始ip地址
	uri->abspath[0] = 0;//初始uri资源绝对路径

	//检查协议类型(rtsp:)
	while (islower(*p) || *p == ':') p++;//检查输入参数是否为小写英文字母,或者输入参数为":"

	//检查符号"//"
	if (*p != '/' || *(p+1) != '/') {
		err("parse ip failed. line: %s\n", line);
		return -1;
	}
	p += 2;//更新游标位置

	q = p;
	//跳过ip地址
	while (isgraph(*q) && *q != ':' && *q != '/') q++;//检查输入参数是否为小写英文字母,或者输入参数为":"
	if (*q == ':') {//检查':'符号
		//rtsp://192.168.0.102:554/live/chn0000 RTSP/1.0
		//这里播放器在请求报文uri的ip地址后面,根据rtsp协议类型,自动加入了端口号信息
		if (sscanf(q + 1, "%u", &tmp) != 1) {//取得端口号
			err("parse uri port failed. line: %s\n", line);
			return -1;
		}
		uri->port = tmp;//传递端口号
	}

	tmp = q - p;//取得uri中的ip地址长度
	if (tmp > sizeof(uri->ipaddr) - 1)
		tmp = sizeof(uri->ipaddr) - 1;
	memcpy(uri->ipaddr, p, tmp);//取得uri ip地址
	uri->ipaddr[tmp] = 0;//末尾置0

	//游标跳过端口号
	while (isgraph(*q) && *q != '/') q++;
	if (*q != '/')
		return (q - line);

	p = q;
	//检查输入参数是否为小写英文字母,或者输入参数为":"
	while (isgraph(*q)) q++;//更新游标位置
	tmp = q - p;
	if (tmp > sizeof(uri->abspath) - 1)
		tmp = sizeof(uri->abspath) - 1;
	memcpy(uri->abspath, p, tmp);//取得资源绝对路径
	uri->abspath[tmp] = 0;//末尾置0

	return (q - line);
}

首先,我们仍然通过rtsp_msg_str2int函数返回rtsp传输方式字段。在初始化url包含的ip地址、端口号及资源路径后,接着通过不断检查报文中的特殊符号更新指针位置,然后将对应的报文内容复制到对应的数据结构中,下面给出调试模式下一些关键数据的结果。

因为请求行中并没有包含端口号信息,因此这里的端口号解析结果为0。

有了url解析函数,我们就可以在请求行解析函数中继续对url字段进行解析了,在指针跳过空格后,进入rtsp_msg_parse_uri函数进行url的解析。

//对请求报文请求行数据进行解析,并保存到rtsp_msg_s结构对象中
static int rtsp_msg_parse_startline(rtsp_msg_s *msg, const char *line) {
	const char *p = line;//传递待解析数据指针
	int ret;//操作结果
	//将输入的header一行内容与rtsp各种相关操作类型名进行比对,返回匹配到的类型名长度
	ret = rtsp_msg_str2int(rtsp_msg_method_tbl, ARRAY_SIZE(rtsp_msg_method_tbl), p);//匹配rtsp请求报文类型
	if (ret != RTSP_MSG_METHOD_BUTT) {//检查返回请求报文类型,是否匹配到rtsp_msg_method_tbl中的某个类型
		msg->type = RTSP_MSG_TYPE_REQUEST;//将报文类型置为请求报文类型
		msg->hdrs.startline.reqline.method = ret;//指定请求类型

		while (isgraph(*p)) p++;//检测空格,指针跳过请求方法名
		p++;//next field,指针跳过请求方法后的空格,准备开始解析请求url

		//解析请求报文uri资源信息,包括uri中的ip地址,端口号及资源绝对路径,并保存到rtsp_msg_uri_s对象中
		ret = rtsp_msg_parse_uri(p,	&msg->hdrs.startline.reqline.uri);
		if (ret <= 0)//检查操作结果
			return -1;
            
        while (isgraph(*p)) p++; p++; //next field,检测空格,指针跳过url及紧随其后的空格

		//匹配rtsp请求报文中的版本号信息
		ret = rtsp_msg_str2int(rtsp_msg_version_tbl, ARRAY_SIZE(rtsp_msg_version_tbl), p);
		if (ret == RTSP_MSG_VERSION_BUTT) {//检查匹配结果,若未匹配到有效数据
			err("parse version failed. line: %s\n", line);
			return -1;
		}
		return 0;
	}

小结

本篇为流媒体开发系列文章的第三篇,本次内容中,我们通过定位报文中空格回车换行符,截取请求行中"方法"、"URL"以及"版本"三个字段的报文数据,通过与一系列报文字段描述表进行比对,解析出了固定格式报文字段信息。

接着又通过检查报文中的":"等特殊字符,不断更新指针位置截取出url包含的信息。

在下一篇文章中,我们会讲述如果对报文首部字段进行解析,欢迎大家继续关注。

往期推荐

ffmpeg播放器实现详解 - FFmpeg编译安装
ffmpeg播放器实现详解 - FFPlay源码编译
ffmpeg播放器实现详解 - 框架搭建
ffmpeg播放器实现详解 - 视频显示
ffmpeg播放器实现详解 - 音频播放
ffmpeg播放器实现详解 - 创建线程
ffmpeg播放器实现详解 - 视频同步控制
ffmpeg播放器实现详解 - 音频同步控制
ffmpeg播放器实现详解 - 快进快退控制


// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 公众号:断点实验室
// 扫描二维码,关注更多优质原创,内容包括:音视频开发、图像处理、网络、
// Linux,Windows、Android、嵌入式开发等

posted @ 2022-09-12 22:48  断点实验室  阅读(383)  评论(0编辑  收藏  举报