14.live555mediaserver-setup请求与响应

live555工程代码路径
live555工程在我的gitee下(doc下有思维导图、drawio图):
live555
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.绘图规则

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

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

本节内容和目标
(1)rtsp协议的setup请求与响应
(2)思维导图绘制
(3)wireshark抓包
(4)对象图
(5)延时任务管理双向循环链表探索

正式开始
DESCRIBE协商完事,VLC接着下发了setup信令,开始setup协商。

1.客户端SETUP请求报文

请求报文如下
注意,setup会下发2个,现在从上节describe结尾url升级成音视频格式的了:
rtsp://192.168.145.1/…/…/…/test/backkom.mkv。
所以describe响应报文sdp里有2个m媒体描述,所以会下发2次setup。如下

14.live555mediaserver-setup请求与响应_第1张图片
14.live555mediaserver-setup请求与响应_第2张图片
图14-1

可以参照live555mediaserver-如何解析rtsp请求报文把这请求报文解析出来。需要注意的是解析的只局限于请求方法、CSeq、Session、Content-Length等,它下发的其他字段就过滤了。
需要关注的是,这里服务端会用来识别发送模式——UDP or TCP?——这个字段是Transport字段——如果是RTP/AVP/TCP,则要求采用TCP方式进行数据传输。如果是RTP/AVP或者RTP/AVP/UDP那么就是UDP。

从上图可以发现以下不同:
(1)第1次setup没有Session字段,因为需要服务端返回,第2次有了,就是第1次服务端返回的字段。
(2)url最后不同,一个track1一个track2,这个是因为describe的sdp里面有2个m字段,给出了track1是指视频,track2是指音频。
(3)interleaved字段。 interleaved不同,track1指定0-1,track2指定2-3,啥意思?rtp over tcp才会有这个字段,指定RTP数据和RTCP数据的通道号——rtp/rtcp/rtsp共用同一个socket,怎么区分数据是rtp/rtcp/rtsp呢?就根据这个指定的。rtp over udp的是client_port字段,指定客户端的socket接受rtp和rtcp的端口号。

参见从零开始写一个RTSP服务器(一)RTSP协议讲解
https://blog.csdn.net/weixin_42462202/article/details/98986535

2.第一次setup

根据前面知道,每一个客户端链接,在服务端都绑定一个对象RTSPServer::RTSPClientConnection,每次客户端协议数据来了,都会调用到RTSPServer::RTSPClientConnection::handleRequestBytes里,如下图目前知道OPTIONS、DESCRIBE、REGISTER、DEREGISTER、特殊url的GET_PARAMETER和SET_PARAMETER是会在这个对象里处理。
14.live555mediaserver-setup请求与响应_第3张图片
图14-2

2.1 新对象RTSPServer::RTSPClientSession的创建

第1次setup时,才会新建新对象RTSPServer::RTSPClientSession,来负责之后的协议处理了。调用流程代码图如下。
14.live555mediaserver-setup请求与响应_第4张图片
RTSPServer::RTSPClientConnection::handleRequestBytes识别到是setup信令后,执行fOurRTSPServer.createNewClientSessionWithId,其中fOurRTSPServer是DynamicRTSPServer对象的父类RTSPServer的引用,且是第1次setup时,才会走这个创建流程,其代码图如下:
14.live555mediaserver-setup请求与响应_第5张图片

标记1:Session的产生
代码图中标记1中sesionId是随机产生的数字,然后和新对象绑定一起,在组响应包时会作为Session字段返回给VLC客户端的,play时vlc又会下发过来,用以找到正确的新对象的。

标记2和标记3:新对象RTSPServer::RTSPClientSession的产生
对应的对象流动图如下图14-3:

14.live555mediaserver-setup请求与响应_第6张图片
图14-3

如图14-3所示,结合代码图和对象图学习下,它主要干2件事:
一个是创建对象RTSPServer::RTSPClientSession
另一个是把这个对象加入到GenericMediaServer::fClientSessions这个成员管理的hash链表了。
第2和第3标记执行完就是如下图的14-4和14-5的模样。
14.live555mediaserver-setup请求与响应_第7张图片
14-4

14.live555mediaserver-setup请求与响应_第8张图片

图14-5

整个图太大,只能截断成14-4和14-5——结合看,或者可以到我gitee上live555工程doc下的对象图。
如图14-4和14-5,因为和上一节describe学习的GenericMediaServer::fServerMediaSessions是同类型的hash链表,就直接拷贝过来了,只是链表成员不一样的——setup把新创建的对象RTSPServer::RTSPClientSession的父类的指针加入到这个链表里的value里了。注意代码图的标记3在add时把sesionid传给了这个链表成员的key值,后面play下发会再次通过这个sessionid进行查找到这个新对象。
创建这个新对象RTSPServer::RTSPClientSession的思维导图如下
在这里插入图片描述

需要指出的是,新对象RTSPServer::RTSPClientSession构造时,其父类的构造函数GenericMediaServer::ClientSession::ClientSession里会调用GenericMediaServer::ClientSession::noteLiveness()。如下代码图
14.live555mediaserver-setup请求与响应_第9张图片
这里就是最终的结果就是放到延时函数管理去,如果超时了就执行超时函数。

fOurServer.fReclamationSeconds
fOurServer是DynamicRTSPServer父类的父类GenericMediaServer的引用, fReclamationSeconds是65,单位是s,因为创建DynamicRTSPServer对象时,其creatNew方法的默认参数如下
在这里插入图片描述
怎么一层层传入的过程就不说了,无非是父类构造函数一层层的调用传递过去的,比较简单。

envir().taskScheduler().rescheduleDelayedTask
接着就调用envir().taskScheduler().rescheduleDelayedTask把延时时间和超时函数、形参(这里是this指针)通知延时管理函数,这是对外接口,内部的操作是创建双向循环链表的队员并根据延时时间找到位置插入这个双向循环的延时链表中。
其调用链路如下:
14.live555mediaserver-setup请求与响应_第10张图片
里面的各个函数下面再学,先把最终结果呈现如下:
14.live555mediaserver-setup请求与响应_第11张图片
它是个双向循环链表,上图有3个队员——最右边的是链表头,从左到右是延时时间从小到大排序,我们这里延时65s是插入的第2个链表队员,所以在中间——链表头是个很大的延时值顶格了(延时68年)。

2.2 探秘延时任务管理

这里延时任务管理进入了视野,那么来探索下延时任务管理的来龙去脉。

2.2.1 延时任务管理思路

它的思路是
(1)搞个双向循环链表管理各成员。各队员根据延时时间从小打到大排序。
(2)计算相对超时时间。但超时时间是按照相对前一个队员的延时时间计算出的相对延时时间。

各情况:
(1)最开始初始化创建链表头,它的延时时间是给的很大值,顶格给的。

(2)假设时间过了几us,然后插入了一个延时任务,延时时间是10ms。
先同步再插入,为啥先同步,因为这个时候时间过去了多久,链表队员的时间得刷新下吧。链表头的相对延时时间会重新计算:会减去几us,因为这个时候过了几us,。

那么假设我把链表头放到了最右边,那么它插入后的链表是:
10ms ——> 链表头

因为相对当前的时间是10ms所以它插入进去就是10ms。
这是时候链表头又会重新计算它的相对延时时间再减去10ms,减去10ms是前面一个延时是10ms,你相对前面的延时是不是得减去?

(3)接着,假设时间又过了5ms,又插入了一个延时65s的任务。
这个时候,先同步这个链表的时间。扫描这个链表头的next就是最左边的第1个成员,它设置是10ms延时,而此时呢,过去了5ms,那是不是得减去5ms呢?而第1个成员还么有超时,后面都是相对它的延时时间,就不用动了。想想是不是?
于是同步后的链表的超时时间变成
5ms ——> 链表头

然后再插入65s这延时任务,它大于5ms这第1个延时任务了,就要放到它后面,但是远远小于链表头的。插入后,要计算它相对前一个的相对延时,它这个不是绝对延时,那就是65s-5ms,也就是64s995ms。于是链表就是这样的:
5ms ——> 64s995ms ——>链表头

(4)假设时间又过去了5ms,又要插入一个100s的延时任务。
同样先同步,链表如下模样:
0ms ——> 64s995ms ——>链表头

然后插入这个100s任务,这100s肯定要放到64s995ms这个任务后面的,但是相对时间怎么计算呢?第1个队员时0ms也就是超时时间到了,100s-0ms,然后呢,还剩100s,那么就和第2个队员的延时时间比较,肯定大于第2个队员的相对延时时间,那么相对延时时间是100s-64s995ms = 35s5ms,这就是计算出了相对时间,35s5ms再和链表头比较肯定不行,于是位置确定了,相对延时时间也计算出来了。插入后的链表模样:
0ms ——> 64s995ms ——>35s5ms ——>链表头

就这样,各种情况就这样,至于超时了在哪检查,这个也很简单,在一个线程里检查就行了。
这就是它的整体思路。用的是相对延时时间,我记得rtthread这个RTOS采用的绝对延时时间,和这个还不一样,但是异曲同工。

相对延时也可以的,都是实现超时处理的定时器功能,对于使用者来说,只要调用对应接口把延时、形参(this指针)和超时函数传入进去就行了,其他不用管,如下接口是插入延时任务的接口。

void TaskScheduler::rescheduleDelayedTask(TaskToken &task,
					  int64_t microseconds, TaskFunc *proc,
					  void *clientData) 
{
    unscheduleDelayedTask(task);
    task = scheduleDelayedTask(microseconds, proc, clientData);
}

或者

 BasicTaskScheduler0::scheduleDelayedTask(int64_t microseconds,
						 TaskFunc* proc,
						 void* clientData)
2.2.2 延时任务管理对象fDelayQueue

前面说了双向循环链表(还直接给出了链表图——倒序手法、或先给结果,现在要正序或详解),那这个链表在哪里呢?谁来管理呢?在这里:
BasicTaskScheduler的父类BasicTaskScheduler0的成员fDelayQueue是延时任务管理的所在——这涉及到BasicTaskScheduler对象的创建过程,live555mediaserver.cpp中main函数中首先创建的是BasicTaskScheduler对象,前面章节没有探索这个延时任务服务,是因为还没有真正用上,所以就不探索。
其所在类对象成员如下代码图.
14.live555mediaserver-setup请求与响应_第12张图片
其对应的对象图是什么样的呢?脑袋里图形化下:
14.live555mediaserver-setup请求与响应_第13张图片

fDelayQueue是直接在BasicTaskScheduler0类里定义了一个类DelayQueue对象作为成员,没有显式地调用它的构造函数,但是加打印看创建BasicTaskScheduler对象时fDelayQueue会自动调用构造函数初始化,其构造函数调用如下图。
在这里插入图片描述
14.live555mediaserver-setup请求与响应_第14张图片
14.live555mediaserver-setup请求与响应_第15张图片
tokenCounter是静态成员。如下
14.live555mediaserver-setup请求与响应_第16张图片
在这里插入图片描述
tokenCounter静态成员初始值是0。也就是说每创建一个DelayQueueEntry对象或派生的子类对象这个计数加1——因此创建链表头fDelayQueue的fToken是1。
另外其延时时间是个很大的值,如下图。
14.live555mediaserver-setup请求与响应_第17张图片
0x7FFFFFFF是2147483647s,算了下,是68年,我操,你能让他一直运行68年么?所以这个链表头超时后的处理是什么呢?是删除它自身。如下

void DelayQueue::handleAlarm()
{
    if (head()->fDeltaTimeRemaining != DELAY_ZERO) synchronize();

    if (head()->fDeltaTimeRemaining == DELAY_ZERO) 
    {
        // This event is due to be handled:
        DelayQueueEntry* toRemove = head();
        removeEntry(toRemove); // do this first, in case handler accesses queue

        toRemove->handleTimeout();
    }
}
void DelayQueueEntry::handleTimeout() 
{
    delete this;
}

因为链表头DelayQueue类没有重写handleTimeout函数,所以调用的是它父类的handleTimeout函数,它父类的这个函数就是delete自身——不过得一直运行68年之后才会触发这个,你说你也不可能让它一直运行68年后吧?68年后人还在不在?

来看下链表头fDelayQueue初始化的对象图长啥样。
14.live555mediaserver-setup请求与响应_第18张图片

为啥会长这个样子?从构造函数看到的,代码就是这样写的,只不过图形化了而已,这都不认识了?

后面会看到链表头和链表队员不是同一类但都继承自DelayQueueEntry,为什么呢? 因为链表头的延时值是个很大的值,而且链表头是负责管理的,还有管理接口函数,所以就采用了同父类不同子类的这个操作——后面看下链表队员就知道了。

2.2.3 任务延时双向循环链表插入

前面有个图是3个链表对象,因为在BasicTaskScheduler对象的创建过程中插入了一个最早的成员如下图
14.live555mediaserver-setup请求与响应_第19张图片
live555mediaserver.cpp中main函数中创建BasicTaskScheduler对象时BasicTaskScheduler::createNew()没有传参,那么就是默认的参数10000,单位是us,即10ms。然后new BasicTaskScheduler,然后调用构造函数BasicTaskScheduler::BasicTaskScheduler(其他父类构造函数暂不管),会调用BasicTaskScheduler::schedulerTickTask()然后调用BasicTaskScheduler0::scheduleDelayedTask,然后在延时任务链表里加入一个队员了,如下图:

14.live555mediaserver-setup请求与响应_第20张图片

可以看到它的队员是AlarmHandler类的,但是和链表头一样都是继承自DelayQueueEntry。
再看下它们的差异之处:
14.live555mediaserver-setup请求与响应_第21张图片
链表头里有很多管理方法,但是没有重写父类DelayQueueEntry的handleTimeout方法。
链表队员除了构造、析构外,只有一个方法handleTimeout——重写了父类DelayQueueEntry的handleTimeout方法。

插入方法DelayQueue::addEntry如下。


void DelayQueue::addEntry(DelayQueueEntry* newEntry) 
{
    synchronize();

    DelayQueueEntry* cur = head();
    while (newEntry->fDeltaTimeRemaining >= cur->fDeltaTimeRemaining) 
    {
        newEntry->fDeltaTimeRemaining -= cur->fDeltaTimeRemaining;
        cur = cur->fNext;
    }

    cur->fDeltaTimeRemaining -= newEntry->fDeltaTimeRemaining;

    // Add "newEntry" to the queue, just before "cur":
    newEntry->fNext = cur;
    newEntry->fPrev = cur->fPrev;
    cur->fPrev = newEntry->fPrev->fNext = newEntry;
}

先调用同步方法DelayQueue::synchronize()然后更新各个链表的相对当前的剩余延时时间其源码如下

void DelayQueue::synchronize() 
{
    // First, figure out how much time has elapsed since the last sync:
    _EventTime timeNow = TimeNow();
    if (timeNow < fLastSyncTime) 
    {
        // The system clock has apparently gone back in time; reset our sync time and return:
        fLastSyncTime  = timeNow;
        return;
    }
    DelayInterval timeSinceLastSync = timeNow - fLastSyncTime;
    fLastSyncTime = timeNow;

    // Then, adjust the delay queue for any entries whose time is up:
    DelayQueueEntry* curEntry = head();
    while (timeSinceLastSync >= curEntry->fDeltaTimeRemaining) 
    {
        timeSinceLastSync -= curEntry->fDeltaTimeRemaining;
        curEntry->fDeltaTimeRemaining = DELAY_ZERO;
        curEntry = curEntry->fNext;
    }
    curEntry->fDeltaTimeRemaining -= timeSinceLastSync;
}

参见2.2.1 思路的同步——先计算这次更新距离上次插入链表或创建链表头的时间流逝了多少us,然后比较链表头指向的next链表队员的剩余延时时间如果没有超时,则只更新这个队员就行了,否则说明其超时了,这个时候要看这个next的next的队员看是否也超时了,和之前一样重复的操作。——总之,这个调用完各个链表的剩余延时时间呀就相对当前时间进行了更新。其中head是:
在这里插入图片描述
注意,这些操作是在这个对象fDelayQueue身上进行的操作(调用),而它是寄身于对象BasicTaskScheduler的父类BasicTaskScheduler0下的。

2.2.4 延时任务的撤销

撤销是这个接口BasicTaskScheduler0::unscheduleDelayedTask

void BasicTaskScheduler0::unscheduleDelayedTask(TaskToken& prevTask) 
{
    DelayQueueEntry* alarmHandler = fDelayQueue.removeEntry((intptr_t)prevTask);
    prevTask = NULL;

    printf("delete alarmHandler %p TaskToken %d\n", alarmHandler, (intptr_t)prevTask);
    delete alarmHandler;
}
DelayQueueEntry* DelayQueue::removeEntry(intptr_t tokenToFind) {
  DelayQueueEntry* entry = findEntryByToken(tokenToFind);
  removeEntry(entry);
  return entry;
}

DelayQueueEntry* DelayQueue::findEntryByToken(intptr_t tokenToFind) 
{
    DelayQueueEntry* cur = head();
    while (cur != this) 
    {
        if (cur->token() == tokenToFind) return cur;
        cur = cur->fNext;
    }

    return NULL;
}

class DelayQueueEntry 
{
public:
    virtual ~DelayQueueEntry();

    intptr_t token() 
    {
        return fToken;
    }
void DelayQueue::removeEntry(DelayQueueEntry* entry) 
{
    if (entry == NULL || entry->fNext == NULL) return;

    entry->fNext->fDeltaTimeRemaining += entry->fDeltaTimeRemaining;
    entry->fPrev->fNext = entry->fNext;
    entry->fNext->fPrev = entry->fPrev;
    entry->fNext = entry->fPrev = NULL;
  // in case we should try to remove it again
}

如上可知,每个任务链表队员 的fToken都是唯一的,所以根据fToken就能找到这个对象,然后从这个双向循环链表中移除的。注意,移除了它,后面的队员得加上它剩余得相对延时时间。

2.2.5 延时任务的调度

这个延时任务双向循环链表插入成员了,在哪检查超时和调用超时函数呢?在BasicTaskScheduler::SingleStep里,截取下和延时函数管理相关的代码如下图:
14.live555mediaserver-setup请求与响应_第22张图片
和其思维导图:
14.live555mediaserver-setup请求与响应_第23张图片

BasicTaskScheduler::SingleStep由live555mediaserver.cpp的main来驱动的,是个无限循环的,也就是由主函数main(主线程)来驱动的。
它是先调用DelayQueue::timeToNextAlarm() ,

DelayInterval const& DelayQueue::timeToNextAlarm() 
{
  if (head()->fDeltaTimeRemaining == DELAY_ZERO) return DELAY_ZERO; // a common case

  synchronize();
  return head()->fDeltaTimeRemaining;
}

timeToNextAlarm就是取出第1个链表队员的剩余延时时间,然后作为select超时时间,为啥这么搞?——这样不就兼顾了延时函数处理和select的IO多路复用功能,这不是一石二鸟,都不耽误,太聪明了!
但是感觉这个同步怎么会在中间?就是第1个队员超时了就不进行同步了?这个,也没问题,下次再同步一样,反正都会记录流逝的时间的。

void DelayQueue::handleAlarm()
{
    if (head()->fDeltaTimeRemaining != DELAY_ZERO) synchronize();

    if (head()->fDeltaTimeRemaining == DELAY_ZERO) 
    {
        // This event is due to be handled:
        DelayQueueEntry* toRemove = head();
        removeEntry(toRemove); // do this first, in case handler accesses queue

        toRemove->handleTimeout();
    }
}

在这里,注意,链表队员是AlarmHandler类的,继承自DelayQueueEntry ,它重写了DelayQueueEntry 的handleTimeout方法,在这里可以看到用到了c++的多态——碰到链表队员,它的调用是子类AlarmHandler的handleTimeout方法——执行超时函数,如下

class AlarmHandler: public DelayQueueEntry 
{

private: // redefined virtual functions
    virtual void handleTimeout() 
    {
        (*fProc)(fClientData);
        DelayQueueEntry::handleTimeout();
    }

总结来说,怎么说呢?网上也有说的,可以参考,比如
3 live555源码分析(三)——live555 任务调度。
我和他们的区别是:
(1)把详细的类图画出来了——代码图形化了。
(2)与业务相结合——这很关键。上面的文章讲了任务对象的IO调度、事件触发和这个延时任务调度,其中IO的话在前面网络编程线索已经学习过,今天又学习这个延时任务,为啥不一下子全看完?——答案是“没有用到”,——没有用到很关键,这是和业务相结合的关键特性——不是一上来眉毛胡子一把抓——这往往学不动,不知其所以然——它这个玩意诞生的目的是啥?不知道——直接上了就学没有来龙去脉没有业务需求作为导向,就是无根之树,没有安全感。
他这篇适合后面我写总结的章节。
同时和业务相结合的特点这就注定类图的更新是以业务为导向,根据业务的出现而慢慢扩展“地图疆域”。

在这里可以知道,第1次setup业务有个65s的限制(其实是每次),在65s内如果没有完成这个操作,服务端就会删除新对象并断开链接啦。65s内完成第一次setup操作,会调用撤销任务的操作,如何撤销?请看下面的第3节第2次setup操作

2.3 调用新对象的SETUP方法

以上是延时任务管理链表管理。回过头来再看流程,这是要调用新对象RTSPServer::RTSPClientSession的handleCmd_SETUP方法了
14.live555mediaserver-setup请求与响应_第24张图片
14.live555mediaserver-setup请求与响应_第25张图片
在这里插入图片描述
14.live555mediaserver-setup请求与响应_第26张图片

handleCmd_SETUP方法又调用GenericMediaServer::lookupServerMediaSession,而它是个虚方法,实际调用的是DynamicRTSPServer::lookupServerMediaSession(原因参见上一节)。然后查找fServerMediaSessions管理的hash链表是否有这个url文件路径,因为describe已经插入了,在这里找到了,又移除它,又重新创建,具体原因,没有细看。
这个最后又会回调RTSPServer::RTSPClientSession的静态方法SETUPLookupCompletionFunction1,SETUPLookupCompletionFunction1像个跳板一样,跳入到新对象的handleCmd_SETUP_afterLookup1方法,然后最终来到了setup组响应报文的地方——handleCmd_SETUP_afterLookup2。
其流程如下图14-6.
14.live555mediaserver-setup请求与响应_第27张图片
图14-6

如图14-6数字1是调用GenericMediaServer::lookupServerMediaSession的流向,太大了截图放不下,算了。数字2是又回调回来了。最终是数字4来到了handleCmd_SETUP_afterLookup2。
其主要干了啥事,如下思维导图,图14-7.
14.live555mediaserver-setup请求与响应_第28张图片

图14-7

创建对象数组 struct streamState 保存各个ServerMediaSubsession对象,这个保存的就是前面创建的ServerMediaSubsession对象。RTSPServer::RTSPClientSession::handleCmd_SETUP_afterLookup2代码片段图如下:
在这里插入图片描述

14.live555mediaserver-setup请求与响应_第29张图片
在这里插入图片描述
到这里,插一句,

2.4 调用getStreamParameters方法创建很多关键新对象

接着又调用了OnDemandServerMediaSubsession::getStreamParameters方法,如下:
在这里插入图片描述
14.live555mediaserver-setup请求与响应_第30张图片
14.live555mediaserver-setup请求与响应_第31张图片
14.live555mediaserver-setup请求与响应_第32张图片
在这里插入图片描述

OnDemandServerMediaSubsession::getStreamParameters创建了好几个新对象,一执行,代码图对应的对象图如下图14-8。

14.live555mediaserver-setup请求与响应_第33张图片

图14-8

如图14-8图中上面2个红色虚框里就是getStreamParameters方法执行后创建的视频新对象,不要小看这个方法,仔细看才发现内容这么多,一堆新对象诞生了,为后续的rtp流数据创建了基础服务框架。下面音频是第2次setup创建的音频的对象。

2.5解析传输方式等

接着是解析协商传输方式,如下图14-9。
14.live555mediaserver-setup请求与响应_第34张图片
图14-9
streamingMode这引用变量默认是RTP_UDP。while来解析RTP_TCP和RAW_UDP,而我们的VLC下发的setup信息如图14-1中是RTP/AVP/TCP,这里自然就是RTP_TCP了。

2.6组SETUP响应包

然后根据根据不同的传输方式组不同的setup响应报文。
14.live555mediaserver-setup请求与响应_第35张图片
图14-10
注意呀,里面除了传输方式,还有个字段非常关键——Session字段,这个就是前面看到的sessionid了,这个告诉VLC客户端,你等会play的时候下发的Session字段应该填什么,这样服务端才能找到新对象RTSPServer::RTSPClientSession。

到此整个setup响应流程完毕。setup的完整处理思维导图如下图14-11.
14.live555mediaserver-setup请求与响应_第36张图片
图14-11。

其组装的setup响应报文用wireshark抓包如下图14-12.。

第1次setup响应
14.live555mediaserver-setup请求与响应_第37张图片

图14-12

3.第2次setup

3.1 如何撤销第1次setup的65s的定时任务呢?

关键接力棒接口:GenericMediaServer::ClientSession::noteLiveness

后头看下,第1次setup时是在新对象RTSPServer::RTSPClientSession的父类构造函数GenericMediaServer::ClientSession::ClientSession里进行调用的。第2次setup,客户端把session下发下来,服务端根据这个字段就能找到第1次setup服务端创建的RTSPServer::RTSPClientSession对象。然后呢?然后如下图
14.live555mediaserver-setup请求与响应_第38张图片
它继续调用了
定时任务的接力棒接口
GenericMediaServer::ClientSession::noteLiveness,
为啥叫接力棒?因为第1次setup设置的65s定时任务就在这里撤销,然后又接着接力地加入新的65s的定时任务。哎呀,真的是。
于是,我加了打印,代码如下

void GenericMediaServer::ClientSession::noteLiveness() 
{
#ifdef DEBUG
  char const* streamName
    = (fOurServerMediaSession == NULL) ? "???" : fOurServerMediaSession->streamName();
  fprintf(stderr, "GenericMediaServer::ClientSession::noteLiveness session (id \"%08X\", stream name \"%s\"): Liveness indication\n",
	  fOurSessionId, streamName);
#endif
  if (fOurServerMediaSession != NULL) fOurServerMediaSession->noteLiveness();

    if (fOurServer.fReclamationSeconds > 0) 
    {
        envir().taskScheduler().rescheduleDelayedTask(fLivenessCheckTask,
        					  fOurServer.fReclamationSeconds*1000000,
        					  (TaskFunc*)livenessTimeoutTask, this);
#ifdef DEBUG
        fprintf(stderr, "GenericMediaServer::ClientSession::noteLiveness envir().taskScheduler().rescheduleDelayedTask fReclamationSeconds %ds fLivenessCheckTask %d\n",
        fOurServer.fReclamationSeconds, (int)fLivenessCheckTask);
#endif
    }
}

说它先撤销之前的定时任务,原因在于如下代码,

void TaskScheduler::rescheduleDelayedTask(TaskToken &task,
					  int64_t microseconds, TaskFunc *proc,
					  void *clientData) 
{
    unscheduleDelayedTask(task);
    task = scheduleDelayedTask(microseconds, proc, clientData);
}

unscheduleDelayedTask调用就是撤销,但是第1次setup完成的时候,fLivenessCheckTask最后是有值的,就代表了这个定时任务对象,它是key可以找到这对象,当第2次setup的时候,就直接调用unscheduleDelayedTask撤销了第1次的65s的定时任务,但是,它紧接着又重新插入这同样的65s的定时任务了,这个时候会更新fLivenessCheckTask变量值的

我保存了下日志,如下:
第一次setup:
在这里插入图片描述
第1次setup时fLivenessCheckTask是null,撤销没啥操作,然后加入的延时任务,返回的token(key值)是164。

第2次setup
在这里插入图片描述
看到了吧,它撤销了164的定时任务,然后又新开了170的65s的定时任务。所以GenericMediaServer::ClientSession::noteLiveness是个接力棒,名不虚传

哎呀,那么这第2次setup的65s的定时任务又由谁撤销呢?哦呵,是下一个节点play。那play的呢?是rtp流又接力的,具体还没看。

优化点
如何定时任务和定时时间一样,可以不可以不要这么频繁的销毁重建对象,只需要重新刷新该对象的时长不就行了?

3.2 handleCmd_SETUP

回来,已经彻底搞清楚定时任务何时销毁何时创建了,完美,然后,再看接下来的流程:调用RTSPServer::RTSPClientSession::handleCmd_SETUP
在这里插入图片描述
在这里插入图片描述
此时fOurServerMediaSession不再为NULL,所以DynamicRTSPServer::lookupServerMediaSession中,不再重复创建很多对象了,直接扎到describe创建的对象了。
14.live555mediaserver-setup请求与响应_第39张图片
然后就跳跃到RTSPServer::RTSPClientSession::SETUPLookupCompletionFunction1
接着
RTSPServer::RTSPClientSession::handleCmd_SETUP_afterLookup1

最终还是这里
RTSPServer::RTSPClientSession::handleCmd_SETUP_afterLookup2

整个流程如下:
14.live555mediaserver-setup请求与响应_第40张图片

然后呢,找到了音频的ServerMediaSubsession,然后在OnDemandServerMediaSubsession::getStreamParameters又创建了音频的好几个新对象,一执行,对象图和第一次setup是一样的,无非是再复制下,如下图。
14.live555mediaserver-setup请求与响应_第41张图片

看下面的2个红框。

第2次setup响应
14.live555mediaserver-setup请求与响应_第42张图片
可以看到,和第一次setup的响应,只是Cseq序号、interleaved字段不一样外,其他都一样。

4.小结

探秘了延时任务管理双向循环链表。如下此时的定时器任务的链表模样:
14.live555mediaserver-setup请求与响应_第43张图片
为了实现setup业务,又新增了几个对象如下图14-13.
14.live555mediaserver-setup请求与响应_第44张图片

图14-13.
可以看到,除了模拟的2个类(类图太大了,为了在一张图展现出来就模拟了下就是示意的意思)不是本节新创建的——最左边是早创建了,最右边是describe就创建了,在setup又销毁重建了,就不算了——其他的新增了几个对象:
(1)新对象RTSPServer::RTSPClientSession。SETUP、PLAY、PAUSE、TEARDOWN、正常url的GET_PARAMETER和SET_PARAMETER都在这个新对象里处理了。
(2)struct streamState对象数组。这个为何是数组呢?因为如果describe阶段sdp有2个m字段——假如一个音频一个视频,则VLC会下发2次setup,那也就会走2次setup流程,一次视频的对象,一次是音频的对象,哇,好多对象。
(3)把新对象RTSPServer::RTSPClientSession加入到hash链表GenericMediaServer::fClientSessions。它这个呀主要是把setup新建立的对象RTSPServer::RTSPClientSession管理起来,和sessionid绑定,组sdp响应包。
(4)新对象Destinations。加入到hash链表OnDemandServerMediaSubsession::fDestinationsHashTable中去。
(5)OnDemandServerMediaSubsession::getStreamParameters创建的对象。
在这里插入图片描述

你可能感兴趣的:(手把手一起学live555,网络,tcp/ip,网络协议)