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

学习demo

live555mediaserver.cpp

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

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

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

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

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

本节内容和目标
(1)rtsp协议的describe请求与响应
(2)思维导图绘制
(3)wireshark抓包
(4)c++虚函数继承知识、虚函数形参初始值继承知识
(5)对象图、链表图
(6)封装、继承、多态

正式开始
上节学习了rtsp协商的第一个节点OPTIONS,客户端知道了服务端有啥服务了,那么接下来它要调用这些服务了,但是有顺序的,接下来的节点是DESCRIBE。

1.DESCRIBE请求

请求报文如下
图13-1

可以参照live555mediaserver-如何解析rtsp请求报文把这请求报文解析出来。需要注意的是解析的只局限于请求方法、CSeq、Session、Content-Length等,它下发的其他字段就过滤了。

2.DESCRIBE请求与响应


图13-2

如上图,根据识别出的请求方法describe,就进入到了RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE方法。

拼接完整文件路径
首先是拼接完整url中的文件路径——相对于服务端程序的路径——url也可以写成rtsp://192.168.1.0/…/…/test.264等诸如此类的,因为它分两步,先识别…/…和test.264,然后在这个方法里把它们两个再用“/”拼接成…/…/test.264,你看就是相当于还原了url的路径。
当然写成rtsp://192.168.1.0/test.264这个,它在这个方法中就不会和“/”拼接了,如上图的380行是有过滤的,最后这个它会取到test.264这个文件名。

密码验证
接下来authenticationOK是密码验证,默认没有开,所以它默认返回true。

关键调用
接着它就调用了这么一句:fOurServer.lookupServerMediaSession(urlTotalSuffix, ESCRIBELookupCompletionFunction, this);

fOurServer保存的是DynamicRTSPServer对象的父类的父类GenericMediaServer的引用,而父类GenericMediaServer中有这个虚方法lookupServerMediaSession,
子类DynamicRTSPServer也有这个虚方法lookupServerMediaSession,
那么问题来了:父类子类里都有同名方法,该调用的谁的方法呢?
答案是子类的方法,即
DynamicRTSPServer::lookupServerMediaSession。

为何不是GenericMediaServer
的lookupServerMediaSession方法呢?这就涉及到c++的虚函数继承知识了。

c++虚函数继承
若父类有虚函数,派生的子类里也有同名函数,则创建子类调用其父类的虚函数则是子类的同名函数。为啥呢?这涉及到虚表和运行时虚表指针指向的问题。
先说虚表。 编译器编译阶段,如果父类有虚函数则对应一个虚表、派生的子类都会生成对应一个虚表。且虚表里各个虚函数偏移地址就固定了。如果父类有虚函数,那么派生的子类都会有虚表的,如果子类里没有同名的方法,子类虚表里的这个方法是父类的虚方法地址(也就是继承),如果子类里也有同名的方法,则子类的虚表里就存的是子类自己的同名方法(也就是派生、重写、覆盖)。
再说运行时。 创建一个对象(不管子类对象还是父类对象)对象第一个是vbptr指针就是指向哪个虚表的指针。这个指针指向的是当前创建的对象的虚表——创建父类对象就指向父类虚表,创建子类对象就指向子类对象。
如果父类子类都有相同的虚函数,那么父类子类的虚表里各有一个虚函数,但是如果创建的是子类对象,那么这个对象的虚表指针就指向子类的虚表(也就是它选择了子类的虚表)——因此,如果通过这个子类对象的父类指针或引用来调用这个同名的虚函数,那么这个子类对象就从子类虚表里找这个同名方法,调用的方法自然是子类的同名方法了——这给人的感觉就是你通过子类对象的父类指针或引用调用的虚函数竟然是子类的虚函数,这就是多态的原理。

DynamicRTSPServer父类的父类GenericMediaServer有一个虚函数lookupServerMediaSession,如下

图13-3

DynamicRTSPServer对象也有个虚函数lookupServerMediaSession

图13-4

因为DynamicRTSPServer是GenericMediaServer的派生,根据c++的虚表等编译器的规则和运行规则那么调用GenericMediaServer的方法lookupServerMediaSession其实就是调用DynamicRTSPServer的方法lookupServerMediaSession,因为你调用这个父类方法的前提是创建的是子类对象DynamicRTSPServer,此时这个对象的虚表指针指向(选择)的是DynamicRTSPServer的虚表。

虚函数形参初始值继承关系
另外还有一个知识点。GenericMediaServer方法lookupServerMediaSession的最后一个参数有初始值,而其派生的子类DynamicRTSPServer的方法lookupServerMediaSession最后一个参数却没有初始值,那么问题来了:派生类会继承父类的形参的初始值么?我也不知道,也很困惑,这咋办?说到这,通过交流,我受到启发,学习c++,总看书是不行的,看懂了知识点,你以为你懂了,但是让你写代码,你是写不出来的,学习设计模式也一样——看懂归看懂,写代码归写代码。看懂和会用之间是有距离的。 那怎么办?有句话叫“学以致用”,学和致用是有距离的。如何缩短这距离?写demo。
也就是说学c++、学设计模式、学算法等等,看懂是第一步,写demo是“致用”的有效手段。合上书,自己写demo,运行起来,这样才能把知识点有进一步的了解。——浅层的“致用”。
那么为了搞懂这个知识点,我就写demo验证下。如下图。

这是第一种情况,我把宏设为false了,这就是live555开源项目的实例demo——创建子类对象返回父类指针,再调用与子类同名的父类的虚方法,这个时候验证得出会继承父类虚函数形参的初始值的。所以
fOurServer.lookupServerMediaSession(urlTotalSuffix, ESCRIBELookupCompletionFunction, this);
只传递了3个形参,但它还是会匹配到GenericMediaServer有4个形参的方法lookupServerMediaSession,最后一个形参没有传,那就是初始值true。

这就可以结束了,但是如果好奇,可以更改下demo看下第2种情况,如下图

把main中的测试打开,创建子类对象返回子类对象指针,接着调用run方法,然后编译不通过,说没有匹配到2个形参的run方法,这是因为子类run方法里的第3个形参没有初始值,那它就匹配不到。

第3种情况,子类父类同名同参虚函数的同位置的形参都各自有初始值的情况,会怎么样?如图。

结果如下:
如果创建子类返回父类指针,再调用虚函数方法run,形参初始值是父类的。
如果创建子类返回子类指针,再调用虚函数方法run,形参初始值是子类的。

这3种情况,《c++ primer》不推荐用第3种,这太乱了,要用的话父类和子类同名虚函数形参初始值最好一样。否则也太花里胡哨了!

回来继续看下追踪到的这个方法DynamicRTSPServer::lookupServerMediaSession的实现:

图13-5

图13-5里的标记1-5是大致流程。它是流程简单,内容复杂。所以要一点一点地扣。也是比较惆怅怎么写。要不一个个标记的写?

图13-5的标记1

标记1是fopen打开url解析出的路径的文件,原来如此,所以url拼写规则就是
rtsp://ip:port/文件路径
其中,文件路径可以写…/…/test.264或直接test.264,或者./…/…/…/test.264。反正相对可执行文件来说的位置,想怎么写怎么写,只要能指向正确的文件路径就行。

图13-5的标记2

摘抄如下

从hash表里查找这个有没有这个路径,实现如下。

但是在说这个调用前,得知道fServerMediaSessions是个啥玩意儿?属于哪个对象的成员?它是DynamicRTSPServer对象的父类的父类GenericMediaServer的成员。它是个BasicHashTable对象的指针,对其进行揭秘,如下图:

图13-6 hash链表图(画对象图、链表图,我很兴奋,很幸福,爽!)

BasicHashTable对象管理了一个单向链表,这个单向链表是在头节点插入,也就是从头部插入的方式。
直接上来就讲图也是不好的,咋一看,太猛了,那么这个图咋来的呢?还得看初始化如下图。

怎么说呢,这个涉及到封装多态的概念。

封装多态
代码加图13-6可以看到fServerMediaSessions的诞生是调用BasicHashTable的父类HashTable的静态方法create来创建子类对象BasicHashTable再返回父类指针。这一过程,只能看到父类的方法,其实现是由子类实现的,这是封装。多态是因为看到父类的方法,子类负责实现,不同的子类实现是不一样的。——等下,这个好像不符合。算了就这样了,就当我胡说八道。

fServerMediaSessions的初始化可以解释图13-6中fStaticBuckets[0]到fStaticBuckets[3]的意思,它是个指针数组,最大是由宏SMALL_HASH_TABLE_SIZE定义的,这个宏是4,为何是4?唉,又得解释,因为后面hash算法是结果是0-3,于是就数组的各个成员都是链表头结点——各带领一个单向链表。
fBuckets是个二重指针,指向这个fStaticBuckets的第一个元素的地址。

我怎么知道是单向链表图,还是什么头结点插入?还得看下面图:

图13-7
164行和165行这个操作下来就是从头节点进行插入。164行是新队员指向头结点紧挨的队员(头结点保存紧挨的队员),165行头结点更改保存的地址为新队员的地址。如下图:

但是还有一点没有解释清楚,图13-6里的264/265等是啥玩意?其实就是说的url里最后是.264/.265等文件的所在队列。如下

图13-7

因为在add的时候,传入的key是url的文件路径,然后它要查看hash表里是不是已经有了,如果没有找到,那就创建新的TableEntry队员插入到单向链表里,但是要找到index,这个就在BasicHashTable::lookupkey里获取的,怎么获取的呢?如图13-7,BasicHashTable::hashIndexFromKey中搞的,怎么搞的?如图13-7,BasicHashTable::hashIndexFromKey中第270行和第272行是核心,就是左移3位加下一个字符了,最后和fMask掩码做与操作,fMask初始化时3呀,也就是最终值映射到0-3的区间。那个左移3位+下个字符的操作是啥意思呢?比如rtsp://192.168.15.22/test.264,经过前面一系列操作,它会取出key就是test.264,那么就是“test.264”这个几个字符相累加,但是每次都是累积和左移3位再加下一字符,其实等同于保留最后一个字符的低3位,在这就是“4”的字符值52,二进制是0b0110100的低3位是0b100,然后又和掩码0x3相与,结果就是0,毫无疑问,“test.264”所在队列的index就是0,那么也就是说要看最后一个字符的低2位是啥,那index就是啥,至此,图13-6的264/265等标记意思说完了。
在此总结下,各个url的文件是放在哪个链表队列里的:
264文件 对应索引: 0
265文件 对应索引: 1
aac文件 对应索引: 2
amr文件 对应索引: 2
dv文件 对应索引: 2
m4e文件 对应索引: 1
mkv文件 对应索引: 2
mp3文件 对应索引: 3
mpg文件 对应索引: 3
ogg文件 对应索引: 3
ts文件 对应索引: 3
vob文件 对应索引: 2
wav文件 对应索引: 2
webm文件 对应索引: 1

所以图13-5里我在头结点(各数组元素)里标注了264/265/aac/mp3,各头结点我都举个文件的例子,上面才是完整的各个文件的单向链表所在队列的头结点。

这个时候终于可以看下图13-5标记2的调用fServerMediaSessions->Lookup了,如下图:

图13-8

可以看到fServerMediaSessions->Lookup也是调用了BasicHashTable
::lookupKey,这个在图13-7里已经贴了,需要对图13-7
补充说明的是先找到index这个前面已经说过,然后在这个对应的数组成员带领的单向链表了遍历查找这个key,找到了就返回对应的TableEntry对象的指针,如果没找到就返回NULL。
当然这个时候这是第一次,里面还没有链表成员呢,肯定是NULL。
咦!你说我说了半天,这个图13-5的标记2最后返回的是NULL?我说,是呀,我没办法呀!

图13-5的标记3
如果文件不存在或者路径写错了,那么fopen就打不开文件,返回NULL了,则走图13-5的标记3,但是如果此时hash表里有这个文件那么就从hash表的单向链表队列里移除。
第一次是不会进这个的,但是后面如果不是第一次的话,从单向链表队列如何移除的呢?如下图:

图13-9 单向链表队员移除操作

上图最右边是单向链表队员移除的核心操作,比较绕,你细细品品,如下图,fNext是TableEntry * 类型的,这个对象开辟了块内存来存放它,自然fNext这个成员也是在内存中的,图13-9的第202行,ep = &((ep)->fNext)中ep就拿到了TableEntry对象的内存地址,然后取出fNext成员的内存地址,如下图TableEntry对象的fNext成员在内存中,所以也是有地址的,这句话就是定位到这个fNext成员的内存地址。接着图13-9的第199行,就是匹配到了这个要删除的链表队员,那么把当前这个链表队员的fNext的值要改成删除队员的下一个链表队员。

怎么说呢,举个例子,单向链表A->B->C->D,我要删除C,那么搞个指针变量指向B的时候,判断下B的成员fNext保存的地址是不是C的地址,如果是就把B的fNext成员变量更新为D的地址,这就自然从单向链表中删除了队员C。上面的操作就是这个思路。

图13-5的标记4
如果文件存在,则走的标记4,如下。

第1个if是文件在hash表存在就移除,就是个容错判断,刚开始hash表就有这个是不对的。这个移除操作和图13-5标记3一样,前面已分析过,不再赘述。

第2个if可是DynamicRTSPServer::lookupServerMediaSession的核心操作哦。
主要是干2件事:
(1)调用createNewSMS创建ServerMediaSession对象。

strrchr是取指定字符在字符串最后一个匹配的位置,比如test.264取“.”,那么它会找到最后个.的位置,也就是“.264”。
接着创建对象ServerMediaSession对象,将其指针返回出去。
然后创建264/265等等的具体对象,都是类FileServerMediaSubsession派生出的子类,而类FileServerMediaSubsession都是继承于类ServerMediaSubsession,类ServerMediaSubsession是继承于类Medium。
以H264VideoFileServerMediaSubsession为例如下继承关系。

sms->addSubsession(ServerMediaSubsession* subsession)形参是这样类型的,这就涉及多态了,不同文件都会创建子类对象,返回其父类对象ServerMediaSubsession的指针,然后addSubsession加入到ServerMediaSession对象中。

注意,这一步是创建2个对象——ServerMediaSession和264VideoFileServerMediaSubsession,但是呢,ServerMediaSession这个对象通过addSubsession把264VideoFileServerMediaSubsession给装到自己的成员变量里了。
如下图

ServerMediaSession::addSubsession这个操作也是加入链表,如上图ServerMediaSession的fSubsessionsHead和fSubsessionsTail成员指向了H264VideoFileServerMediaSubsession对象的父类ServerMediaSubsession地址,我先这么简单画,它这个也是单向链表,只是有头尾指针。

(2)RTSPServer::addServerMediaSession加入到hash单向链表中。
上面2个对象创建完,通过RTSPServer::addServerMediaSession又把ServerMediaSession对象给装走了,装到哪里去了呢?就是13-6的单向链表了。如下代码

代码的图形化如下:

fServerMediaSessions->Add前面已经讲过,可以看到插入单向链表时,它把ServerMediaSession对象指针放到链表成员对象TableEntry的成员变量value里了,前面我们知道key是文件路径。后面就可以通过这个链表成员对象TableEntry来找到上面创建的2个对象了。
真是一层一层又一层。——如果代码不能图形化,真是好难理解。

图13-5的标记5
图13-5的标记5如下

又掉用了个回调函数。
其中回调函数completionFunc是RTSPServer::RTSPClientConnection
::DESCRIBELookupCompletionFunction。
形参completionClientData是RTSPServer::RTSPClientConnection对象的this指针,
形参sms是上面说的创建的ServerMediaSession对象指针。

也就是说图13-2的调用fOurServer.lookupServerMediaSession最后的调用就是调用传进去的回调,看其跳的轨迹:

可以看到,这就像一个跳板一样,又回到了RTSPServer::RTSPClientConnection
::DESCRIBELookupCompletionFunction——它也是个静态方法,所以调用它必须得传给它对象的this指针才行呀。
最终呀,是调用的RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE_afterLookup——这个也是describe响应报文最终组织的地方。

describe响应报文最终组织的地方
来看下这个describe响应报文最终组织的地方:RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE_afterLookup,如下图:

图13-18

图13-18是最终处理describe的地方——组sdp、组响应报文。

组sdp信息
待续…

组url
fOurRTSPServer.rtspURL这个调用其实就是调用RTSPServer::rtspURL,这个它组url,形式是rtsp://ip:port/文件路径,其中ip:port是直接找本地的ip和监听端口,文件路径是用的RTSPServer::RTSPClientConnection对象客户端带的。
我以为它会直接使用RTSPServer::RTSPClientConnection对象解析的url呢,没想到它是这样的。

组响应报文
待续…

流程梳理

整个流程梳理下,可以知道RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE中调用fOurServer.lookupServerMediaSession,即DynamicRTSPServer::lookupServerMediaSession,它最终调用了RTSPServer::RTSPClientConnection::DESCRIBELookupCompletionFunction。
这发现一个特点:RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE和RTSPServer::RTSPClientConnection::DESCRIBELookupCompletionFunction都是RTSPServer::RTSPClientConnection对象的方法,中间的fOurServer.lookupServerMediaSession只是一个“转发”——相当于一个跳板——又跳回到原来对象的另一个方法了。
那RTSPServer::RTSPClientConnection::handleCmd_DESCRIBE中为何不直接调用RTSPServer::RTSPClientConnection::DESCRIBELookupCompletionFunction,而非得经过DynamicRTSPServer对象的lookupServerMediaSession方法呢?从DynamicRTSPServer::lookupServerMediaSession这个方法中我发现原来跳回来的时候RTSPServer::RTSPClientConnection::DESCRIBELookupCompletionFunction的形参需要调用createNewSMS来获取,而且要用DynamicRTSPServer对象来记录它,所以要这样中间跳一层。
这就比如有A和B两个对象,B对象的方法1要调用B的另一个方法2,但是方法2的某个形参需要用到A对象的方法来获取,那这怎么办呢?live555是这么设计的:A为B开了个专门的转换接口。在这里A对象就是DynamicRTSPServer,B对象就是RTSPServer::RTSPClientConnection,A对象里的转换接口就是DynamicRTSPServer::lookupServerMediaSession——也是跳板。其中跳回来的B对象的方法2是静态方法——RTSPServer::RTSPClientConnection::DESCRIBELookupCompletionFunction——为何是静态方法?因为A对象实现的转换接口是直接调用方法指针的而不是对象指针->方法的形式。

可以看到A对象转换接口返回值——ServerMediaSession对象指针——在这里大有用处:获取rtspURL,来放到响应buffer里。
待续…

小结

describe作为rtsp协商的必经节点还是比较复杂的。它这个主要是查找url中解析出的本地文件是否存在,如果存在就解析,组sdp,最后发给客户端。是什么文件类型。为接下来的节点做铺垫。
另外hash单向链表这个是老早初始化完的,但是前面很多节都没有说,因为和我的业务关注点无关,只有用到才去说,我不是胡子眉毛一把抓,不然很难学的,学到哪再去看。

待续…

13.live555mediaserver-describe请求与响应相关推荐

  1. python的Web框架,Django框架中的请求与响应

    请求与响应 简单流程图 我们先来了解一个请求与响应的大概流程 视图函数接受到的request到底是个什么对象呢? 服务器接收到http协议的请求后,会根据报文创建HttpRequest对象视图函数的第 ...

  2. Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改

    Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改 前提 本文编写的时候使用的Spring Cloud Gateway版本为当时最新的版本Gr ...

  3. curl 请求日志_HTTP入门(一):在Bash中curl查看请求与响应

    HTTP入门(一):在Bash中curl查看请求与响应 本文简单总结HTTP的请求与响应. 本文主要目的是对学习内容进行总结以及方便日后查阅. 详细教程和原理可以参考HTTP文档(MDN). 本文版权 ...

  4. 【Go】优雅的读取http请求或响应的数据-续

    原文链接:https://blog.thinkeridea.com/201902/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju_2 ...

  5. [Python爬虫] 一、爬虫原理之HTTP和HTTPS的请求与响应

    一.HTTP和HTTPS HTTP协议(HyperText Transfer Protocol,超文本传输协议):是一种发布和接收 HTML页面的方法. HTTPS(Hypertext Transfe ...

  6. JavaWeb | HTTP 协议请求与响应格式

    一.HTTP 是什么 计算机网络核心概念:网络协议 网络协议种类非常多,其中一些耳熟能详的,IP,TCP,UD- 其中还有一个应用非常广泛的协议HTTP,HTTP 协议大概率是咱们日后开发中用的最多的 ...

  7. HTTP协议、【HTTP请求、响应格式】及一次HTTP请求的完整过程

    HTTP协议及一次[请求.响应]的完整过程 HTTP协议简介 HTTP协议工作原理 一次HTTP请求的完整过程 浏览器根据域名解析IP地址 浏览器通过IP地址与WEB服务器建立一个TCP连接 浏览器给 ...

  8. 深入操作系统底层分析nginx网络请求及响应过程

    0. 网络传输阶段 比如说主机A是家里windows的一台笔记本电脑,主机B是linux服务器上的一个nginx,其监听80或443等web端口. 在笔记本的浏览器发送了一个http get请求,其数 ...

  9. HTTP常见请求头/响应头

    一.常用的http请求头 1.Accept Accept: application/json  浏览器可以接受服务器回发的类型为 application/json. Accept: */*   代表浏 ...

最新文章

  1. CentOS6.*安装gitolite
  2. BLE-NRF51822教程4-串口BLE解析
  3. usestate中的回调函数_React 中获取数据的 3 种方法:哪种最好?
  4. flutter 日志输出,Flutter打印日志,flutter log,flutter 真机日志
  5. mybatis基于XML(二)
  6. python 中 print 函数用法总结
  7. python基础二:函数
  8. [CF453A] Little Pony and Expected Maximum【数论】
  9. CRC-16的原理和实现
  10. 1.中小型企业通用自动化运维架构 -- 自动化运维流程
  11. 编码之Base64编码
  12. win10u盘被写保护怎么解除_教你win10系统中u盘被写保护怎么解除
  13. 电流(或电压)的平均值与有效值
  14. 速学Sql Server从基础到进阶
  15. Ring Buffer介绍
  16. 生活美学 | 8种咖啡冲煮器具分别有什么特点
  17. [BZOJ4466][Jsoi2013]超立方体
  18. java调用kettle脚本ktr
  19. 音乐、音效素材库,好听的BGM都在这~
  20. 【spark使用】4. Dataset转换算子使用

热门文章

  1. getElementById( ) 方法
  2. Spring Boot学习笔记----mybatis注解(一)
  3. html photoswipe原理,手机图片预览插件photoswipe.js使用总结
  4. CSS学习笔记10:超链接样式
  5. 专心技术,拒绝浮躁,静下心来,做一个有创造力的coder
  6. 了解维客模式wiki,联想《集思广益系统》
  7. RGB565 to RGB24
  8. Linux定时 (计划) 任务
  9. 从12个球中找出唯一一个质量不同的球,并说明轻重
  10. P3628 [APIO2010]特别行动队