LIVE555原始碼研究之四:MediaServer (一)
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.19於浙江杭州
相關文章
- LIVE555研究之三:LIVE555基礎
- Live555原始碼解析(4) - 魚兒上鉤來原始碼
- 深入研究 Laravel 原始碼第四天Laravel原始碼
- Foursquare 原始碼研究之------登入續原始碼
- WebRTC研究 (一) 編譯原始碼Web編譯原始碼
- 【Zookeeper】原始碼分析之伺服器(四)之FollowerZooKeeperServer原始碼伺服器Server
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- Nginx原始碼研究之nginx限流模組詳解Nginx原始碼
- Foursquare 原始碼研究之---------使用者登入原始碼
- PHP原始碼研究PHP原始碼
- ICE原始碼研究原始碼
- Java併發之ReentrantLock原始碼解析(四)JavaReentrantLock原始碼
- redux原始碼分析之四:compose函式Redux原始碼函式
- Spring原始碼之Bean的載入(四)Spring原始碼Bean
- 深入Weex系列(四)之Module元件原始碼解析元件原始碼
- 【JUC】JDK1.8原始碼分析之CyclicBarrier(四)JDK原始碼
- 原始碼探探之StartActivity(一)原始碼
- Spring原始碼之IOC(一)BeanDefinition原始碼解析Spring原始碼Bean
- Linux核心原始碼分析之setup_arch (四)Linux原始碼
- 【Zookeeper】原始碼分析之請求處理鏈(四)之FinalRequestProcessor原始碼
- preact原始碼分析(四)React原始碼
- SDWebImage原始碼解析(四)Web原始碼
- spring原始碼解析之IOC容器(四)——屬性注入Spring原始碼
- 【JUC】JDK1.8原始碼分析之LinkedBlockingQueue(四)JDK原始碼BloC
- 【集合框架】JDK1.8原始碼分析之IdentityHashMap(四)框架JDK原始碼IDEHashMap
- 深入研究 Laravel 原始碼第一天Laravel原始碼
- Struts原始碼研究發現的一個問題原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- jQuery 原始碼分析第一篇之入口原始碼jQuery原始碼
- 【PHP7原始碼分析】PHP7原始碼研究之淺談Zend虛擬機器PHP原始碼虛擬機
- Spring原始碼分析之IoC(一)Spring原始碼
- Spring原始碼解析之BeanFactoryPostProcessor(一)Spring原始碼Bean
- Free5GC原始碼研究(4) - AUSF研究GC原始碼
- Free5GC原始碼研究(5) - NRF研究GC原始碼
- 死磕 java集合之TreeMap原始碼分析(四)-內含彩蛋Java原始碼
- Spring Security系列之核心過濾器原始碼分析(四)Spring過濾器原始碼
- Spring Cloud系列(四):Eureka原始碼解析之客戶端SpringCloud原始碼客戶端
- FaceBook POP原始碼解析四原始碼