4.live555mediaserver-第一次select
這是[手把手一起學live555]的第5篇(按這個序號看,請找正確順序看)。
live555工程在我的gitee下(doc下有思維導圖、drawio圖):https://gitee.com/lure_ai/live555/tree/master
學習demo
live555mediaserver.cpp
學習線索和姿勢
1.學習的線索和姿勢
網絡編程
流媒體的地基是網絡編程(socket編程)。
[網絡編程學習]-0.學習路線。
繪圖規則
本文的對象圖和思維導圖遵守的規則詳見:
2.繪圖規則
本節內容和目標
(1)TCP非阻塞服務端網絡編程流程第一個select節點(TCP非阻塞服務網絡編程流程:socket創建、bind、listen、select、accept、select、recvfrom/send、close)
(2)思維導圖繪制
(3)對象圖繪制
(4)c++純虛函數繼承知識
(5)雙向循環鏈表
正式開始
3.live555mediaserver學習-從socket創建到listen已經追蹤到了TCP非阻塞服務端網絡編程的listen了,本節主要講解接下來的流程:第一個select。
對于阻塞式服務端網絡編程模式,listen后一般直接accept調用,然后阻塞等待客戶端的鏈接,但是上一節我們一起知道live555是TCP非阻塞服務端網絡編程,非阻塞accept是立即返回結果有還是沒有,這怎么辦?開個線程不斷輪詢?這也太廢cpu了,那怎么辦?IO多路復用呀!一般用select呀(幾十幾百個客戶端鏈接下夠用了)。
這個時候我們得把這個服務端的socket找個地方管理起來呀!在哪管理呢?而且最好和已連接的客戶端socket一起管理。因為select會放到一個線程里面跑——一個select只監聽一個socket也太大材小用了!——會不會合理利用線程資源?我們的目標應該是一個線程中一個select不僅監聽服務端socket還要能監聽已鏈接的客戶端socket,這樣線程資源利用最大化,這樣才能榨干CPU性能!
這種需求,就要求我們要把服務端和已連接的客戶端socket統一管理起來!具體方案呢,就是套接字放在一起select進行監聽,然后監聽到了就去匹配socket,匹配到socket則執行對應方法。那么兩部分,一部分是socket、方法等信息要保存,這可以用鏈表鏈起來統一管理,另外就是select監聽集里要放進去,這個操作要操作下。
這是總體思路,live555也是這么做的。這我怎么知道的?我也是看過后才說這些話的。
我是怎么發現的呢?偶然也是必然,追根結底是因為我看代碼是懷著網絡編程的線索來看的——創建socket后必然是bind,bind后必然是listen,listen后不用想必然是select,select后必然是accept,accept后必然又是select,select后必然是recvfrom/send——這是網絡編程天然的線索!小樣兒!你這live555代碼,能跑哪里去?!(可惜網上沒有人從這個角度出發去分析的)
那么來看下live555是如何實現這個方案的。
先把上節的一張圖搬來,如下。
3.live555mediaserver學習-從socket創建到listen已經把DynamicRTSPServer::createNew里調用的方法setUpOurSocket講完了,那么接著就要new DynamicRTSPServer來創建第3個對象了。
那么創建這第3個對象DynamicRTSPServer做了什么操作?和我們的下一個TCP非阻塞服務端網絡編程的流程select又有什么關系呢?(其實呢,答案就在上面)
來看下,new這第3個對象的思維導圖:
因為思維導圖太長了,截圖分成了3份再合并到一個圖里了——推薦一個非常好用的截圖工具——網上搜下snipaste自然知道——誰用誰知道。
如上思維導圖,創建DynamicRTSPServer對象的過程都干了什么事?
我只撿我關注的點說:
(1)保存服務端監聽套接字socket、端口和請求方法,并放到鏈表中去管理。
(2)把服務端監聽socket放到select的監聽集里。
(3)把環境對象BasicUsageEnvironment的基類UsageEnvironment引用保存到對象DynamicRTSPServer基類Medium的成員fEnviron中(見下圖)。
(4)其他。
那么現在咱們只需關注(1)和(2)。看下它們是如何實現的,我覺得先上圖吧(思維導圖你說看的太長不懂,那就結合下面對象圖吧!),這樣容易理解!我把創建DynamicRTSPServer對象的流動圖畫出來,如下。
我前面為啥沒講前2個類對象的創建呢?因為直到第3個類對象DynamicRTSPServer創建時它們才派上用場!它們沒用的時候,我去干巴巴地講它們創建、分析它們有啥意思?
從上圖看再結合思維導圖和代碼,我發現:
創建DynamicRTSPServer對象時,拿走了BasicUsageEnvironment對象的基類UsageEnvironment的引用,保存到它的基類Medium的成員fEnviron中。
而創建UsageEnvironment對象創建時,拿走了BasicTaskScheduler對象的基類TaskScheduler的引用,保存到它的基類UsageEnvironment的成員fScheduler中。
——這種創建子類,拿走基類指針/引用來使用的模式叫做接口繼承。 ——這可是多態的基礎呀,屏蔽了子類實現的細節。可查看我相關博客。這樣的話,第3個兄弟(第3個創建的對象)可以調用前面2個兄弟的方法和屬性——記住這一點(其實類似c語言的指針,你就可以這么理解)——我才稱之為3兄弟。
然后你說你還是不知道這有啥用?你說沒看到我說的流動,那是你沒有仔細看,也許是我畫的那個虛線箭頭太細了,好吧,我給你指出來吧,如下圖。
圖3-1 DynamicRTSPServer對象構造流動
看到了么,這是創建第三個兄弟對象DynamicRTSPServer時的流動。——我把它創建時的構造函數的操作進行了圖形化顯示!(我真想寫成ppt,這樣子的話會好許多)
現在拿著上面的對象流動圖結合之前的思維導圖,再結合代碼,來和我一起分析下這對象的構造流程:
來先從圖3-1的紅色數字1流動方向看起來,建議把上面的圖放到旁邊對照代碼、思維導圖看——建議這個圖保存下來,或者用snipaste截圖工具F1再按F3直接可以駐留當前屏幕——可以任意拖放到屏幕的任何位置,滾輪滾動可以放大縮小,且不影響你看代碼和我這篇文筆記。
如圖3-1的紅色數字1的流程方向,這是DynamicRTSPServer對象創建時執行其父類構造函數GenericMediaServer::GenericMediaServer中會有這一句調用:
env.taskScheduler().turnOnBackgroundReadHandling(fServerSocketIPv4, incomingConnectionHandlerIPv4, this)
而它這個調用鏈就長了,env是第1個對象BasicUsageEnvironment的基類UsageEnvironment引用,env.taskScheduler()就取到了第1個對象BasicTaskScheduler的基類TaskScheduler的引用,然后這就是調用第1個對象的方法turnOnBackgroundReadHandling的走向,即圖3-1的紅色數字2的走向,接著就走圖3-1的紅色數字就是3、4的流向——2調用3但是3是個純虛函數,它的實現是在子類的子類實現的——紅色數字4的流向。——這就涉及到c++的純虛函數繼承的知識點。
c++純虛函數繼承
有沒有想過為啥TaskScheduler::setBackgroundHandling這個虛函數不在它的子類
BasicTaskScheduler0實現的,而是在它子類的子類TaskScheduler里實現的?不是說繼承父類包含純虛函數的子類不實現該純虛函數會編譯報錯么?
注意注意,這個知識點——如果繼承的父類含有純虛方法的子類不去創建子類對象則不實現該純虛方法也是可以的,沒問題的。
而我們上圖中的TaskScheduler::setBackgroundHandling純虛方法在它子類的子類里實現的,是因為沒人去實例化它的子類,反而是會實例化它的孫子輩類——BasicTaskScheduler類,所以在它的孫子輩類實現了該純虛方法。——另外它的其他純虛方法已經在它的子類BasicTaskScheduler0里實現了,總體來說實例化孫子輩類——BasicTaskScheduler類,所有父類的純虛方法都已經被實現了。所有沒問題。
回來,繼續看我們的圖的流向——最終找到了紅色數字4流向的節點,即最終調用的是:
BasicTaskScheduler::setBackgroundHandling(fServerSocketIPv4, SOCKET_READABLE, incomingConnectionHandlerIPv4, this),
如下圖:
因為傳入的第二個形參是SOCKET_READABLE,所以在這里它會把這個服務端socket加入監聽可讀的socket集(BasicTaskScheduler的成員fReadSet)里。
這樣就把服務端socket添加到select的監聽集里面了!此時,我們前面的一個構思實現了。接著就是看它是怎么將socket和對應的執行方法保存起來放到鏈表里管理的了。 ——離select越來越近了!
它又調了這個函數:
fHandlers->assignHandler(fServerSocketIPv4, SOCKET_READABLE, incomingConnectionHandlerIPv4, this)
這對應于圖3-1的紅色數字5
既然調用了BasicTaskScheduler0的成員fHandlers的assignHandler方法,那我先介紹下fHandlers在什么時候創建的,是什么類型,然后再介紹這個被調用的方法。
BasicTaskScheduler0的成員fHandlers創建時機
來,看圖3-1,BasicTaskScheduler0的成員fHandlers是在什么時機創建的呢?是在第一個兄弟BasicTaskScheduler對象new出來的時候創建的。如下圖,BasicTaskScheduler的父類BasicTaskScheduler0的構造函數里new HandlerSet出來后給成員fHandlers了,fHandlers的類型自然是 HandlerSet*。
BasicTaskScheduler0的成員fHandlers所在位置圖形化表示如下圖。
如上圖,它指向的對象是圖中左上的HandlerSet類對象——它連接到了圖中右邊BasicTaskScheduler0類的成員fHandlers(這個時候是實線+實心棱形是包含關系)。——注意,HandlerSet類里也有一個成員叫fHandlers——但是類型和BasicTaskScheduler0類的成員fHandlers不一樣,不同類的成員只是同名而已,可不要混淆了——搞不懂為啥起一樣的成員名字,難道沒發現閱讀代碼會誤導人么?——幸好我有這圖。
而HandlerSet是管理鏈表的對象。它的成員fHandlers指向鏈表頭。在new HandlerSet時,它的構造函數HandlerSet::HandlerSet會調用這個鏈表頭的構造函數HandlerDescriptor::HandlerDescriptor,在這里它會把鏈表頭結點HandlerSet的成員fHandlers初始化完畢——主要是將next和prev都指向它自己,如下函數整理(用截圖工具spinaste繪制)。
分析下上面這個構造函數,它的形參nextHandler是指向鏈表的隊尾(隊頭在最右,隊尾在最左),如果隊尾和這個構造函數創建的對象地址一樣,那說明這個形參就是鏈表頭——說明你要初始化鏈表頭呀——這時走if語句。如果鏈表不為空,那么形參nextHandler指向的鏈表尾和當前的新開辟的對象地址肯定不一樣,那此時走else流程。
而上圖就是介紹鏈表頭是如何初始化的——在HandlerSet::HandlerSet里初始化fHandlers時,調用HandlerDescriptor構造函數傳入的形參是它本身的地址——那形參nextHandler傳遞的就是鏈表頭的地址,this也是指向這個鏈表頭,所以走的是if語句。鏈表頭初始化后的模樣如下圖。
可以看到,此時,鏈表頭next和prev都是指向自己——自己指向自己的地址,自己和自己玩耍——鏈表頭表示很孤單、很寂寞、很冷。
到這介紹完BasicTaskScheduler0的成員fHandlers是何時創建及初始化的了。那么繼續分析調用的它的方法assignHandler,對應圖3-1 紅色數字5的流向,如下圖HandlerSet::assignHandler做了什么呢?它做了兩件事:
(1)調用new HandlerDescriptor并添加到 HandlerSet 維護的fHandlers鏈表中去。這就完成了之前我們構思的鏈表管理了。——對應到圖3-2的紅色數字6。
(2)把服務端監聽socket及其對應的處理函數等都傳給這個new出來的鏈表成員對象了——HandlerDescriptor類對象了。它的作用(意義):
保存服務端監聽socket用以select匹配監聽到后是哪個socket
保存對應的執行方法——select只要監聽到有新客戶端鏈接后就執行對應方法,保存的方法如下:
GenericMediaServer::incomingConnectionHandlerIPv4(void* instance, int /mask/)。
這個鏈表成員是第1個入隊的,和鏈表頭是元老,固定的——保存服務端socket等信息的鏈表隊員。它也是唯一的、且不重復的,因為只創建了一個服務端監聽socket。
第2條不用多講,詳細分析下第1條的創建鏈表新隊員并入隊的過程——有意思,new HandlerDescriptor時調用的構造函數如下圖。
形參nextHandler指向隊尾,此時隊尾就是鏈表頭——鏈表隊列為空時,隊尾和隊頭都指向鏈表頭。新new出來的這個隊員(this)肯定和隊尾(nextHandler)不是同一個地址,上圖走else流程——一頓操作后,就把新隊員掛到了HandlerSet對象成員fHandlers這個鏈表頭的左邊,成為了鏈表的一員。此時,鏈表圖的模樣如下:
鏈表頭終于不孤單了——鏈表有2個成員了,一個是隊長,一個是隊員,好基友。
** 可以看到這個是雙向循環鏈表**。
——實際上,此時鏈表成員應該有3個,一個是鏈表頭,一個是保存IPV4服務端端口554/8554的信息的隊員,一個是保存IPV6服務端端口554/8554的信息的隊員,不想畫了,一樣的流程。——記住此時鏈表已經有2個隊員了(去除鏈表頭這個隊長)
但是這個鏈表隊列特殊的是new HandlerDescriptor的時候形參傳入隊尾的指針,新隊員就順勢把自己插入鏈表了——原來鏈表插入可以這么搞的!!大開眼界!
到此,我們開頭的第2個構思實現了。
到此,new DynamicRTSPServer的操作已經講完了。
到此,也已經把socket加入select監聽集和鏈表管理流程講完了。
好了,怎么樣,結合對象圖、思維導圖和代碼,是不是非常容易理解代碼?而且關鍵是圖形化了!我的目標和追求就是看完后,這些圖能在大腦中動起來!
視野再切換到main函數里,按順序應該是下面的流程了:
略過那個IP打印——這個主要是獲取本地的諸多網卡中的一個網卡地址——因為創建服務端socket時綁定的IP是ANY——只要你往你本地的任意一個網卡發送數據它都會接收到如果是傳給554/8554那它都能收到——后面可以實驗下,url的IP改成本地的任意一個網卡IP,這個服務器照樣能收到。
注意這個IP地址打印的IP信息如果獲取到ipv4的地址了就不再去獲取ipv6的地址了。
接下來是http的服務端socket創建,它要綁定80/8000/8080端口——和我們創建服務端socket并綁定554/8554的流程一樣,不分析了,重復了——最后結果是:創建了2個新的鏈表隊員放到鏈表里,然后把這2個socket加入select監聽集。——一個是保存IPV4的http服務端socket信息,另一個是保存IPV6的http服務端socket信息。——這塊就不展開了,和我們之前講的流程再重復一次就行了。
此時,實際的鏈表圖我也不畫了,我只介紹下就行了,此時鏈表里應該有4個隊員+一個鏈表頭隊員共5個成員。具體,從右到左:
鏈表頭(隊長),
第1個隊員:IPV4的綁定554/8554端口的服務端監聽socket隊員,對應方法GenericMediaServer::incomingConnectionHandlerIPv4(void* instance, int /mask/),
第2個隊員:
IPV6的綁定554/8554端口的服務端監聽socket隊員,對應方法GenericMediaServer::incomingConnectionHandlerIPv6(void* instance, int /mask/),
第3個隊員:
IPV4的綁定80/8000/8080端口的服務端監聽socket隊員,對應方法RTSPServer::incomingConnectionHandlerHTTPIPv4(void* instance, int /mask/)
第4個隊員:
IPV6的綁定80/8000/8080端口的服務端監聽socket隊員,對應方法RTSPServer::incomingConnectionHandlerHTTPIPv6(void* instance, int /mask/)
其實畫圖也好畫——在后續的總結里會放出該圖。
從這可以看到live555的伸縮性很強——支持標準rtsp端口554/8554,支持rtsp over http(端口80/8000/8080)。
我這一節的目標是select在哪里?——鋪墊了這么多,準備了這么多,終于才能來到select身邊——live555mediaserver.cpp的main函數的最后2句,如下圖。
來上我的思維導圖:
select在哪里? 在BasicTaskScheduler::SingleStep里,你又會問:你怎么又知道?來上圖,我慢慢道來。
圖3-2 main循環和select追蹤
先看下調用代碼:
env->taskScheduler().doEventLoop();
其中env->taskScheduler()就是拿到了對象BasicTaskScheduler基類TaskScheduler的引用,而再.doEventLoop()就是調用基類TaskScheduler的成員doEventLoop(對應圖3-2數字1的流向),而它是純虛方法,它的實現是在子類BasicTaskScheduler0來實現的(對應圖3-2數字2的流向)來看下它的實現:
它是個while循環,變量watchVariable默認是NULL的。在這個循環里,它調用了BasicTaskScheduler0::SingleStep(對應圖3-2數字3的流向),然鵝,它也是個純虛函數:
它的實現在在哪呢?也在它的子類——BasicTaskScheduler類里實現的——最后指向了BasicTaskScheduler::SingleStep(對應圖3-2數字4的流向),用snipaste截圖工具截取了下我關注的點(很容易截取幾秒種3步搞定——就問你想不想用這么好的截圖工具?):
而我們前面已經說過在BasicTaskScheduler::setBackgroundHandling里面,已經將服務端socket添加到BasicTaskScheduler的socket監控集fReadSet里了,而這個時候,BasicTaskScheduler::SingleStep里就把它進行select監聽了。——原來select在這里!
此時,萬事具備只欠東風了——只待客戶端來發起鏈接了。
好,本次select流程節點結束,下一節追蹤accept流程節點。
小結
我發現在select前,live555做了很多準備工作,主要是2個:第一個是添加socket到select監控集,第二個創建一個鏈表成員將socket和對應的方法等保存起來,并用雙向循環鏈表管理起來。
也就是說前半部分是為select做了許多準備,導致我們這節前部分內容也很長,但是最后高潮部分卻特別短。
總結
以上是生活随笔為你收集整理的4.live555mediaserver-第一次select的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt编写的SMTP客户端(库)
- 下一篇: [转载]三、二、一 …… Geronim