DarwinStreamingServer程序结构分析DarwinStreamingServer是Apple公司提供的开源实时流媒体播放服务器程序。整个程序使用C++编写,在设计上遵循高性能,简单,模块化等程序设计原则,务求做到程序高效,可扩充性好。本文简述了程序的整个结构,目的是为了以后阅读及修改程序的方便。1.前言DarwinStreamingServer是Apple公司提供的开源实时流媒体播放服务器程序。整个程序使用C++编写,在设计上遵循高性能,简单,模块化等程序设计原则,务求做到程序高效,可扩充性好。本文简述了程序的整个结构,目的是为了以后阅读及修改程序的方便。解开程序,程序的主要目录结构及作用如下APIModules/模块程序的目录APIStubLib/程序公共接口类目录CommonUtilitiesLib/通用库Server.tproj/主程序目录QTFileLib/mov,MP4文件读写库RTSPClientLib/RTSP客户端协议库RTCPUtilitiesLib/RTCP协议库PlaylistBroadcaster.tproj/MP4播放列表广播器2.重要基类结构每个C++程序总有自己的基本类库结构,这种结构一般是程序的工具类,及基本接口类。在DarwinStreamingServer中CommonUtilitiesLib目录下是放着程序的基本类库结构主要重要的是:Task类,TaskThread类,TaskThreadPool类:这三个类封装了线程库。三个类相互作用,通过FriendClass,使表现在外端的只有Task类,在这里,我们可以这样看:Task就是CPU使用片断,TaskThreadPool是CPU管理程序,TaskThread就是CPU。这样就使我们只需关心Task类就可,至于怎么调度,这是TaskThread及TaskThreadPool的事了。下面我们来看这三个类是怎么互相作用从而形成一个整体的。a.VirtualTaskThread::Entry():这个函数是整个线程函数的第一个执行函数,线程入口函数是TaskThread从父类继承过来的StaticOSThread::_Entry(),这个静态函数的唯一作用是执行Entry这个函数,这时的this指针是传进来的参数。这样就实现了线程的类封装。TaskThread::Entry()函数是一个循环,它不停的调用本身的WaitForTask函数,从自己内部的Queue中取得要运行的Task类指针。然后运行其中的Task:::Run()函数。b.VirtualTaskThread::WaitForTask()这个函数负责不停的从内部的队列中取到Task才返回。c.staticTaskThread**TaskThreadPool::sTaskThreadArray.这个变量是一个很重要的变量,通过它,我们就能在线程外与线程内通信,(要知道TaskThread的指针在线程类就是this指针,在线程外就是sTaskThreadArray的里面放的值。这样的话,我们就可以通过这个指针(或者说死了,也就是这个队列)来进行通讯。d.voidTask::Signal()这个函数就是我们要往TaskThread队列里面加Task的函数.这个函数直接访问TaskThreadPool::sTaskThreadArray指针,并且因此直接访问到TaskThread的内部队列变量。e.VirtualintTask::Run()这个函数就是我们程序要运行的任务,也就是程序要在这里做什么。在这里程序的返回值是一个重要的信息,当返回-1时程序会直接删除该Task指针,当返回一个正数n时线程会在n毫秒后再次调用这个Task的Run函数,当返回0时,线程并不删掉这个Task指针,只是接着执行下一个Task.(注意;在我们继承Task类,而重写Run虚函数时,我们第一件要作的事情是运行Task类的GetEvent()函数。)在Task,TaskThread,TaskThreadPool三个类的结构中,通过友元包装了TaskThread.TaskThreadPool,只留下了Task给我们重写,这是一种类的重要组合方法。Task(OutThread)--Signal(function)-----TaskThreadPool(OutThread)--TaskThread(InThread)EventContext类,EventThread类:这两个类也像上面的类一样,组合成一个事件发生及线程的包装类。我们可以把它看作一个Select的封装调用。这个线程就是专门用来监测事件的发生的(当然也可以直接在这里处理罗)。类的框架及主要函数如下a.VirtualvoidEventThread::Entry():这个就是线程运行空间函数。这个函数就是循环调用Select等待事件的发生。当事件发生时,它从内部Table中取得它的指针(这些指针都是在请求事件发生是调用函数注册进去的,这种指针必须是EventContext的指针。也就是说事件处理类必须是EventContext的父类)。当取得EventContext的指针后,线程接着调用EventContext-ProcessEvent().b.VirtualvoidEventContext::ProcessEvent():这个就是处理事件函数,这个处理事件函数的缺省调用就是掉用本身的Task*EventContext::fTask变量的Signal函数。这是通过构建或EventContext::SetTask()函数设的,联想到上面的Task,TaskThread,TaskThreadPool三者的关系,这个Signal函数就是将运行处理函数交给别的线程处理的唯一函数,但是你也可以改写掉这个函数,可以让他直接在这个线程处理就完了。当然这样会大大降低响应的速度。看到这里,整个程序的线程调度框架你大概可以猜出来了吧。c.VoidEventContext::RequestEvent():这个函数其实是事件请求函数,通过这个函数我们就相当于表达这样一种意思,当我们请求的事件发生的时候,请调用我们的ProcessEvent函数,也就是调用运行我们重写的Task::Run()函数。整个事件的处理流程是:I:EventContext::RequestEvent()II:EventThread::Entry()III:当事件触发时:EventContext::ProcessEvent()IIII:Task::Signal();IIIII:Task::Run();注:I,II,III,IIII是在监听线程中发生的,而IIIII是在处理线程中发生的。d.这样的话,我们就实现了专用线程select函数的封装调用。TCPListenerSocket类:这个类其实就是上面EventThread的衍生使用,他专门用于包装Accept()调用。当Accept发生时,也就是selectRead事件的促发。他产生一个TCPSocket类并调用本身的VirtualTCPListenerSocket::GetSessionTask()(这是一个纯虚函数)产生一个Task处理类,并且帮他注册一个kRead事件。1.程序框架结构程序的基本框架:a.从线程划分来讲:程序可以理解为共有5个线程(在Linux下看起来是6个,因为Linux是进程模拟线程,所以其中有一个是管理线程)。(1)EventMoitor线程:这就是我刚才在EventContext,EventThread结构中讲的EventThread线程,他的主要功能就是监视事件的发生,当事件发生时调用EventContext的ProcessEvent()函数,(也就是调用Task::Signal()函数),把任务加入到线程池TaskThreadPool处理中去,TaskThread会调用Task::Run()自动处理的。同时这个线程也负责了Accept调用,TCPLisnterSocket(TCPListenerSocket类本身也是一个Socket类)也加入到这个调用中去,当kRead事件发生时,ProcessEvent()调用GetSessionTask()产生一个Task子类和Socket子类。并将Task类加入到这个Socket类中(顺便说一下,这个Socket类是继承EventContext类的,他本身有一个socketfd,因此它本质上是围绕select调用而使用的。)同时为这个Socket类注册kRead事件(其实就是将描述符加入到select的ReadSet中去)。(注:当你看ProcessEvent()函数时,你可能会以为ProcessEvent是一个死循环.其实这里并不是一个死循环,我们需要注意的是在程序中所有的Socket都是UnBlock方式,所以当再没有连接时,这个循环会从Break这个地方跳出去。追求每一点都是Unblock的,让程序不在任何一点阻塞,这是这个程序的一个很鲜明的特点。我们用线程替代进程,使用Select调用,追求的难道不是这一点吗。当处理繁忙时,把每一个CPU时间片都用在处理任务,这就是最高效的服务器程序所追求的。无点阻塞,这是程序所追求的,但也很可能形成这样一种情况,在空闲时,程序会形成死循环,以至于这个线程占用CPU过多,导致CPU负载过高,这个问题程序用了两个方法来解决,1.在处理完一系列事件后,使用ThreadYield()调用让出CPU时间。(这种方法可能在有些系统不能使用,例如Linux就是无效的,因为在Linux线程本质上就是线程,它得到调度的机会和其他进程一样。因此只有在某些协作型的线程,例如Solaris的线程库,不将线程等同于进程来调度时(可以在程序中设置),即,线程和进程调度优先级不一样时(也就是所有的线程共享一个进程的CPU时间片),这样这个调用才真正有效。)2。就是Select调用的Timeout参数。(1)IdleTask线程:这是一个空闲线程,这相当于让CPU去干别的事情,他是通过OsCond和OsMutex来完成这个功能的(在不同的*作系统有不同的实现,例如在标准PosixThread是pthread_cond_wait,)。我们可以通过IdleTask::SetIdleTimer()函数来实现这个定时任务。在到时间了才唤醒,这样的话,当一个Task子类,例如TCPListenerSocket类它多重继承了TCPSocket类和IdleTask,当这个线程要等一段时间再执行时。TCPListenerSocket就调用TCPListnerSocket::SetIdleTimer(),即IdleTask::SetIdleTimer(),来实现一段定时。当前线程是被IdleThread这个线程阻塞的。(至于程序为什么这样写,我猜想是为了是方便的实现一种定时机制及让出CPU时间,还有为什么要为了这个而单独产生一个线程,这也可能是为了使用的方便吧!)(1)TaskThread线程:终于进入了真正干事的进程了。这个就是所有Task的执行线程了,他的作用就是不停的执行Task()里面的Run()函数,根据Run函数的返回值,再决定对Task如何处理。在程序的框架中,对于单个CPU,TaskThreadPool只产生一个线程,也就是一个TaskThread线程。(多个线程会产生上下文切换的问题,)但对于多个CPU则产生多个TaskThread线程(这点大家都明白吧)。(注:其实不同的问题有不同的解法,对于不会阻塞I/O上的程序我认为这一点是好的,越少的线程就意味着越少的上下文切换,也就意味这CPU化更多的时间在干正事上,但当线程中可能有单点阻塞时,这就在一定程度上会阻碍其他Task的执行,所以在写程序的时候一定要注意不能在任何一点阻塞)。因为在线程中要涉及文件读写I/O,所以我对在这里单CPU采用单线程在这里这一点持保留意见。不过作者在这里也考虑到了