muduo網路庫學習(一)對io複用的封裝Poller,面向物件與基於物件
高效併發的網路框架大多離不開io多路複用函式,Linux下有三種
- select
- poll
- epoll
關於三者的區別可以參考 linux網路程式設計—–幾種伺服器模型及io多路複用函式
前段時間看Libevent原始碼時也學習過對epoll/poll/select的封裝,但是畢竟c語言寫的庫,是通過函式指標實現多型。我學習的muduo原始碼是c++11版本的,利用c++進行封裝。
首先複習一下C++面向物件和基於物件的區別
- 面向物件
- 面向物件的三大特點:封裝,繼承,多型缺一不可
- 封裝:資料和處理資料的函式統一起來,封裝在一個class中
- 繼承:通過繼承某個類派生出一個新類,被繼承的類稱作基類,派生出的類稱作派生類。派生類是對基類的補充,二者之間滿足一定的歸屬關心,如動物(基類),鳥(派生類)。繼承可以是public/private/protected繼承,也可以是虛繼承(用於解決多重繼承帶來的重複問題),基類可以是抽象基類(不能被例項化),但是派生類需要重新實現基類定義的每個純虛擬函式。
- 多型:在繼承的基礎上通過基類指標指向派生類的例項化物件,達到呼叫派生類虛擬函式的目的,多型又被叫做執行時多型,是在執行期根據基類指標實際指向的物件型別判斷呼叫哪個函式的方式。
- 基於物件
- 無繼承,無多型,只有封裝
- 利用類封裝好的介面實現對資料的操作
如何禁止編譯器自動生成拷貝建構函式/賦值運算子
- 繼承boost::noncopyable
- 自定義空基類,基類中將兩個函式放在private域,派生類private繼承該基類
- 在自己的private域中宣告兩個函式,不予實現
c++11版本採用第2中,boost版本採用第1中,第三種效果不好,因為錯誤是在連結期發現,前兩個是在編譯期
現如今大多C++程式都是基於物件的,面向物件只在整個程式中佔一小部分比重。
muduo採用的也是基於物件的手法,但是對io多路複用的封裝採用的是面向物件,即定義一個基類,派生出不同的派生類。muduo只派生了poll/epoll兩個類封裝,因為二者在實現上有相似性,可以共用基類。
基類Poller主要用於設計統一介面,兩個派生類EPollPoller/PollPoller用於實現各自的操作,Poller定義如下
/* 禁止編譯器自動生成拷貝建構函式/賦值操作運算子 */
class Poller : noncopyable
{
public:
typedef std::vector<Channel*> ChannelList;
Poller(EventLoop* loop);
virtual ~Poller();
/// Polls the I/O events.
/// Must be called in the loop thread.
/*
* 監聽函式,對於epoll是epoll_wait,對於poll是poll
* 返回epoll_wait/poll返回的時間
*/
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;
/// Changes the interested I/O events.
/// Must be called in the loop thread.
/* 更新監聽事件,增刪改對fd的監聽事件 */
virtual void updateChannel(Channel* channel) = 0;
/// Remove the channel, when it destructs.
/// Must be called in the loop thread.
/* 刪除監聽事件 */
virtual void removeChannel(Channel* channel) = 0;
virtual bool hasChannel(Channel* channel) const;
static Poller* newDefaultPoller(EventLoop* loop);
void assertInLoopThread() const
{
ownerLoop_->assertInLoopThread();
}
protected:
/*
* Channel,儲存fd和需要監聽的events,以及各種回撥函式(可讀/可寫/錯誤/關閉等)
* 類似libevent的struct event
*/
typedef std::map<int, Channel*> ChannelMap;
/* 儲存所有事件Channel,類似libevent中base的註冊佇列 */
ChannelMap channels_;
private:
/*
* EventLoop,事件驅動主迴圈,用於呼叫poll函式
* 類似libevent的struct event_base
*/
EventLoop* ownerLoop_;
};
類中採用前向宣告,即在定義Poller之前宣告一下class Channel;
,用處是避免讓標頭檔案#include <muduo/net/Channel.h>
從而增加依賴性,因為標頭檔案中並沒有使用Channel,只是定義了這個型別的變數,所以只宣告就好了,而在成員函式的實現中需要使用Channel的介面,這就需要讓編譯器知道Channel是怎麼定義的,就需要在.cpp
檔案中#include <muduo/net/Channel.h>
。另外,因為Channel是Poller的成員變數,當Poller析構時也會呼叫Channel的解構函式,這就需要讓編譯器知道Channel解構函式的定義,所以Poller的解構函式需要在.cpp
中定義。
以上也是大多muduo類採用的方法,這種方法可以降低依賴關係,如果Channel檔案改變,不需要重新編譯Poller檔案
派生類EPollPoller的實現就是重新實現基類Poller宣告的純虛擬函式,簡單的呼叫epoll的介面。在poll返回後也會將就緒的fd(muduo是由Channel管理,libevent是由struct event管理)新增到啟用佇列中
/*
* 對epoll函式的封裝,繼承自Poller
*/
class EPollPoller : public Poller
{
public:
EPollPoller(EventLoop* loop);
virtual ~EPollPoller();
/* epoll_wait */
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels);
/* ADD/MOD/DEL */
virtual void updateChannel(Channel* channel);
/* DEL */
virtual void removeChannel(Channel* channel);
private:
static const int kInitEventListSize = 16;
/* EPOLL_CTL_ADD/MOD/DEL轉成字串 */
static const char* operationToString(int op);
/* epoll_wait返回後將就緒的檔案描述符新增到引數的啟用佇列中 */
void fillActiveChannels(int numEvents,
ChannelList* activeChannels) const;
/* 由updateChannel/removeChannel間接呼叫,執行epoll_ctl */
void update(int operation, Channel* channel);
typedef std::vector<struct epoll_event> EventList;
int epollfd_;
EventList events_;
};
類的宣告中的EventList記錄著所有監聽的epoll_event,.cpp
中就是實現上述函式,進行增刪改等,主要記錄一些沒接觸過的知識
epoll_create1
int epoll_create(int size);
- 建立epollfd,早期linux引入的建立監聽epollfd的函式,傳入的引數size作為給核心的一個提示
- 核心會根據這個size分配一塊這麼大的資料空間用來監聽事件(struct epoll_event)
- 當在使用的過程中出現大於size的值時,核心會重新分配記憶體空間。
- 目前這個size已經沒有作用,核心可以動態改變資料空間大小,但仍然需要傳入大於0的數
int epoll_create1(int flag);
- 建立epollfd,新版linux引入的函式,當flag為0時和不帶size的epoll_create效果一樣
- 目前flag只支援EPOLL_CLOEXEC,在建立的過程中將返回的epollfd描述符設定CLOSE-ON-EXEC屬性
- 當程式exec執行新程式時自動close epollfd
static_assert
static_assert(bool flag, char *msg);
- 編譯期斷言,程式在編譯的過程中執行
- 若flag為真,什麼也不做
- 若flag為假,產生一條編譯錯誤,輸出錯誤資訊msg,錯誤位置為當前行號
static_assert可以增加編譯期對程式的控制,準確定位出錯的可能
assert
assert(bool flag);
- 執行期DEBUG模式下的斷言
- 若flag為真,什麼也不做
- 若flag為假,終止程式
注意assert只有在debug模式下才會有效,在release模式下這條語句就被編譯器刪除了
對此,通常在assert後面有一個(void)n;等語句
int fd = channel_->fd();
assert(channels_[fd] == channel);
(void)fd;
如果以release模式下執行,assert被刪除,編譯器會發出警告通知,提示變數fd未使用
而如果在設定編譯條件時將警告提升為錯誤,那麼編譯就不會繼續進行
(void)fd;意為將fd轉為void型別,簡單使用一下fd,消除警告