muduo网络库源码阅读Step by Step
Posted on: Nov 26 2015 Categories: muduo C++ Tags: muduo
一般寫服務端程序都需要有一個稱手的網絡庫來幫我們處理瑣碎的網絡通信細節,比如連接的建立、關閉,讀取數據,發送數據,接收、發送緩沖區的管理等,常用的C/C++網絡庫有libevent,asio,libev,我們項目組使用的是muduo網絡庫。muduo是陳碩寫的,基于非阻塞IO和事件驅動的現代C++網絡庫,原生支持one loop per thread模型(即reactor模型),它適合開發Linux下的面向業務的多線程服務端網絡應用程序。在Linux上muduo的性能(吞吐量、高并發下的事件處理效率)比其它網絡庫都要好,編程接口也很友好,它使用了很多Boost C++的現代特性(比如用RAII來管理資源,用function/bind來代替虛函數作為庫的回調接口,借助shared_ptr實現線程安全的對象回調等),代碼寫得很精練簡潔,而且源代碼中附帶有很多例子程序,是學習網絡編程的很好范例,值得細細品讀。
我從進項目組開始,花了6個月時間,把muduo相關的代碼、例子程序、書都研究了一遍,感覺受益非淺,尤其是對處理網絡通信的細節、異常處理了然于胸,寫起代碼來很順溜。現在大概總結下閱讀muduo網絡庫的順序。
Step 0 熟悉boost智能指針、function/bind的使用
在muduo庫內部使用了boost::scoped_ptr,weak_ptr,shared_ptr來管理各個網絡對象(channel,tcpconn,socket,acceptor, connector)的生命期,尤其是TcpConnection,是網絡庫和用戶代碼共有的,當某個連接不需要時,不能在用戶代碼中持有這個shared_ptr,否則會造成資源泄漏。更重要的是,在寫代碼時要學習、融入這種思想:用智能指針來管理動態分配的資源。如果能正確使用智能指針,在C++代碼中一般不需要出現delete語句。muduo中另一個重要的思想是用function/bind來封裝回調函數。function對象類似于函數指針(不過它可以帶狀態,即綁定一個類對象的成員函數),在發生某些事件時,執行相關的回調函數。比如它連接斷開、建立時會回調OnConnection(),數據讀完后會回調OnMessage。在用戶代碼中,也可以模仿這種思想。
Step 1 熟悉muduo庫的編譯、安裝,使用
這個階段主要摸熟muduo庫的使用,即使用它封裝的接口來編寫網絡應用程序。多敲敲代碼,看書,把example目錄下的所有例子程序都做一遍,這樣差不多能處理常見的網絡通信業務。然后考慮寫更專業一點的網絡服務程序,這時候會考慮:網絡通信的消息格式怎么定?心跳協議怎么定?系統中 各個server如何分工,如何交互、連接?遇到些底層的網絡bug怎么排查?如何做性能調優?
Step 2 閱讀muduo網絡相關的代碼,理解網絡通信的各個細節
這個階段主要研讀muduo實現網絡通信的各個細節(即net目錄下的代碼)。閱讀時,多參考陳碩著的《Linux多線程服務端編程》第8章。跟著書、代碼,走一遍,基本就能理解muduo網絡通信的原理了。這個時候基本上解答了所有step 1產生的疑問,比如:從建立連接、讀數據、寫數據、關閉連接的流程是怎么樣的?讀寫緩沖是是全局放一個,還是每個連接放一個?它的IO模型(即網絡IO線程模型,誰來epoll_wait,誰來accept,如何分配連接到子線程)是什么樣的?各個回調函數是處于主線程,還是子線程的語鏡中?Connector主動發起連接要考慮哪些細節?
這個階段就兩個任務:
- 理解muduo處理網絡通信的流程,各種事件發生的時機。
- muduo對各種網絡異常的處理。
最后要能夠達到:在各種環境下,遇到網絡問題,能夠定位問題出在哪里。
Step 3 閱讀muduo線程庫,定時器,runInLoop實現
muduo的線程庫封裝了一些常用的多線程設施,比如thread,條件變量,鎖,countDownLatch,線程池,線程私有變量,BlockingQueue等。沒有boost thread庫提供得多,但是也足夠用了。有些東西封裝得挺有特色,可以看看。定時器和runInLoop幾乎是所有網絡庫的標配。muduo中定時器實現得較簡單(用stl::set來存放所有timer),用timerfd來獲取定時事件的到來。具體可參考《Linux多線程服務端編程》第8.2節。runInLoop很有特色、很重要,它可以將一個線程的某些操作 轉到 另一個線程來做,這樣可以使很多非線程安全的函數變成線程安全的(比如定時器操作),也可以將某些耗時的操作異步化(比如可以將數據庫寫操作放到另一個線程處理)。具體實現中,會將一個function對象綁定到另一個loop io線程的待執行列表中,loop io線程處理完所有網絡事件、定時器事件后,就會執行所有綁定的function。里面用到了eventfd,來通知那個線程有待執行的funcion了。
可以看出,muduo使用了socket fd(網絡IO事件), timerfd(定時器到來事件), eventfd(通知事件), signal fd(信號到來事件)來處理相關的所有事件,只需一個epoll循環,整個程序的處理流程很清晰簡潔。
Step 4 閱讀muduo日志庫
muduo日志庫的代碼不多,很容易閱讀。讀的時候可參考《Linux C++多線程服務端編程》第5章,走一遍,大概能理解它的流程。閱讀的時候,主要學習兩點:
需求分為功能需求、性能需求。
- 功能需求主要考慮: 接口設計是否合理、易用?需要支持哪些功能?muduo日志庫的設計原則是:盡量提供最精簡的日志設施,不必要的就不提供。封閉的接口盡量便于程序員閱讀、查錯。
- 性能需求考慮單位時間內可寫入的最大日志量,且要求它不會阻塞正常的業務處理流程。muduo的設計原則:性能只需要“足夠好”,即能達到現代硬盤的最大寫入帶寬即可。且在實現時,要考慮減少多線程的鎖爭用,盡量不阻塞正常的業務處理邏輯。
日志庫的實現邏輯基本大同小異,邏輯基本是:在各個業務線程中拼裝日志串,然后將日志串存入一個Buffer(訪問時需要加鎖),另外有一個日志線程不停地從Buffer中取數據,然后將它輸出到日志文件中。實現的時候會考慮:
- Buffer如何設計?什么時候喚醒日志線程從Buffer中取數據?
- 如何減少 業務線程、日志線程 訪問Buffer時的鎖競爭?
- 日志串如何組裝,才能使它組裝速度足夠快、且要兼顧接口設計的易用性?
- 要考慮線程間的競爭、寫入速度等各種情況,保證不會丟失每條日志串。
- 什么時候切換寫到另一個日志文件?什么時候flush到日志文件?
- 若日志串寫入過多,日志線程來不及消費,怎么辦?
muduo日志庫的性能很高,大概可以達到每秒200多萬條,非常快。代碼中做了很多性能調優,比如實現了一個memory output stream,來加快各種類型轉換成字符串。利用線程私有變量緩存了一些變量值來加快日志串的組裝。用雙緩沖技術來減少線程之間的鎖競爭、最大化一次性輸出日志的吞吐量。閱讀時注意每個優化細節,然后在平時做性能調優時 模仿它。
Step 5 定制開發,修改源碼。借鑒某些組件的實現,應用到自己的代碼中
muduo網絡庫有很多功能不提供,比如SSL加密,配置業務線程的個數,設置多個監聽端口。在實際應用時,可能根據需要,來修改muduo庫。我們項目組做的修改有:
將各業務線程的個數、網絡IO線程的個數、心跳線程的個數 等可配置化。
在實際應用中,會根據業務特點來決定線程模型。有的時候,會將一些耗時的操作(比如操作mysql)、壓力大的操作單獨配置一些線程來操作。
一個TcpServer支持多個監聽端口。
在實際高并發服務的項目中,一般都會有一個鏈接服務器,來接受并管理所有的外部鏈接。同時,它也需要與內部的server通信。這時候,就需要監聽兩個端口號。為了編程的方便,我們使muduo支持了一個TcpServer監聽多個端口號。
注:也可以不修改,可創建兩個TcpServer,每個TcpServer監聽一個端口號。
TcpServer支持發起連接。
在實際項目中,經常需要一個服務端程序作為一個server來接受別人的連接,也需要主動發起一些連接到別的server上。為了編程的方便,我們使支持了TcpServer既可以發起連接,也可以接受連接。
注:也可以不修改,創建單獨的TcpServer來接受連接,創建單獨的TcpClient來發起連接。
在thirdpartServer中利用runInLoop、定時器的實現。
在我們的項目中,有個server要與蘋果的Apns服務通信,而Apns只接受Ssl加密通信,但是muduo不支持Ssl連接。當時考慮過,修改muduo使之支持Ssl通信,但是由于當時還不熟悉Ssl編程,直接修改muduo會帶來一些不必要的風險,所以直接使用Linux上的原始函數、openSsl庫來與Apns通信。通信時要用到定時器和runInLoop,我就把muduo的這部分實現代碼借鑒進來,實現了這兩個功能。
總結
以上是生活随笔為你收集整理的muduo网络库源码阅读Step by Step的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux ethtool 查看网卡状态
- 下一篇: TCP send 阻塞与非阻塞