Linux(muduo網絡庫):18---muduo簡介之(muduo庫的由來、編譯安裝、目錄結構、代碼結構、線程模型)

一、由來

  • 2010年3月陳碩先生寫了一篇《學之者生,用之者死——ACE歷史與簡評》(文章參閱:https://blog.csdn.net/Solstice/article/details/5364096),其中提到“我心目中理想的網絡庫”的樣子:

    • 線程安全,原生支持多核多線程
    • 不考慮可移植性,不跨平臺,只支持Linux,不支持Windows。 ·主要支持x86-64,兼顧IA32。(實際上muduo也可以運行在ARM 上)
    • 不支持UDP,只支持TCP
    • 不支持IPv6,只支持IPv4
    • 不考慮廣域網應用,只考慮局域網。(實際上muduo也可以用在廣 域網上)
    • 不考慮公網,只考慮內網。不爲安全性做特別的增強
    • 只支持一種使用模式:非阻塞IO+one event loop per thread,不支持阻塞IO
    • API簡單易用,只暴露具體類和標準庫裏的類。API不使用nontrivial templates,也不使用虛函數
    • 只滿足常用需求的90%,不面面俱到,必要的時候以app來適應 lib
    • 只做library,不做成framework
    • 爭取全部代碼在5000行以內(不含測試)
    • 在不增加複雜度的前提下可以支持FreeBSD/Darwin,方便將來用 Mac作爲開發用機,但不爲它做性能優化。也就是說,IO multiplexing使用poll和epoll
    • 以上條件都滿足時,可以考慮搭配Google Protocol Buffers RPC
  • 在想清楚這些目標之後,陳碩開始第三次嘗試編寫自己的C++網絡庫:

爲什麼需要網絡庫?

  • 使用Sockets API進行網絡編程是很容易上手的一項技術,花半天時間讀完一兩篇網上教程,相信不難寫出能相互連通的網絡程序
  • 例如下面這個網絡服務端和客戶端程序,它用Python實現了一個簡單的“Hello”協議,客戶端發來姓名,服務端返回問候語和服務器的當前時間

  • 上面兩個程序使用了全部主要的SocketsAPI,包括socket、 bind、listen、accept、connect、recv、send、close、 gethostbyname等,似乎網絡編程一點也不難嘛
  • 在同一臺機器上運 行上面的服務端和客戶端,結果不出意料:

  • 但是連接同一局域網的另外一臺服務器時,收到的數據是不完整的。錯在哪裏?

  • 出現這種情況的原因是:高級語言(Java、Python等)的Sockets庫並沒有對Sockets API提供更高層的封裝,直接用它編寫網絡程序很容易掉到陷阱裏,因此我們需要一個好的網絡庫來降低開發難度。網絡庫的價值還在於能方便地處理併發連接(參閱後面的“詳解muduo多線程模型”文章)

二、安裝

  • 安裝注意事項:
    • muduo使用了Linux較新的系統調用(主要是timerfd和eventfd),要求Linux的內核版本大於2.6.28
    • 我自己用Debian 6.0 Squeeze / Ubuntu 10.04 LTS作爲主要開發環境(內核版本2.6.32),以g++ 4.4爲主要編譯器版本,在32-bit和64-bit x86系統都編譯測試通過
    • muduo在Fedora 13 和CentOS 6上也能正常編譯運行,還有熱心網友爲Arch Linux編寫了AUR文件(http://aur.archlinux.org/packages.php?ID=49251
    • 如果要在較舊的Linux 2.6內核(例如Debian 5.0 Lenny、Ubuntu 8.04、CentOS 5等舊版本)上使用muduo,可以參考backport.diff來修改代碼。不過這些系統上沒有充分測試,僅僅是編譯和冒煙測試通過
    • 另外muduo也可以運行在嵌入式系統中,我在Samsung S3C2440開 發板(ARM9)和Raspberry Pi(ARM11)上成功運行了muduo的多個示例。代碼只需略作改動,請參考armlinux.diff
  • 安裝過程如下:

第一步(安裝前準備)

  • 第一步:muduo採用CMake爲build system,CMake的安裝如下:(CMake最好不低於2.8版,CentOS 6自帶的2.6版也能用,但是無法自動識別Protobuf庫)
sudo apt-get install cmake

sudo apt-get install g++

  • 第二步:muduo依賴於Boost,Boost的安裝如下
sudo apt-get install libboost-dev libboost-test-dev

  •  第三步(可選):muduo有三個非必須的依賴庫(curl、c-ares DNS、Google Protobuf)。如果安裝了這三個庫,cmake會自動多編譯一些示例。安裝方法如下:
sudo apt-get install libcurl4-openssl-dev libc-ares-dev
sudo apt-get install protobuf-compiler libprotobuf-dev

 

第二步(編譯、安裝muduo)

  • 第一步:下載muduo源碼包
git clone https://github.com/chenshuo/muduo.git

 

  • 第二步:編譯muduo,命令如下:
# 下載完成之後進入muduo根目錄
cd muduo

# 編譯muduo庫和它自帶的例子
./build.sh -j2

  • 編譯完成之後:
    • 會在muduo源碼根路徑的上一級路徑下生成一個build目錄(下面全文我們以../build表示)
    • 生成的可執行文件位於:../build/release-cpp11/bin
    • 靜態文件位於:../build/release-cpp11/lib

  • 第三步:安裝muduo庫
./build.sh install

 

  • 默認情況下:
    • muduo頭文件安裝在../build/release-install-cpp11/include目錄下
    • 庫文件安裝在../build/release-install-cpp11/lib目錄下
    • 以便muduo-protorpc和muduo-udns等庫使用

第三步(測試)

  • 編譯完成之後我們可以試着運行編譯的例子(位於../build/release-cpp11/bin/目錄下),查看能夠運行成功
  • 此處我們以inspector_test爲例:
    • (下圖1)運行../build/release-cpp11/bin/inspector_test
    • (下圖2)然後通過瀏覽器訪問“192.168.0.101:12345”訪問運行的服務器(其中192.168.0.101更換爲你的Linux IP)
    • (下圖3)或者輸入“192.168.0.101:12345/proc/status”來訪問該服務器的狀態

三、編譯帶有muduo庫的C/C++程序

  • muduo是靜態鏈接的C++程序庫(因爲在分佈式系統中正確安全地發佈動態庫的成本很高)
  • 編譯帶有muduo代碼的程序,規則與命令如下:
    • 頭文件:使用-I選項指出頭文件路徑(頭文件路徑就是上面muduo的頭文件安裝路徑,文章劃上去看)
    • 庫文件:使用-L選項指出庫文件路徑(庫文件路徑就是上面muduo的庫文件安裝路徑,文章劃上去看)
    • 鏈接相應的靜態庫文件:-lmuduo_net、-lmuduo_base
g++ -o muduo_test muduo_test.c -I頭文件路徑 -L庫文件路徑 -lmuduo_net -lmuduo_base

演示案例

  • 待續

四、目錄結構

  • muduo命名規則:
    • 源代碼文件名與class名相同
    • 例如ThreadPool class的定義是muduo/base/ThreadPool.h,其實現位於muduo/base/ThreadPool.cc
  • muduo源碼目錄如下:

 

基礎庫

  • muduo/base是一些基礎庫,都是用戶可見的類。內容如下:

網絡核心庫

  • muduo庫代碼結構:
    • muduo是基於Reactor模式的網絡庫,其核心是個事件循環EventLoop,用於相應計時器和IO事件
    • muduo採用基於對象(object-bases)而非面向對象(object-oriented)的設計風格
    • 其事件回調接口多以function+bind表達,用戶在使用muduo的時候不需要繼承其中的class
  • 網絡核心庫位於muduo/net和muduo/net/poller:一共不到4300行代碼,以下灰底表示用戶不可見的內部類

網絡附屬庫

  • 網絡庫有一些附屬模塊,它們不是核心內容:
    • 在使用的時候需要鏈接相應的庫,例如-lmuduo_http、-lmuduo_inspect等等
    • HttpServer和Inspector暴露出一個http界面,用於監控進程的狀態,類似於Java JMX
  • 附屬模塊位於muduo/net/{http,inspect,protorpc}等處

五、代碼結構

頭文件和庫文件

  • 對於muduo庫而言,只需要掌握5個關鍵類:Buffer、EventLoop、TcpConnection、TcpClient、TcpServer
  • muduo的頭文件明確分爲客戶可見和客戶不可見兩類。下面是安裝之後暴露的頭文件和庫文件

  • 在上面我們安裝的時候,安裝方式爲(以muduo源碼根目錄爲基準):
    • 頭文件:安裝在../build/release-install-cpp11/include目錄下
    • 庫文件:安裝在../build/release-install-cpp11/lib目錄下

  • 下圖是muduo的網絡核心庫的頭文件包含關係:用戶可見的爲白底,用戶不可見的爲灰底:

  • muduo頭文件中使用了前向聲明(forward declaration),大大簡化了頭文件之間的依賴關係:
    • 例如Accrptor.h、Channel.h、Connection.h、TcpConnection.h都前向聲明瞭 EventLoop class,從而避免包含EventLoop.h
    • 另外, 前向聲明瞭Connector class,從而避免將內部類暴露給用戶
    • 類似的做法還有TcpServer.h用到的Acceptor和EventLoopThreadPool、EventLoop.h用到的Poller和TimerQueue、TcpConnection.h用到的Channel和Socket等等

公開接口

  • 這裏簡單介紹各個class的作用,詳細的介紹參見以後的文章
  • 公開接口有:
    • Buffer仿Netty ChannelBuffer的buffer class,數據的讀寫通過buffer 進行。用戶代碼不需要調用read()/write(),只需要處理收到的數據和 準備好要發送的數據(詳情參閱“muduo Buffer類的設計與使用”)
    • InetAddress封裝IPv4地址(end point),注意,它不能解析域名, 只認IP地址。因爲直接用gethostbyname()解析域名會阻塞IO線程
    • EventLoop事件循環(反應器Reactor),每個線程只能有一個 EventLoop實體,它負責IO和定時器事件的分派。它用eventfd()來異步喚醒,這有別於傳統的用一對pipe()的辦法。它用TimerQueue作爲計時器管理,用Poller作爲IO multiplexing
    • EventLoopThread啓動一個線程,在其中運行EventLoop::loop()
    • TcpConnection整個網絡庫的核心,封裝一次TCP連接,注意它不能發起連接
    • TcpClient用於編寫網絡客戶端,能發起連接,並且有重試功能
    • TcpServer用於編寫網絡服務器,接受客戶的連接
  • 在這些類中:
    • TcpConnection的生命期依靠shared_ptr管理(即用戶和庫共同控制)。Buffer的生命期由TcpConnection控制。其餘類的生命期由用戶控制
    • Buffer和InetAddress具有值語義,可以拷貝;其他class 都是對象語義,不可以拷貝

內部實現

  • Channel是selectable IO channel,負責註冊與響應IO事件,注意它 不擁有file descriptor。它是Acceptor、Connector、EventLoop、 TimerQueue、TcpConnection的成員,生命期由後者控制
  • Socket是一個RAIIhandle,封裝一個filedescriptor,並在析構時關閉 fd。它是Acceptor、TcpConnection的成員,生命期由後者控制。 EventLoop、TimerQueue也擁有fd,但是不封裝爲Socket class
  • SocketsOps封裝各種Sockets系統調用
  • Poller是PollPoller和EPollPoller的基類,採用“電平觸發”的語意。 它是EventLoop的成員,生命期由後者控制
  • PollPoller和EPollPoller封裝poll()和epoll()兩種IO multiplexing後 端。poll的存在價值是便於調試,因爲poll(2)調用是上下文無關的,用 strace(1)很容易知道庫的行爲是否正確
  • Connector用於發起TCP連接,它是TcpClient的成員,生命期由後者控制
  • Acceptor用於接受TCP連接,它是TcpServer的成員,生命期由後者控制
  • TimerQueue用timerfd實現定時,這有別於傳統的設置 poll/epoll_wait的等待時長的辦法。TimerQueue用std::map來管理Timer, 常用操作的複雜度是O(logN),N爲定時器數目。它是EventLoop的成 員,生命期由後者控制
  • EventLoopThreadPool用於創建IO線程池,用於把TcpConnection分派到某個EventLoop線程上。它是TcpServer的成員,生命期由後者控制
  • 下圖是muduo的簡化類圖,Buffer是TcpConnection的成員:

六、例子

  • muduo附帶了十幾個示例程序,編譯出來有近百個可執行文件:
    • 這些例子位於examples目錄,其中包括從Boost.Asio、Java Netty、Python Twisted等處移植過來的例子
    • 這些例子基本覆蓋了常見的服務端網絡編程功能點,從這些例子可以充分學習非阻塞網絡編程

  • 在上面我們編譯muduo時,編程生成的可執行文件的路徑爲:../build/release-cpp11/bin

七、線程模型

  • muduo的線程模型爲one loop per thread+thread pool模型:
    • 每個線程最多有一個EventLoop,每個TcpConnection必須歸某個EventLoop管理,所有的IO會轉移到這個線程
    • 換句話說,一個file descriptor(文件描述符)只能由一個線程讀寫。TcpConnection所在的線程由其所屬的 EventLoop決定,這樣我們可以很方便地把不同的TCP連接放到不同的 線程去,也可以把一些TCP連接放到一個線程裏
    • TcpConnection和 EventLoop是線程安全的,可以跨線程調用
  • TcpServer直接支持多線程,它有兩種模式:
    • 單線程,accept()與TcpConnection用同一個線程做IO
    • 多線程,accept()與EventLoop在同一個線程,另外創建一個EventLoopThreadPool,新到的連接會按round-robin方式分配到線程池 中
  • 後面還會以Sudoku服務器爲例再次介紹muduo的多線程模型

八、總結

  • muduo是陳碩先生對常見網絡編程任務的總結,用它能很容易地編寫多線程的TCP服務器和客戶端
  • muduo代碼估計還有一些bug,功能也不完善,例如不支持signal處理(Signal也可以通過signalfd()融入EventLoop中,見https://github.com/chenshuo/muduo-protorpc中的zurg slave例子),待日後慢慢改進
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章