LIVE555源码研究之四:MediaServer (一)

从本篇文章开始我们将从简单服务器程序作为突破点,深入研究LIVE555源码。

从前面的文章我们知道,任何一个基于LIVE555库实现的程序都需要实现自己的环境类和调度类。这里,服务器程序就使用了BasicEnvironment库中实现的简单环境类和简单调度类。说它简单,是因为该环境类仅仅实现了将错误信息输出到控制台。而调度类仅仅通过select模型实现socket的读写。

下面我们来看下简单环境类BasicEnvironment和简单调度类BasicTaskScheduler是如何实现的。

打开live555MediaServer.cpp可以看到熟悉的main函数。main函数比较简单,除去错误信息输出代码,有效代码仅十几行而已。

为了便于分析,我们仅列出精简后的代码

int main(int argc, char** argv) 

{

  TaskScheduler* scheduler = BasicTaskScheduler::createNew();//创建具体调度类

  UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);

  RTSPServer* rtspServer;

  portNumBits rtspServerPortNum = 554;//默认端口554

  rtspServer = DynamicRTSPServer::createNew(*env, rtspServerPortNum, authDB);

  if (rtspServer == NULL) 

  {

    rtspServerPortNum = 8554;//若554被占用,尝试使用8554端口

    rtspServer = DynamicRTSPServer::createNew(*env, rtspServerPortNum, authDB);

  }

 if(rtspServer->setUpTunnelingOverHTTP(80)//rtsp over http 监听80端口

   ||rtspServer->setUpTunnelingOverHTTP(8000)

   || rtspServer->setUpTunnelingOverHTTP(8080)) 

  {

  } 

  env->taskScheduler().doEventLoop();

  return 0; 

}

可以看到开始的两行创建了调度器和环境类对象。CreateNew为static成员变量,内部实现仅仅是new一个对象而已。此处采用简单工厂模式。第二行在创建具体环境类时,将第一行创建的子调度类指针传入,这是因为环境类内部维护了抽象调度类指针,这样任何可以引用到环境类的类都可以输出错误信息同时也可以将自己加入到调度中。

一、BasicUsageEnvironment类

类继承关系如下图:

UsageEnvironment抽象类,定义了相关接口,比较简单。

UsageEnvironment0类定义一个存储错误信息的缓冲区,同时实现了一系列在UsageEnvironment中定义的操作该缓冲区的方法。

BasicUsageEnvironment类重定义了输出操作符。用于向控制台输出错误信息。

二、BasicTaskScheduler类

类继承关系如下图

BasicTaskScheduler用于程序的调度,是整个程序的发动机。在TaskScheduler类中定义的doEventLoop虚成员函数,在TaskScheduler0中实现,用于循环读取任务,并进行调度。

void BasicTaskScheduler0::doEventLoop(char* watchVariable) 

{

  while (1) 

  {

     if (watchVariable != NULL  && *watchVariable != 0)

         break;

     SingleStep();

   }

}

SingleStep在BasicTaskScheduler中实现,每执行一次会调度一个任务执行。

任务调度类中定义了三种类型的任务,分别为:延迟任务、socket任务和事件任务。

在TaskSheduler0中分别定义了DelayQueue(延迟队列)、HandlerSet(存储socket和对应处理函数的映射)、保存事件任务的数组这三种结构。SingleStep函数每执行一次都会从这三种结构搜索满足条件的一个任务,并执行对应处理函数。接下来我们详细介绍下这三种结构:

1.延迟队列DelayQueue

DelayQueue继承自DelayQueueEntry。用于管理延迟队列中的每一项,提供增删改查功能。

DelayQueueEntry代表延迟队列中的一项。其成员为fDeltaTimeRemaining,表示该项任务的剩余时间。

AlarmHandler也继承自DelayQueueEntry,是真正的存储在DelayQueue中的延迟项。其成员为TaskFunc* fProc和void* fClientData。当延迟项的剩余时间为0时,fProc会被调用,fClientData为该函数的参数。TaskFunc为处理函数,其定义如下:

void TaskFunc(void* clientData);

当该延迟项剩余时间为0时,对应处理函数就会被调用。调用处理函数的成员函数为在AlarmHandler实现的handleTimeout,其实现代码如下:

virtual void handleTimeout()

  {

    (*fProc)(fClientData);  //个人认为判断一下fProc是否为空会更好

     DelayQueueEntry::handleTimeout(); //调用基类handleTimeout

  }

DelayQueue的实现并不复杂,但需要注意的是:存储在延迟队列中的每一项是按照剩余时间递增的顺序存储的,同时后一项仅保存与前一项的时间差。

举例说明:正常情况下,在我们构建的自己延迟队列时,假设各项剩余时间分别为:1、3、5、7、9。

按照DelayQueue的算法,保存在DelayQueue中保存的每一项的剩余时间为:1、2、2、2、2。

明白了此算法后再看下DelayQueue的各种方法实现,均是对链表的各种处理,是不是感觉很简单。

DelayQueue中一个很重要的方法是Synchronize()。该方法会在执行SingleStep时被调用,递减每一项的剩余时间。然后再检查第一项的剩余时间。当第一项的剩余时间小于等于0时,其对应处理函数就会被调用,同时该项会被从延迟队列清除出去。

最后一个需要注意的地方:DelayQueue继承自DelayQueueEntry,并作为整个延迟队列的表头,在DelayQueue的构造函数中,调用了基类DelayQueueEntry的构造函数,同时传入参数ETERNITY。

其定义如下:

const DelayInterval ETERNITY(INT_MAX, MILLION-1);

宏INT_MAX为int类型的最大值。 而MILLION定义如下:

static const int MILLION = 1000000;

因此我们可以知道该项的延迟时间是最大的。任何新插入的项都会插入到该项的前面。这导致的结果就是在遍历时不用检查是否到达最后一项,插入的任何任何剩余时间的延迟项都会被插入到最后一项之前。且延迟队列中总会存在一项。其他类可以调用schedulerDelayedTask向调度器添加一项延迟任务:

TaskToken scheduleDelayedTask(int64_t microseconds, TaskFunc*proc, void* clientData);

值为TaskToken,用于标识每一个延迟任务,类似于任务的ID。定义如下:

typedef void* TaskToken;

scheduleDelayedTask用于取消某延迟任务,参数为TaskToken:

  virtual void unscheduleDelayedTask(TaskToken& prevTask);

 

scheduleDelayedTask用于使用新的时间重新调度某延迟项,重新调度后原来的延迟时间不再起作用:

virtual void rescheduleDelayedTask(TaskToken& task,

                   int64_t microseconds, TaskFunc* proc,

                   void* clientData);

2. Socket任务处理

Socket事件保存在HandlerSet * fHandlers开头的链表中。HandlerSet内部定义了HandlerDescriptor成员。该成员内部定义了如下成员:

   int socketNum;  

    int conditionSet;

    TaskScheduler::BackgroundHandlerProc* handlerProc;

    void* clientData;

socketNum为要检测的socket,conditionSet为满足条件的socket的状态,HandlerProc为发生对应事件时需要调用的处理函数, clientData为传递给事件处理函数的参数。其定义如下:

typedef void BackgroundHandlerProc(void* clientData, int mask);

Socket的任务处理比较简单。相信大伙也可以看懂。其他类可以调用turnOnBackgroundReadHandling向调度器添加socket任务:

 void turnOnBackgroundReadHandling(int socketNum, BackgroundHandlerProc* handlerProc, void* clientData);

已经加入调度器的socket任务可以调用turnOffBackgroundReadHandling取消:

void turnOffBackgroundReadHandling(int socketNum)

但上述两个函数已被废弃,仅为了保持向前兼容而加以保留,被下面的函数取代:

//向调度器添加socket任务:

  virtual void setBackgroundHandling(int socketNum, int conditionSet, BackgroundHandlerProc* handlerProc, void* clientData);

    //disableBackgroundHandling用于取消对某socket事件的调度:

  void disableBackgroundHandling(int socketNum);

3. 事件任务处理

为了实现事件任务,定义了以下结构:

  EventTriggerId fTriggersAwaitingHandling, fLastUsedTriggerMask;

  TaskFunc* fTriggeredEventHandlers[MAX_NUM_EVENT_TRIGGERS];

  void* fTriggeredEventClientDatas[MAX_NUM_EVENT_TRIGGERS];

  unsigned fLastUsedTriggerNum;

宏 MAX_NUM_EVENT_TRIGGERS值为32,可以知道事件任务最大支持32项。fTriggeredEventHandlers是一个有32个项的数组。每个项保存一个TaskFunc类型的任务处理函数:

typedef void TaskFunc(void* clientData);

该任务处理函数与延迟队列中的任务处理函数定义相同。fTriggeredEventClientDatas数组中保存对应任务处理函数的参数。

EventTriggerId fTriggersAwaitingHandling, fLastUsedTriggerMask;

typedef u_int32_t EventTriggerId;

EventTriggerId 32位无符号整形。其实它是一个32bit的位图。每一位对应fTriggeredEventClientDatas数组中的每一项。当对应位为1时,表示该数组中的对应位置存在一项。使用位图,可以节省存储空间。但随之而来的问题就是对位图的操作也变的相应复杂。如果让我来搞的话,我宁愿定义一个32项的bool类型数组。

其他类可以调用createEventTrigger向TaskScheduler中添加一项事件任务。

EventTriggerID createEventTrigger(TaskFunc*eventHandlerProc);

使用deleteEventTrigger来删除某事件对象:

  virtual void deleteEventTrigger(EventTriggerId eventTriggerId) = 0;

 

前面我们说过BasicTaskScheduler实现了SingleStep。SingleStep驱动了整个程序的运行。有了前面的铺垫,相信读懂它应该不成问题。对于某些细节问题,此时可以不必深究,可等以后对整个架构有了全局的认识之后,再详细探究。

由于篇幅限制,在此不详细介绍。总结起来在SingleStep执行了以下动作:

1.调用select检查fReadSet、fWriteSet和fExceptionSet看是否有满足条件添加的socket。然后遍历HandlerSet检查每个socket的状态,如果状态得到满足即说明在该socket上发生了对应的事件,然后调用与该socket对应的处理函数。

2. 检查事件任务数组是否存在可用项,如存在则调用对应处理函数。

3. 检查延时队列,看是否存在剩余时间为0的项,如找到则执行对应处理函数,然后将该项删除。

SingleStep每次只会指向上述三种类型的事件中的一项。延迟任务执行后即会被从延迟队列删除。其他两种类型的任务却仍然在任务队列中等待着下次触发。

2014.8.9于浙江杭州

LIVE555源码研究之四:MediaServer (一),布布扣,bubuko.com

时间: 2024-10-18 08:17:47

LIVE555源码研究之四:MediaServer (一)的相关文章

live555源码研究(四)------UserAuthenticationDatabase类

一.UserAuthenticationDatabase类作用 1,用户/密码管理 2,鉴权管理 二.类UserAuthenticationDatabase继承关系图                         live555源码研究(四)------UserAuthenticationDatabase类,布布扣,bubuko.com

live555源码研究(五)------DynamicRTSPServer类

一.类DynamicRTSPServer作用 1,提供RTSP服务 二.类DynamicRTSPServer继承关系图 live555源码研究(五)------DynamicRTSPServer类,布布扣,bubuko.com

live555源码研究(三)------UsageEnvironment类

一.UsageEnvironment类作用 1,他是使用环境的一部分. 2,他提供了对socket触发事件的管理. 二.类UsageEnvironment继承关系图 二.UsageEnvironment成员函数 1, live555源码研究(三)------UsageEnvironment类

live555源码研究(二)------TaskScheduler类

一.TaskScheduler类作用 1, 2 二.TaskScheduler 1, 2 live555源码研究(二)------TaskScheduler类

庖丁解牛-----Live555源码彻底解密(RTP打包)

本文主要讲解live555的服务端RTP打包流程,根据MediaServer讲解RTP的打包流程,所以大家看这篇文章时,先看看下面这个链接的内容; 庖丁解牛-----Live555源码彻底解密(根据MediaServer讲解Rtsp的建立过程) http://blog.csdn.net/smilestone_322/article/details/18923139 在收到客户端的Play命令后,调用StartStream函数启动流 void OnDemandServerMediaSubsessi

Chrome自带恐龙小游戏的源码研究(完)

在上一篇<Chrome自带恐龙小游戏的源码研究(七)>中研究了恐龙与障碍物的碰撞检测,这一篇主要研究组成游戏的其它要素. 游戏分数记录 如图所示,分数及最高分记录显示在游戏界面的右上角,每达到100分就会出现闪烁特效,游戏第一次gameover时显示历史最高分.分数记录器由DistanceMeter构造函数实现,以下是它的全部代码: 1 DistanceMeter.dimensions = { 2 WIDTH: 10, //每个字符的宽度 3 HEIGHT: 13, //每个字符的高 4 DE

Chrome自带恐龙小游戏的源码研究(七)

在上一篇<Chrome自带恐龙小游戏的源码研究(六)>中研究了恐龙的跳跃过程,这一篇研究恐龙与障碍物之间的碰撞检测. 碰撞盒子 游戏中采用的是矩形(非旋转矩形)碰撞.这类碰撞优点是计算比较简单,缺点是对不规则物体的检测不够精确.如果不做更为精细的处理,结果会像下图: 如图所示,两个盒子虽然有重叠部分,但实际情况是恐龙和仙人掌之间并未发生碰撞.为了解决这个问题,需要建立多个碰撞盒子: 不过这样还是有问题,观察图片,恐龙和仙人掌都有四个碰撞盒子,如果每次Game Loop里都对这些盒子进行碰撞检测

Redis源码研究—哈希表

Redis源码研究-哈希表 Category: NoSQL数据库 View: 10,980 Author: Dong 作者:Dong | 新浪微博:西成懂 | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明 网址:http://dongxicheng.org/nosql/redis-code-hashtable/ 本博客的文章集合:http://dongxicheng.org/recommend/ 本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享

Chrome自带恐龙小游戏的源码研究(五)

在上一篇<Chrome自带恐龙小游戏的源码研究(四)>中实现了障碍物的绘制及移动,从这一篇开始主要研究恐龙的绘制及一系列键盘动作的实现. 会眨眼睛的恐龙 在游戏开始前的待机界面,如果仔细观察会发现恐龙会时不时地眨眼睛.这是通过交替绘制这两个图像实现的: 可以通过一张图片来了解这个过程: 为实现图片的切换,需要一个计时器timer,并且需要知道两张图片切换的时间间隔msPerFrame.当计时器timer的时间大于切换的时间间隔msPerFrame时,将图片切换到下一张,到达最后一张时又从第一张