一、由來
-
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++網絡庫:
- 與前兩次不同,這次一開始就想好了庫的名字,叫muduo(木鐸)(這個名字的由來可以參閱陳碩的一篇訪談:http://www.oschina.net/question/28_61182)
- 並在Google code上創建了項目:http://code.google.com/p/muduo
- muduo以git爲版本管理工具,託管於:http://github.com/chenshuo/muduo
- muduo的主體內容在2010年5月底已經基本完成,8月底發佈0.1.0版, 2012年11月的最新版本是0.8.2
爲什麼需要網絡庫?
- 使用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
- 陳碩先生的Github有一個項目展示瞭如何使用CMake和普通makefile編譯基於muduo的程序,可參閱:https://github.com/chenshuo/muduo-tutorial
演示案例
- 待續
四、目錄結構
- 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的示例項目。由於License等原因沒有放到 muduo發行版中,可以單獨下載:
- https://github.com/chenshuo/muduo-udns:基於UDNS的異步DNS解析
- https://github.com/chenshuo/muduo-protorpc:新的RPC實現,自動管理對象生命期
七、線程模型
- 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例子),待日後慢慢改進