muduo庫整體架構簡析


  muduo是一個高質量的Reactor網絡庫,采用one loop per thread + thread pool架構實現,代碼簡潔,邏輯清晰,是學習網絡編程的很好的典范。

  muduo的代碼分為兩部分,base和net,base部分實現一些基礎功能,例如log, thread, threadpool, mutex, queue 等,這些基礎模塊在后面網絡庫中很多地方都可以復用, base庫的類相互之間耦合性較低,源碼閱讀起來並不困難,此處不做過多探究。

 net部分使用base中的工具類實現更高層次的邏輯,網絡編程無非是對socket和其使用的epoll/poll等進行封裝,使其便於使用,屏蔽掉底層網絡庫的一些 "坑", 在滿足了基礎的網絡IO之后,就需要考慮高性能,高並發的問題,muduo 的是由poll/epoll 這些IO復用模型構成,但是單個IO線程在面對大量請求時難免處理不過來,所以就需要結合多線程或者線程池,一個線程對應一個epoll進行網絡IO,這樣就可以充分利用硬件多核系統。從軟硬兩方面綜合提升性能。net部分封裝的較為徹底,對上層提供的接口簡單易用,所以涉及復雜的內部處理,接下來就對其內部實現進行探究。

  下面這張圖是陳碩提供的muduo 網絡庫的類圖,本次講解主要是圍繞下面這張圖,弄明白這樣就相當於弄明白這個架構了。(圖中灰色的類是內部類,白色的是外部類)

  首先是EvenLoop類,他是事件循環(反應器 Reactor),每個線程只能有一個 EventLoop 實體,它負責 IO 和定時器事件的分派。 它用 TimerQueue 作為計時器管理,用 Poller 作為 IO Multiplexing。TimeQueue底層使用timerfd_*系列函數將定時器轉換為fd添加到事件循環中,當時間到達后就會自動觸發事件, 其內部使用 set 管理一些注冊好的Timer,由於set有自動排序功能,所以注冊到事件循環的總是第一個需要處理的Timer。Poller是IO mutiplexing的實現,它是一個抽象類,具體實現由其子類PollPoller (封裝poll), EpollPoller (封裝epoll) 實現,這是muduo庫中唯一一個用面向對象的思想實現的,通過虛函數提供回調功能。Poll中的updateChannel方法用於注冊和更新關注的事件,所有的 fd 都需要調用它添加到事件循環中。 除了用TiemQueue和Poller管理時間事件和IO事件外,EvenLoop還包含一個任務隊列,它用來做一些計算任務,你可以將自己的任務添加到任務隊列中,EvenLoop在一次事件循環中處理完IO事件就會進行依次取出這些任務進行執行,這樣當多個線程需要處理同一資源時可以減少鎖的復雜性, 將資源的管理固定地交由一個線程來處理,其他線程對資源的處理只需要添加到該線程的任務隊列中,由該線程異步執行, 如此只需要在任務隊列加鎖即可,其他地方無需上鎖,減少鎖的濫用。 但是有一個問題,如果EvenLoop阻塞在epoll_wait處就無法處理這些計算任務了,畢竟計算任務是在處理完IO事件后才執行的,所以此時需要通過某種通信方式喚醒該線程,被喚醒后就取出隊列中的任務進行執行。muduo采用 eventfd(2) 來異步喚醒。

  muduo中通過Channel對fd 進行封裝,其實更合適的說法是對fd事件相關方法的封裝,例如負責注冊fd的可讀或可寫事件到EvenLoop,又如fd產生事件后要如何響應。 一個fd對應一個channel, 它們是聚合關系,Channel在析構函數中並不會close掉這個fd。 它有一個handleEvent方法,當該fd有事件產生時EvenLoop會調用handleEvent方法進行處理,在handleEvent內部根據可讀或可寫事件調用不同的回調函數(回調函數可事先注冊)。 它一般做為其他類的成員,例如EvenLoop通過一個vector<Channel*> 對注冊到其內的眾多fd的管理,畢竟有了Channel就有了fd及其對應的事件處理方法,所以你會看到上圖中EvenLoop與Channel是一對多的關系。

  Socket也是對fd的封裝,但不同與channel, 它僅封裝 ::socket 產生的fd, 並且提供的方法也是一些獲取或設置網絡連接屬性的方法,他和 fd 是組合關系,當Socke析構時會close掉這個fd。不管如何封裝fd, 一些系統函數傳遞的參數總是fd,所以你會看到上圖中一些類中既有 fd 又有Channel或Socket, 這也是在所難免的。

  TcpConection是對一個連接的抽象,一個TcpConnection包含一個Socket和一個Channel,  上面說到channel::handleEvent會在產生事件后調用事先注冊的回調函數,其實在TcpConnection構造的時候就會為其所屬的Channel注冊好這些回調函數,handleRead,handleWrite....分別對應可讀可寫事件產生后調用的回調函數。事件產生后會調用handleRead(或handleWrite), TcpConceton會在handleRead中做一些處理,然后轉交給上層,提交到上層的具體體現就是調用上層注冊的回調函數(又是一樣的套路😊),因為Channel是個內部類,所以它的回調函數的注冊只能TcpConnection完成,上層只需要將回調函數注冊到TcpConnection后其會自動處理。之所以加了TcpConnection這一層是為了解決Tcp協議收發數據時阻塞問題,比如::write時內核緩沖區滿了,只能等到下次EPOLLOUT事件產生后再寫,對於一個牛逼的網絡庫來說,應該是上層只需調用一次sendMessage發送全部數據,網絡庫內部將數據分批::write給peer, TcpConnection就實現了這一點,它內部維護一個應用層的inputBuffer和outputBuffer保證數據的可靠發送,同時再連接斷開時也能保證數據發送完成后再斷開。

  Accepter用來接受連接,其內是維護一個listenfd及其對應的channel, 他會對listenfd進行一些初始化操作例如::bind, ::listen,然后就調用channel的方法注冊到事件循環中,事件產生后回調其handleRead進行accept連接,然后調用上層注冊的newConnectonCallback回調函數。Accepter是個內部類它屬於一個TcpServer,  Accepter::newConnectionCallback的實例就是由TcpServer進行注冊的, 連接產生后TcpServer主要對這個連接創建一個TcpConnection對象並設置好其對應的回調函數,TcpServer維護一個TcpConnection的map來管理連接到他的client, TcpSever通過線程池來處理並發請求,線程池內是多個IO線程也就是多個EvenLoop,每個連接到來回自動分配到其中一個來創建TcpConnection相關。這樣就大大提高了TcpServer的請求處理能力!

  Connector與TcpClient的關系如同Accepter與TcpServer的關系,只不過它使用來發起主動連接的,它需要注意的就是自動重連等這些功能。

  掌握上面這些就大致明白整個架構了,因為muduo是基於對象的編程思想,所以上面涉及到了很多回調函數之類的,其實這只是muduo庫暴露給用戶的接口而已,系統經過層層封裝后,每層都需要向上提供定制化的回調函數,所以看起來就比較亂了,慢慢捋就好了

  這里只是做個簡單的總結,內部還有好多實現細節值得我們學習。同時陳碩對一些C++的使用方式也值得我們去探究,希望我們能在這些大牛的后面快速成長吧。

      參考:  陳碩的博客 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM