《2022年Apple公司Darwin流式服务器源代码分析[借 .pdf》由会员分享,可在线阅读,更多相关《2022年Apple公司Darwin流式服务器源代码分析[借 .pdf(25页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、Apple 公司 Darwin 流式服务器源代码分析(尉明)当前,伴随着 Internet 的飞速发展,计算机网络已经进入到每一个普通人的家庭。在这个过程中,一个值得我们关注的现象是:Internet中存储和传输内容的构成已经发生了本质的改变,从传统的基于文本或少量图像的主页变为大容量、富信息量的流式媒体信息。一份早在1998 年提交的研究报告就曾指出,流式媒体统治 Internet的潮流是不可抗拒的,该报告估计到2003年,存储在网络服务器上的内容超过 50%的将是流式媒体信息。但今天看来,这个估计还是有些保守了。所谓的流式媒体简单的讲就是指人们通过网络实时的收看多媒体信息:如音频流、视频流
2、等。与流式媒体对应的传统工作方式是下载+播放模式,即用户首先下载多媒体文件,然后再在本地播放,这种方法的一个主要缺点是启动延迟较大,例如一个 30 分钟长的 MPEG-I 文件(相当于 VCD 质量),即使使用1.5Mbps的速率下载,也需要半个小时才能完成,这样一个漫长的等待时间实在是无法忍受。在窄带网络环境中,几乎所有基于Internet的流式媒体产品都有着类似的工作原理:首先需要开发高效的压缩编码技术,并通过一套完整有效的传输体系将其发布到用户的桌面上。目前在流式媒体领域,有三种占有主导地位的产品,它们分别是 Apple 公司的 Quick Time、Microsoft 公司的 Medi
3、a Server以及 Real公司的 Real System。本文将介绍 QuickTime 技术及其开放源代码的Darwin 流化服务器。1 QuickTime 技术介绍Apple 公司近日发布了 QuickTime 5 及 QuickTime Streaming Server 3(简称QTSS)。作为客户端的 QuickTime 5 是用于在 Internet上对高质量音频和视频内容进行创建、播放及提供数字流的软件,目前 QuickTime 在全世界的使用量已经超过 1 亿 5 千万份。QuickTime Streaming Server 3是 Apple 基于标准的、开放式源代码的流式服
4、务器软件的新版本,它包括以下新功能:跳读保护(Skip Protection),一项获得专利的特性组合,它可以保证Internet 上数字流的质量,防止中断;全新的易于使用、基于Web 的界面,用户可以在本地或远程进行管理,实现服务器配置。作为Internet 流媒体联盟(ISMA)的创建者之一,Apple 不氖褂锰uickTime 已被国际标准组织(ISO)选为 MPEG-4 的基本文件格式,可预见 Apple 将有更多 MPEG-4 产品和技术的推出。QuickTime 正迅速成为世界领先的跨平台多媒体技术,而且是迄今为止唯一的开放式源代码、基于标准的数字流解决方案。ZDNet 在 200
5、0年 9 月对于三种流式媒体服务器的特征比较说明了QTSS不仅仅被技术开发者关注,而且可以通过简单的定制成为成熟强大的产品,评测结果可见表1。表 1 ZDNet 对三类产品的评测结果服务器模块 QTSS 2.01 Media Server 7 RealServer Basic 7操作系统支持Windows NT,2000;FreeBSD;Linux;Mac OS;SolarisWindows NT,2000 Windows NT,2000并发流个数 2,000 2,000 25 free/3000 pro现场直播和广播Yes Yes Yes在线广告支持Yes Yes Yes名师资料总结-精品资
6、料欢迎下载-名师精心整理-第 1 页,共 25 页 -PPV/流加密 No/No Yes/Yes Yes/Yes分配流能力 No Yes YesSMIL 标准支持 Yes No YesRTSP标准支持 Yes No Yes多播支持Yes Yes Yes状态报告Yes Yes Yes服务器日志 Yes Yes Yes防火墙和代理支持Yes Yes Yes远程监控Yes Yes Yes客户可以使用 QuickTime Player 或其他支持 QuickTime 的应用程序在 Windows或 Macintosh 平台上接收视频流,而且QuickTime Player可以从苹果公司的网站上下载免费
7、使用。如果安装了 QuickTime 的插件,客户还可以直接通过浏览器收看。客户希望点播一个节目时,QuickTime Player 或插件将向 QTSS发送请求,指明要点播的节目名。如果该节目存在,QTSS将向客户发送相应的视频流。当客户希望收看现场直播(或实时广播)时,它首先从 QTSS获得关于当前频道的编码格式、地址等相关信息,然后再接受该频道的媒体流。对于那些希望在 Internet上实时流化视频或音频信息的用户,QTSS 服务器将是一个很好的选择,通过它可实现多项任务,例如:创建一个 24 小时在线的 Internet 广播电台;现场实况转播:如公司会议、体育节目等;创建远程学习站点
8、:如能够点播视频和演讲;图 1 是一个利用 QTSS服务器建立的现场直播场景。2 Darwin 流化服务器介绍Darwin Streaming Server(简称 DSS)是 QuickTime Streaming Server开放式源代码的版本,同时支持FreeBSD、Linux、Solaris、Windows NT 和 Windows 2000等多个操作系统,是当前所有同类产品中支持平台最多的一个。DSS的源代码和相关文档可从以下站点获得:http:/DSS源代码完全采用标准C+语言写成,编程风格非常优秀,每个C+类都对应着一对和类同名的.h/.cpp文件。但是由于大量采用了面向对象的概念
9、,如继承、多态等等;而且源文件和类相当多,所以不太容易讲清楚。因此,读者最好事先把代码完整的过滤一两遍,再配合本文,就能看得更清楚点。整个服务器包括多个子系统,分别存放在独立的工程内,如图2 所示。其中,最为重要的是基础功能类库(CommonUtilitiesLib)和流化服务器(StreamingServer)两个工程,前者是整个系统的通用代码工具箱,包括了线程管理、数据结构、网络和文本分析等多个功能模块。DSS和其他相关的工具使用基础功能类库工程中定义的功能类实现以下三个目标:(1)抽象出系统中相同或类似的功能,用于降低代码的冗余度;(2)封装基本功能,简化高层编码的复杂度;(3)隔离开操
10、作系统平台相关的代码。而流化服务器工程中包含了DSS 对多个国际标准的实现,是整个服务器的主工名师资料总结-精品资料欢迎下载-名师精心整理-第 2 页,共 25 页 -程。在本文中,我们将重点分析这两个工程中的核心代码和模块。另外,我们还将简单介绍利用 DSS 提供的开发接口(Module)扩展和定制服务器的方法。DSS实现了四种 IETF 制定的国际标准,分别是:实时流传输协议RTSP(Real-time Streaming Protocol,RFC 2326)、实时传输协议(RTP Real-time Transfer Protocol,RFC 1889)、实时传输控制协议 RTCP(Re
11、al-time Transport Control Protocol,RFC 1889)、会话描述协议 SDP(Session Description Protocol,RFC 2327)。这四个标准是开发所有流式媒体产品都必须掌握的,因此在对相关代码进行分析和二次开发之前,希望读者了解上述四种协议的基本思想,上述协议样本可从以下网站获得:http:/www.ietf.org3 基础功能类库(Common Utilities)3.1 OS 类Darwin Streaming Server支持包括 Windows,Linux 以及 Solaris 在内的多种操作系统平台。我们知道,Windows
12、 和 Unix(或 Unix-like)操作系统之间无论从内核还是编程接口上都有着本质的区别,即使是 Linux 和 Solaris,在编程接口上也大为不同。为此,DSS开发了多个用于处理时间、临界区、信号量、事件、互斥量和线程等操作系统相关的类,这些类为上层提供了统一的使用接口,但在内部却需要针对不同的操作系统采用不同的方法实现。表2 罗列出了 DSS中的主要OS 类和数据结构。表 2 DSS中的主要 OS 类和数据结构类(数据结构)名主要功能OS 平台相关的功能类,如内存分配、时间等OSCond 状态变量的基本功能和操作OSMutex 互斥量的基本功能和操作OSThread 线程类OSFi
13、leSource 简单文件类OSQueue 队列类OSHashTable 哈希表类OSHeap 堆类OSRef 参考引用类3.1.1 OSMutex/OSCond Class在有多个线程并发运行的环境中,能同步不同线程的活动是很重要的,DSS 开发了 OSMutex和 OSCond两个类用以封装不同操作系统对线程同步支持的差异。我们首先分析 OSMutex 类,这个类定义了广义互斥量的基本操作,类定义如下:class OSMutex1 public:2 OSMutex();/构造函数3 OSMutex();/析构函数4 inline void Lock();/加锁名师资料总结-精品资料欢迎下载
14、-名师精心整理-第 3 页,共 25 页 -5 inline void Unlock();/解锁6 inline Bool16 TryLock();/异步锁,无论是否成功立即返回7 private:8#ifdef _Win32_9 CRITICAL_SECTION fMutex;/临界区10 DWORD fHolder;/拥有临界区的线程id11 UInt32 fHolderCount;/进入临界区线程数/其他略 在 Windows 平台上,OSMutex 类是通过临界区(CRITICAL_SECTION)来实现的,第 10 行定义了临界区变量fMutex。类实例化时构造函数调用Initial
15、izeCriticalSection(&fMutex)初始化临界区变量,对应的在析构函数中调用DeleteCriticalSection(&fMutex)清除。Lock()函数用于对互斥量加锁,它调用私有方法RecursiveLock 实现:void OSMutex:RecursiveLock()/当前线程已经拥有互斥量,只需增加引用计数1 if(OSThread:GetCurrentThreadID()=fHolder)2 3 fHolderCount+;/增加引用计数4 return;5 6#ifdef _Win32_7:EnterCriticalSection(&fMutex);/申请进
16、入临界区8#else9(void)pthread_mutex_lock(&fMutex);10#endif11 Assert(fHolder=0);12 fHolder=OSThread:GetCurrentThreadID();/更新临界区拥有者标志13 fHolderCount+;14 Assert(fHolderCount=1);第 1 行检测如果当前线程已经拥有互斥量,就只需将内部计数fHolderCount加 1,以便纪录正在使用互斥量的方法数。如果当前线程还没有得到互斥量,第7 行调用 EnterCriticalSection()函数申请进入临界区;如果当前已经有其他线程进入临界区
17、,该函数就会阻塞,使得当前线程进入睡眠状态,直到占用临界区的线程调用 LeaveCriticalSection(&fMutex)离开临界区后才可能被唤醒。一旦线程进入临界区后,它将首先更新临界区持有者标志(第12 行),同时将临界区引用计数加 1。注意到另外一个函数TryLock(),该函数也是用于为互斥量加锁,但与 Lock()不同的是,TryLock()函数为用户提供了异步调用互斥量的功能,这是因为它调用:TryEnterCriticalSection(&fMutex)函数申请进入缓冲区:如果临界区没有被任何线程拥有,该函数将临界区的访问区给予调用的线程,并返回TRUE,否则它名师资料总结
18、-精品资料欢迎下载-名师精心整理-第 4 页,共 25 页 -将立刻返回 FALSE。TryEnterCriticalSection()和 EnterCriticalSection()函数的本质区别在于前者从不挂起线程。接着分析 OSCond类,该类定义了状态变量(Condition Variable)的基本操作,类定义如下:class OSCond 1 public:2 OSCond();/构造函数3 OSCond();/析构函数4 inline void Signal();/传信函数5 inline void Wait(OSMutex*inMutex,SInt32 inTimeoutInM
19、ilSecs=0);/等待传信函数6 inline void Broadcast();/广播传信函数7 private:8#ifdef _Win32_9 HANDLE fCondition;/事件句柄10 UInt32 fWaitCount;/等待传信用户数/其他略 虽然同是用于线程同步,但 OSCond类与 OSMutex 大不相同,后者用来控制对关键数据的访问,而前者则通过发信号表示某一操作已经完成。在Windows平台中,OSCond是通过事件(event)来实现的;构造函数调用CreateEvent()函数初始化事件句柄fCondition,而析构函数则调用CloseHandle()关
20、闭句柄。OSCond的使用流程是这样的:线程调用Wait(OSMutex*inMutex,SInt32 inTimeoutInMilSecs=0)函数等待某个事件的发生,其中 inTimeoutInMilSecs 是最长等待时间,0 代表无限长。Wait()函数内部调用了 WaitForSingleObject(fCondition,theTimeout)函数,该函数告诉系统线程在等待由事件句柄fCondition标识的内核对象变为有信号,参数theTimeout告诉系统线程最长愿意等待多少毫秒。如果指定的内核对象在规定时间内没有变为有信号,系统就会唤醒该线程,让它继续执行。而函数 Signa
21、l()正是用来使事件句柄fCondition 有信号的。Signal()函数内部实现很简单,只是简单调用SetEvent函数将事件句柄设置为有信号状态。使用 OSCond的过程中存在一种需求,就是希望通知所有正在等待的用户事件已经完成,而 Signal()函数每次只能通知一个用户,因此又开发了另外一个广播传信函数如下:inline void OSCond:Broadcast()/提示:本函数相当循环调用Signal()函数1#ifdef _Win32_2 UInt32 waitCount=fWaitCount;/等待传信的用户数3 for(UInt32 x=0;x fRunning=true)
22、,当 Entry()函数运行完后再设为非运行状态;在运行过程中,用户可以通过StopAndWaitForThread()、join()、Detach()以及 ThrowStopRequest()等函数改变线程其他状态变量。3.1.3 OSHashTable/OSQueue/OSHeap/OSRef ClassDSS定义了几个通用的较为复杂的数据结构,它们都以类的方式封装。这些数据结构不但贯穿于 DSS 的所有源代码,而且由于其封装的十分好,读者可以在看懂源代码的基础上很容易的将它们从DSS的工程中抽取出来,构建自己的基础类库,为将来的开发工作打下良好的基础。另外,对这些基础数据结构源代码的研究
23、将提高我们对于面向对象技术的掌握和领会。最主要的数据结构有四种:哈希表(OSHashTable)、队列(OSQueue)、堆(OSHeap)和对象引用表(OSRef)。前三种是我们在编程中大量使用的数据结构,而对象引用表则是类似于COM/DCOM 组件编程中 IUNKOWN 接口功能的数据结构,它首先为每个对象建立了一个字符串形式的ID,以便于通过这个 ID 找到对象(类似于 QueryInterface);另外 OSRef 类还为每个对象实例建立了引用计数,只有一个对象不再被任何人引用,才可能被释放(类似于AddRef和 Release)。鉴于这几个类在结构上有相似之处,下面我们将分析OSH
24、ashTable的源代码,以便能够帮助读者更好的理解其他几个类。OSHashTable的代码如下:templateclass OSHashTable/*提示:OSHashTable被设计成为一个类模版,两个输入参数分别为:class T:实际的对象类;class K:用于为 class T计算哈希表键值的功能类。*/1 public:2 OSHashTable(UInt32 size)/构造函数,入参是哈希表中对象的最大个数3 4 fHashTable=new(T*size);/申请分配 size个哈希对象 class T的空间5 Assert(fHashTable);6 memset(fHa
25、shTable,0,sizeof(T*)*size);/初始化7 fSize=size;/*下面的代码决定用哪种方式为哈希表的键值计算索引;如果哈希表的大小不是2 的幂,只好采用对fSize求余的方法;否则可以直接用掩码的方式,这种方式相对速度更快*/8 fMask=fSize-1;9 if(fMask&fSize)!=0)/fSize 不是 2 的幂10 fMask=0;名师资料总结-精品资料欢迎下载-名师精心整理-第 7 页,共 25 页 -11 fNumEntries=0;/当前对象数12 13 OSHashTable()/析构函数14 15 delete fHashTable;/释放空
26、间16/下面介绍向哈希表中添加一个class T对象的源代码17 void Add(T*entry)18 Assert(entry-fNextHashEntry=NULL);/*利用功能类 class K,计算 class T对象的哈希键值,其计算方法由用户在 class K中定义*/19 K key(entry);20 UInt32 theIndex=ComputeIndex(key.GetHashKey();/利用键值计算索引21 entry-fNextHashEntry=fHashTable theIndex;/在新加对象中存储索引值22 fHashTable theIndex =ent
27、ry;/将该对象插入到索引指定的位置23 fNumEntries+;/24/下面介绍从哈希表中删除一个class T对象的源代码25 void Remove(T*entry)26/首先从哈希表中找到待删除的对象/1、计算哈希键值和其对应的对象索引27 key(entry);28 UInt32 theIndex=ComputeIndex(key.GetHashKey();29 T*elem=fHashTable theIndex;30 T*last=NULL;/*2、通过对象索引查找对象,如果不是要找的对象,接着找下一个,直到找到为止。这是因为,存放的时候就是按照这种模式计算索引的。*/31 w
28、hile(elem&elem!=entry)32 last=elem;33 elem=elem-fNextHashEntry;34/找到该对象,将其删除35 if(elem)36 37 Assert(elem);38 if(last)39 last-fNextHashEntry=elem-fNextHashEntry;40 else/elem在头部41 fHashTable theIndex =elem-fNextHashEntry;42 elem-fNextHashEntry=NULL;43 fNumEntries-;名师资料总结-精品资料欢迎下载-名师精心整理-第 8 页,共 25 页 -
29、44 45/下面介绍从哈希表中查找一个class T对象的方法46 T*Map(K*key)/入参为哈希键值47 48 UInt32 theIndex=ComputeIndex(key-GetHashKey();/计算索引49 T*elem=fHashTable theIndex;/找到索引对应的对象50 while(elem)51 K elemKey(elem);52 if(elemKey=*key)/检查是否找对53 break;54 elem=elem-fNextHashEntry;/如果不是,继续找下一个55 56 return elem;57/以下略 以上介绍了哈希表的构造以及三种基
30、本操作:添加、删除和查询。另外,DSS还定义了 OSHashTableIter类用于枚举 OSHashTable中的 class T对象;其中主要的操作有 First和 Next 等,限于篇幅,此处就不再详述。3.2 Tasks类因为服务器从整体上采用了异步的运行模式,这就需要一种用于事件通信的机制。举例来说:一个 RTSP 连接对应的Socket 端口监测到网络上有数据到达,此时必须有一个模块(或代码)被通知(notify)去处理这些数据。为此,DSS 定义了 Task 及其相关类作为实现这一通信机制的核心。在 Task.h/cpp 文件中,定义了三个主要的类,分别是:任务线程池类(Task
31、ThreadPool Class)、任务线程类(TaskThread Class)以及任务类(Task Class)。每个 Task 对象有两个主要的方法:Signal 和 Run。当服务器希望发送一个事件给某个Task对象时,就会调用Signal()方法;而Run()方法是在Task 对象获得处理该事件的时间片后运行的,服务器中的大部分工作都是在不同Task 对象的 Run()函数中进行的。每个Task 对象的目标就是利用很小的且不会阻塞的时间片完成服务器指定某个工作。任务线程类是上文介绍的OSThread 类的一个子类,代表专门用于运行任务类的一个线程。在每个任务线程对象内部都有一个OSQ
32、ueue_Blocking 类型的任务队列,存储该线程需要执行的任务。后面的分析可以看到,服务器调用一个任务的Signal 函数,实际上就是将该任务加入到某个任务线程类的任务队列中去。另外,为了统一管理这些任务线程,DSS 还开发了任务线程池类,该类负责生成、删除以及维护内部的任务线程列表。图 4 描述了任务类的运行。下面我们首先分析TashThread 类,该类的定义如下:class TaskThread:public OSThread/OSThread 的子类/提示:所有的Task 对象都将在TaskThread 中运行1 public:2 TaskThread():OSThread(),
33、fTaskThreadPoolElem(this)/构造函数名师资料总结-精品资料欢迎下载-名师精心整理-第 9 页,共 25 页 -3 virtual TaskThread()this-StopAndWaitForThread();/析构函数4 private:5 virtual void Entry();/从 OSThread重载的执行函数,仍然能够被子类重载6 Task*WaitForTask();/检测是否有该执行的任务7 OSQueueElem fTaskThreadPoolElem;/对应的线程池对象8 OSHeap fHeap;/纪录任务运行时间的堆,用于WaitForTask
34、函数/*关键数据结构:任务队列;在Task的 Signal 函数中直接调用fTaskQueue 对象的EnQueue 函数将自己加入任务队列*/9 OSQueue_Blocking fTaskQueue;/此处略 作为 OSThread 的子类,TaskThread 重载了 Entry 函数,一旦TaskThread 的对象被实例化,便运行该函数。Entry()函数的主要任务就是调用WaitForTask()函数监测任务队列,如果发现新任务,就在规定时间内执行;否则,就被阻塞。下面我们简要分析Entry()函数的流程:void TaskThread:Entry()1 Task*theTask=
35、NULL;/空任务2 while(true)/线程循环执行3 /监测是否有需要执行的任务,如果有就返回该任务;否则阻塞;4 theTask=this-WaitForTask();5 Assert(theTask!=NULL);6 Bool16 doneProcessingEvent=false;/尚未处理事件7 while(!doneProcessingEvent)8 9 theTask-fUseThisThread=NULL;/对任务的调度独立于线程10 SInt64 theTimeout=0;/Task 中 Run 函数的返回值,重要/核心部分:运行任务,根据返回值判断任务进度11 if(
36、theTask-fWriteLock)12 /如果任务中有写锁,需要使用写互斥量,否则可能造成死锁13 OSMutexWriteLocker mutexLocker(&TaskThreadPool:sMutexRW);14 theTimeout=theTask-Run();/运行任务,得到返回值15 theTask-fWriteLock=false;16 17 else18 /使用读互斥量19 OSMutexReadLocker mutexLocker(&TaskThreadPool:sMutexRW);20 theTimeout=theTask-Run();/运行任务,得到返回值名师资料总结
37、-精品资料欢迎下载-名师精心整理-第 10 页,共 25 页 -21 22/监测 Task 中 Run()函数的返回值,共有三种情况23/1、返回负数,表明任务已经完全结束24 if(theTimeout fEvents);34 if(doneProcessingEvent)35 theTask=NULL;36/3、返回正数,表明任务希望在等待theTimeout 时间后再次执行37 else38/*将该任务加入到Heap 中,并且纪录它希望等待的时间。Entry()函数将通过waitfortask()函数进行检测,如果等待的时间到了,就再次运行该任务*/39 theTask-fTimerHe
38、apElem.SetV alue(OS:Milliseconds()+theTimeout);40 fHeap.Insert(&theTask-fTimerHeapElem);41(void)atomic_or(&theTask-fEvents,Task:kIdleEvent);/设置 Idle 事件42 doneProcessingEvent=true;43/此处略 注意,如果 Task 的 Run()函数返回值TimeOut 为正数,意味着该任务是一个周期性的工作,例如发送数据的视频泵(pump),需要每隔一定时间就发出一定量的视频数据,直至整个节目结束。为此,在第 3843 行,将该任务
39、加入到堆fHeap 中去,并且标记该任务下次运行的时间为TimeOut 毫秒之后。将来通过调用WaitForTask()函数就能检测到该任务是否到达规定的运行时间,WaitForTask()函数的代码如下:Task*TaskThread:WaitForTask()1 while(true)2 /得到当前时间,该函数为静态函数,定义见OS.h3 SInt64 theCurrentTime=OS:Milliseconds();/*如果堆中有任务,且任务已经到执行时间,返回该任务。PeekMin函 数 见OSHeap.h,窃听堆中第一个元素(但不取出)*/4 if(fHeap.PeekMin()!=
40、NULL)&(fHeap.PeekMin()-GetValue()GetEnclosingObject();名师资料总结-精品资料欢迎下载-名师精心整理-第 11 页,共 25 页 -/如果堆中有任务,但是尚未到执行时间,计算需要等待的时间6 SInt32 theTimeout=0;7 if(fHeap.PeekMin()!=NULL)/计算还需等待的时间8 theTimeout=fHeap.PeekMin()-GetV alue()-theCurrentTime;9 Assert(theTimeout=0);/等待 theTimeout 时间后从堆中取出任务返回10 OSQueueElem*
41、theElem=fTaskQueue.DeQueueBlocking(this,theTimeout);11 if(theElem!=NULL)12 return(Task*)theElem-GetEnclosingObject();13 上文曾经提到,Task 对象内有两个方法:Signal 和 Run。Run 函数是一个虚函数,由Task 的子类重载,它的用法我们在分析TaskThread 的 Entry()函数和 WaitForTask()函数中已经讨论了。而另 一个Signal()函数 也十分 重要:服务器 通过调 用该函 数将Task 加入TaskThread,并且执行Run()函数
42、。Signal()函数的核心部分如下:void Task:Signal(EventFlags events)/fUseThisThread 用于指定该任务运行的任务线程1 if(fUseThisThread!=NULL)/存在指定任务线程/将该任务加入到指定任务线程的任务队列中2 fUseThisThread-fTaskQueue.EnQueue(&fTaskQueueElem);/不存在指定的任务线程,随机选择一个任务线程运行该任务3 else4/从线程池中随机选择一个任务线程5 unsigned int theThread=atomic_add(&sThreadPicker,1);6 th
43、eThread%=TaskThreadPool:sNumTaskThreads;/将该任务加入到上面选择的任务线程的任务队列中7 TaskThreadPool:sTaskThreadArraytheThread-fTaskQueue.EnQueue(&fTaskQueueElem);8 至此我们已经将DSS 的线程和任务运行机制分析完了,这种由事件去触发任务的概念已经被集成到了DSS 的各个子系统中。例如,在 DSS 中经常将一个Task 对象和一个Socket对象关联在一起,当Socket 对象收到事件(通过select()函数),相对应的Task 对象就会被传信(通过Signal()函数)
44、;而包含着处理代码的Run()函数就将在某个任务线程中运行。因此,通过使用这些Task 对象,我们就可以让所有连接都使用一个线程来处理,这也是 DSS 的缺省配置方法。3.3 Socket 类作为一个典型的网络服务器,DSS 源代码中的Socket 编程部分是其精华之一。DSS 定义了一系列Socket 类用于屏蔽不同平台在TCP/UDP 编程接口和使用方法上的差异。DSS 中的名师资料总结-精品资料欢迎下载-名师精心整理-第 12 页,共 25 页 -Socket 类一般都采用异步模式的(即非阻塞的),而且能够向对应的Task 对象传信(Signal),这点我们在上一节介绍过。Socket
45、类中具有代表性的类是:EventContext、EventThread、Socket、UDPSocket、TCPSocket 以及 TCPListenerSocket 等等,它们之间的继承关系见图5。在 eventcontext.h/.cpp 文件中,定义了两个类:EventContext 类和 EventThread 类。Event Context 提供了检测Unix 式的文件描述符(Socket 就是一种文件描述符)产生的事件(通常是 EV_RE 或EV_WR)的能力,同时还可以传信指定的任务。EventThread 类是 OSThread类的子类,它本身很简单,只是重载了OSThread
46、 的纯虚函数Entry(),用以监控所有的Socket端口是否有数据到来,其代码分析如下:void EventThread:Entry()/*该结构定义在ev.h 中,记录 Socket 描述符和在该描述符上发生的事件*/1 struct eventreq theCurrentEvent;2:memset(&theCurrentEvent,0,sizeof(theCurrentEvent);/初始化该结构3 while(true)4/首先监听Socket 端口的事件5 int theErrno=EINTR;6 while(theErrno=EINTR)7 8#if MACOSXEVENTQUE
47、UE/Macos 平台9 int theReturnValue=waitevent(&theCurrentEvent,NULL);10#else/其他平台/*调用 select_waitevent 函数监听所有的Socket 端口,直到有事件发生为止*/11 int theReturnValue=select_waitevent(&theCurrentEvent,NULL);12#endif/有事件发生,唤醒相应的Socket 端口13 if(theCurrentEvent.er_data!=NULL)14/通过事件中的标识找到相应的对象参考指针15 StrPtrLen idStr(char*
48、)&theCurrentEvent.er_data,sizeof(theCurrentEvent.er_data);16 OSRef*ref=fRefTable.Resolve(&idStr);17 if(ref!=NULL)18 /通过参考指针得到EventContext 对象19 EventContext*theContext=(EventContext*)ref-GetObject();/利用 EventContext 对象的 ProcessEvent 方法传信对应的Task20 theContext-ProcessEvent(theCurrentEvent.er_eventbits);
49、名师资料总结-精品资料欢迎下载-名师精心整理-第 13 页,共 25 页 -21 fRefTable.Release(ref);/减少引用计数22/此处略 上述代码有两点需要注意:首先在第11 行,调用select_waitevent 函数监听所有Socket端口的事件。该函数在Windows 平台上是采用WSAAsyncSelect(异步选择)模型实现的。具体实现是:系统首先创建一个窗口类,该类专门用于接受消息;在每个Socket 端口创建后,调用WSAsyncSelect 函数,同时将上述窗口类的句柄作为参数传入;将来这些Socket端口有事件发生时,Windows 就会自动将这些事件映射
50、为标准的Windows 消息发送给窗口类,此时select_waitevent 函数通过检查消息就能够获得对应Socket 端口发生的事件。对于Windows 平台下 Socket 的异步编程技术细节请参阅Windows 网络编程技术一书。另外,在第20 行调用的EventContext 对象的 ProcessEvent 函数实现上很简单,只有一行代码:fTask-Signal(Task:kReadEvent);其中 fTask 为该 EventContext 对象对应的Task 对象;ProcessEvent 函数向 Task 对象传信,以便及时处理刚刚发生的Socket 事件。与 Even