9.live555mediaserver-如何识别完整的rtsp请求报文?

这是[手把手一起学live555]的第10篇(按这个序号看,请找正确顺序看)。
live555工程在我的gitee下(doc下有思维导图、drawio图):https://gitee.com/lure_ai/live555/tree/master

章节目录链接
0.前言——章节目录链接与为何要写这个?
https://blog.csdn.net/yhb1206/article/details/127259190?spm=1001.2014.3001.5502

学习demo
live555mediaserver.cpp

学习线索和姿势
1.学习的线索和姿势

网络编程
流媒体的地基是网络编程(socket编程)。
[网络编程学习]-0.学习路线

绘图规则
本文的对象图和思维导图遵守的规则详见:
2.绘图规则

TCP非阻塞服务端网络编程流程
socket创建、bind、listen、select、accept、select、recv/send-close

rtsp协商流程
option、describe、setup、play、pause、teardown、get parameter、set parameter。

本节内容和目标
(1)rtsp完整请求报文的识别流程
(2)思维导图
(3)对象图
(4)wireshark抓包

正式开始

上一节对rtsp协议进行了感性浏览、认识,有道是巧妇难为无米之炊,作为服务端,先接受客户端的rtsp请求报文,再解析,再组响应报文回给客户端,这一流程的起始点是基于是完整rtsp请求报文的。所以本节探索如何识别完整rtsp请求报文的流程。
还是拿来7.live555mediaserver-第1阶段小结(完整对象图和思维导图)的链表图作为切入点,如下图。

在这里插入图片描述
图9-1 socket链表图
根据前面学习,知道了最左边“n个客户端”标注的第2个方法是客户端向服务端传输数据时的处理方法,即recvfrom/send-close所在——也是客户端向服务端发送rtsp请求报文所必然走的入口。其方法是如图9-1标注的第2方法:GenericMediaServer::ClientConnection::incomingRequestHandler(void* instance, int /mask/)

同时这个方法也是
5.live555mediaserver-accept》分析的新客户端与服务端建立链接会创建该客户端专属的RTSPServer::RTSPClientConnection对象(每个客户端搞一个“小秘”)的父类的静态方法,如下图。
在这里插入图片描述从上图右边对象图中RTSPServer::RTSPClientConnection类的父类中可以找到那个黑色的静态方法,其实现如下图。
在这里插入图片描述
可以看到GenericMediaServer::ClientConnection::incomingRequestHandler(void* instance, int )这个静态方法里调用了对应RTSPServer::RTSPClientConnection对象的父类方法GenericMediaServer::ClientConnection::incomingRequestHandler()。在这个父类方法里
recvfrom读完buffer后就来执行GenericMediaServer::ClientConnection::handleRequestBytes(bytesRead)
然而它是个纯虚方法,是在子类RTSPServer::RTSPClientConnection实现的,即最后调用的是RTSPServer::RTSPClientConnection::handleRequestBytes(bytesRead)。
这个方法实现如下:
在这里插入图片描述
如上图,这个方法就是个do while循环——在里面包含了识别完整rtsp请求报文、解析和响应的操作。这节学习下它是如何识别rtsp完整报文的。

查找完整rtsp请求报文流程

巧妇难为无米之炊,完整rtsp请求报文就是这“米”。怎么找米?找米是个针对性的处理流程。——对,就是针对rtsp协议的特点、规律而进行的特殊处理(http暂时不关注)。
那么rtsp协议具有什么特点呢?如《rtsp协议格式解析》一文所述(讲的太棒了,膜拜),截图如下
在这里插入图片描述
在这里插入图片描述
上面是大家遵循的人为规定,那么实际是长啥样的呢?用wireshark抓包看下。
这是请求的抓包:
在这里插入图片描述
这是响应的抓包:
在这里插入图片描述
可以发现或总结出以下规律或特点:
(1)rtsp报文分为3个部分:开始行、首部行、实体。请求报文后面一般不会有实体,响应报文某些是没有实体;
(2)一个完整的rtsp请求或响应的判断是最后有\r\n\r\n这个4个字符来标识的;
(3)空格分开行内各字段,\r\n表示一行之结束——区分行与行的标志;
(4)请求是方法先行(OPTIONS等);
(5)响应是版本先行(RTSP/1.0)。

根据这些特点来学习rtsp请求报文流好学了,live555也是按照这个特点来搞的。

巧妇难为无米之炊:存米的物什
先来看下接受rtsp报文数据是存放在哪里,又是如何管理的。如下图。
在这里插入图片描述
在这里插入图片描述
fRequestBuffer是保存客户端socket读取的rtsp请求报文的buffer。这就是存米的地方。
fResponseBuffer是回给客户端的响应报文的buffer(暂不关注)。其他的成员也是标记等作用。
请求报文的buffer这个数组fRequestBuffer是如何处理的?先看其初始化。

在这里插入图片描述
fRequestBytesAlreadySeen记录已处理的数组fRequestBuffe下标,初始化为0。
fRequestBufferBytesLeft初始化为数组整个大小。
fLastCRLF初始化为了数组fRequestBuffer的下标-3——它的意思就是这个buffer的地址-3——只是个值而已。来看下buffer初始化图。
在这里插入图片描述
如上图,这个buffer不是环形buffer,直接拿给recvfrom去装货了。可以看到fLastCRLF所指向的位置——fRequestBuffer-3的位置——我用虚线表示,因为初始化时它只是记录这个值,而不会去访问虚线的这个地址的。那初始化时为何是fRequestBuffer-3呢? 这是等待扫描出完整的rtsp报文的必不可少的核心操作。后面再进行详细解释。

根据buffer中接收到的rtsp报文数据完整性分为以下几种情况。

第1种情况,从buffer中有完整的rtsp报文,且刚刚好一个完整的报文——不多也不少。如下图。
在这里插入图片描述

图中画了上下2个图,图中上图表示的是RTSPServer::RTSPClientConnection::handleRequestBytes(bytesRead)中的do while循环中找到完整的rtsp报文数据了,此时成员fLastCRLF 指向了报文数据最后的“\r\n\r\n”的第1个\r的地址。
图中下图表示是RTSPServer::RTSPClientConnection::handleRequestBytes(bytesRead)中的do while循环处理完这个完整rtsp报文后,又重置了下,恢复了原始状态。

它是怎么找到完整的rtsp报文的呢?如下图核心扫描操作。
在这里插入图片描述
前面我们发现的4个规律之一就是一个完整的rtsp请求或响应的判断是最后有\r\n\r\n这个4个字符来标识的——根据这个规律,live555就是如上图while循环就是扫描\r\n\r\n这个4个字符来判断是否是完整rtsp报文数据的。

上图的第778行、第780行、第785行和第787行就能保证顺利找到\r\n\r\n这4个顺序的字符了。fLastCRLF保存的是上一对“\r\n“中的\r的地址,第780行保证是紧挨着的2对“\r\n”,否则就跳过。最后找到\r\n\r\n,fLastCRLF 就指向第1个\r的地址——也是找到完整rtsp报文的最后截止地方了,endOfMsg自然置为true了,并break出来了。
然后就去解析这整个rtsp报文了,解析处理完后,这个do while循环最后调用RTSPServer::RTSPClientConnection::resetRequestBuffer重置buffer的初始状态了。如下图。
在这里插入图片描述

第2种情况,buffer中有完整的rtsp报文,但还有多余的内容。
此时的场景图如下图。
在这里插入图片描述

这种情况完整的rtsp报文识别和处理流程和第1种情况是一模一样的,不一样的在于第2种情况多了处理多余数据的操作——识别完处理完rtsp报文数据后,增加了一个移动的操作,如下图。
在这里插入图片描述
用了memmove就是防止有交叠的数据,用它比memcpy安全。这样就是把剩余部分移动到了这个buffer的头部。然后它会接着又要do while循环重头开始了,来查找剩余的数据里是否有完整的rtsp报文。假设,此时没有找到完整的rtsp报文。这种情况就是第3种情况了,请看下面。

第3种情况,buffer中没有一个完整的rtsp报文。
场景图:
在这里插入图片描述

结合第1种情况的代码截图进行讲解:图中上图画的是没有找到\r\n\r\n这个完整rtsp的标识符,但是其中有一个\r\n,那么fLastCRLF就指向了\r这位置。图中下图是下次select到客户端socket可读,然后读出来的情况,此时呢,刚刚好是一个完整的rtsp报文——这就变成第1种情况了,怎么处理的,请参见情况1。然后你说,如果此时有多余的呢?那就是第2种情况,去看上面第2种情况怎么处理的,不再赘述。还有人说,如果再来的数据还是不完整的rtsp报文数据呢?那就是本情况,重头看下本情况的处理,不再赘述(等待数据直到完整rtsp报文)。

+2的原因

现在来探讨下 tmpPtr = fLastCRLF + 2; 中+2的原因。拿来之前的图
在这里插入图片描述
为何+2呢?为啥不是+3、+4、+5、+6等等呢?图中我的注释讲的很清楚了。再次强调下原因:除了保证第一次时能进到774行,又要保证如果不是完整的rtsp报文,下次来数据了,能准确定位到上一个\r\n之后的新位置。为啥呢?下面聊一聊。
如果rtsp报文不完整,while循环有2种情况:
(1)如果数据中有\r\n, fLastCRLF 就指向了\r\n的\r的位置,下次来了数据,+2跳过上个\n,来到一个新位置,继续扫描截止符“\r\n\r\n”——但是如果如上图\r\n在中间,我靠这个我觉得它原始的做法比较浪费——又要从中间扫描\r\n,有部分旧数据要重复处理了——有没有很好的方法?——优化点
(2)如果数据中没有一对\r\n, fLastCRLF 还是初始值(buffer-3的位置)。下次来了数据,就会进到情况1代码图的770行的if语句——从buffer头的位置再次排查新老数据——我靠这个也是浪费呀——优化点

fLastCRLF初始化为buffer-3位置的原因
也就是说每次循环tmpPtr = fLastCRLF + 2; 中为了不漏掉截止符“\r\n\r\n”,所以一定要+2。那为了保证当fLastCRLF是初始值时,fLastCRLF+2了还能进入770行,从而使得tmpPtr从buffer开头重新扫描,而不能访问非法地址,得想办法让初始值+2了还能满足第770行,所以fLastCRLF初始值+2一定要小于buffer的起始地址的,那么就倒推呗——x+2<0,所以x≤-3。而它初始值就选择了buffer-3的地址值。

do while循环最后统计已处理大小+4的原因
先看下问题点
在这里插入图片描述
do while每次循环结束时,都会计算已处理的长度requestSize,然后再计算剩余未处理的长度numBytesRemaining,如果有剩余numBytesRemaining > 0则将剩余数据搬移到buffer的头部。
do while循环走到最后这里已经说明是一个完整的rtsp请求报文了,fLastCRLF此时是已经指向\r\n\r\n的第1个\r了。+4是指向了剩余空间的第1个字节的地址,但是因为buffer地址是从0开始的,所以fLastCRLF+4就是整个的已处理长度了。——这个有什么不好理解的么?举个栗子就好了:数组a[10],此时fLastCRLF指向的\r\n\r\n的\r的下标4,fLastCRLF+4就是下标8,下标7是整个的数据截止地址,8反而是长度了。
同时,图上数字2是要搬移到buffer头部的操作,那么剩余数据的首地址也正好是fLastCRLF+4的地方,所以图上数字1和2都可以适应requestSize。
注意,上图的contentLength是实体长度,先不管它,因为请求报文一般是没有实体长度的即为0,所以我图上都忽略它了。

到此,识别完整rtsp请求报文的过程学习完毕。

小结

这个存放客户端发过来的rtsp报文buffer不是个环形buffer——也没必要,因为它是直接被丢给recvfrom的从socket缓存中取到数据。

每次do while循环处理完都要重置下剩余标识、已处理标识等,如果有多余的数据就挪移到buffer头部,如果剩余数据不是完整rtsp报文,那就退出do while循环,并更新已处理标识、剩余标识。然后select等待客户端socket接受到的客户端数据等待下次再处理。这有3种情况。这个的机制很粗暴。
在这个过程中还看到一些优化点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值