Reactor事件驅動的兩種設計實現:面向對象 VS 函數式編程這裡的函數式編程的設計以muduo為例進行對比說明;Reactor實現架構對比面向對象的設計類圖如下:函數式編程以muduo為例,設計類圖如下:面向對象的Reactor方案設計我們先看看面向對象的設計方案,想想為什麼這麼做; 拿出Rea...
Reactor事件驅動的兩種設計實現:面向對象 VS 函數式編程
這裡的函數式編程的設計以muduo為例進行對比說明;
Reactor實現架構對比
面向對象的設計類圖如下:
函數式編程以muduo為例,設計類圖如下:
面向對象的Reactor方案設計
我們先看看面向對象的設計方案,想想為什麼這麼做;
拿出Reactor事件驅動的模式設計圖,對比來看,清晰明瞭;
從左邊開始,事件驅動,需要一個事件迴圈和IO分發器,EventLoop和Poller很好理解;為了讓事件驅動支持多平臺,Poller上加一個繼承結構,實現select、epoller等IO分發器選用;
Channel是要監聽的事件封裝類,核心成員:fd文件句柄;
成員方法圍繞著fd展開展開,如關註fd的讀寫事件、取消關註fd的讀寫事件;
核心方法:
enableReading/Writing;
disableReading/Writing;
以及事件到來後的處理方法:
handleEvent;
在OO設計這裡,handleEvent設計成一個虛函數,回調上層實際的數據處理;
AcceptChannel和ConnetionChannel派生自Channel,負責實際的網路數據處理;根據職責的不同而區分,AcceptChannel用於監聽套接字,接收新連接請求;有新的請求到來時,生成新的socket並加入到事件迴圈,關註讀事件;
ConnetionChannel用於真實的用戶數據處理,處理用戶的讀寫請求;涉及到具體的數據處理,當然,在這裡會需要用到應用層的緩存區;
比較困難的是用戶邏輯層的設計;放在哪裡合適?
先看看需求,用戶邏輯層需要知道的事件點(在這之後可能會有應用層的邏輯):
連接建立、消息到來、消息發送完畢、連接關閉;
這四個事件的源頭是Channel的handleEvent(),直接調用者應該Channel的派生類(AcceptChannel和ConnetionChannel),貌似可以將用戶邏輯層的指針放到Channel里;
且不說架構上是否合理,單是實現上右邊Channel這一塊(含AcceptChannel和ConnetionChannel)對用戶是透明的,用戶只需要關註以上四個事件點,底層的細節用戶層並不關心(比如是否該在事件迴圈中關註某個事件,取消關註某個事件,對用戶都是透明的),所以外部用戶無法直接將用戶邏輯層的指針給Channel;
想想用戶與網路庫的介面在哪裡?
IO分發器對用戶也是透明的,用戶可見就是EventLoop,在main方法中:
EventLoop loop;
loop.loop();
用戶邏輯層也就只有通過EventLoop與Channel的派生類關聯上;
這樣,就形成的最終的設計類圖,在main方法中:
UserLogicCallBack callback;
EventLoop loop(&callback); //在定義 EventLoop時,將callback的指針傳入,供後續使用;
loop.loop();
而網路層調用業務層代碼時,則通過eventloop_的過渡調用到業務邏輯的函數;
比如ConnetionChannel中數據到達的處理:
eventloop_->getCallBack()->onMessage(this);
函數式編程的Reactor設計
函數式編程中,類之間的關係主要通過組合來實現,而不是通過派生實現;
整個類圖中僅有Poller處使用了繼承關係;其它的都沒有使用;
這也是函數式編程的一個設計理念,更多的使用組合而不是繼承來實現類之間的關係,而支撐其能夠這樣設計的根源在於function()+bind()帶來的函數自由傳遞,實現回調非常簡單;
而OO設計中,只能使用基於虛函數/多態來實現回調,不可避免的使用繼承結構;
下麵再看看各個類的實現;
事件迴圈EventLoop和IO分發器沒有區別;
Channel的職責也和上面類似,封裝事件,所不同的是,Channel不再是繼承結構中的基類,而是作為一個實體;
這樣,handleEvent方法就不再是一個純虛函數,而是包含具體的邏輯處理,當然,只有最基本的事件判斷,然後調用上層的讀寫回調:
void Channel::handleEvent()
{
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
{
if (readCallback_) readCallback_();
}
if (revents_ & POLLOUT)
{
if (writeCallback_) writeCallback_();
}
}
這樣的關鍵是設置一堆回調函數,通過boost::function()+boost::bind()可以輕鬆的做到;
Acceptor 和TcpConnection
Acceptor類,這個對應到上面的AcceptChannel,但實現不是通過繼承,而是通過組合實現;
Acceptor用於監聽,關註連接,建立連接後,由TCPConnection來接管處理;
這個類沒有業務處理,用來處理監聽和連接請求到來後的邏輯;
所有與事件迴圈相關的都是channel,Acceptor不直接和EventLoop打交道,所以在這個類中需要有一個channel的成員,並包含將channel掛到事件迴圈中的邏輯(listen());
TcpConnection,處理連接建立後的收發數據;業務處理回調完成;
TCPServer
TCPServer就是膠水,作用有二:
- 作為最終用戶的介面方,和外部打交道通過TCPServer交互,而業務邏輯處理將回調函數傳入到底層,這種傳遞函數的方式猶如數據的傳遞一樣自然和方便;
- 作用Acceptor和TcpConnection的粘合劑,調用Acceptor開始監聽連接並設置回調,連接請求到來後,在回調中新建TcpConnection連接,設置TcpConnection的回調(將用戶的業務處理回調函數傳入,包括:連接建立後,讀請求處理、寫完後的處理,連接關閉後的處理),從這裡可以看到,業務邏輯的傳遞就跟數據傳遞一樣,多麼漂亮;
示例對比
通過一個示例來體會這兩種實現中回調實現的差別;
示例:分析讀事件到來時,底層如何將消息傳遞給用戶邏輯層函數來處理的?
OO實現
channel作為事件的監聽介面,加入到事件迴圈中,當讀事件到來時,需要調用
ConnetionChannel上的handleEvent();而非同步數據的讀請求最終需要業務邏輯層來判斷是否讀到相應的數據,這就需要從ConnetionChannel中調用用戶邏輯層上的OnMessage();
看看這段邏輯的OO實現序列圖:
代碼層面的實現:
定義用戶邏輯處理類UserLogicCallBack,接收消息的處理函數為onMessage();
我們關註最終底層是如何調用到業務邏輯層的onMessage()的;
int main()
{
UserLogicCallBack urlLogic;
EventLoop loop(urlLogic);//將用戶邏輯對象與事件迴圈對象關聯起來
loop.loop();
}
callback_用戶邏輯層的對象在EventLoop初始化時傳入:
class EventLoop{
EventLoop(CallBack & callback):
callback_(callback)
{
}
CallBack* getCallBack()
{
return &callback_;
}
CallBack& callback_; //回調方法基類
}
當讀事件到來,在ConnectionChannel中通過eventloop對象作為橋梁,回調消息業務處理onMesssage();
void ConnectionChannel::handleRead(){
int savedErrno = 0;
//返回緩存區可讀的位置,返回所有讀到的位元組,具體到是否收全,
//是否達到業務需要的數據位元組數,由業務層來判斷處理
ssize_t n = inputBuffer_.readFd(fd_, &savedErrno);
if (n > 0)
{
//通過eventloop作為中介,調用業務層的回調邏輯
loop_->getCallBack()->onMesssage(this,&inputBuffer_);
}
else if (n == 0)
{
handleClose();
}
else
{
errno = savedErrno;
handleError();
}
}
函數式編程實現
而muduo的回調,使用boost::function()+boost::bind()實現,通過這兩個神器,將使用者和實現者解耦;
通過TcpServer,將用戶邏輯層的函數傳遞到底層;讀事件到來,回調用戶邏輯;
以下是時序
代碼層面,我們看看用戶邏輯層的代碼是如何傳入的:
UserLogicCallBack中包含TcpServer的對象;
TcpServer server_;
在構造函數中,將onMessage傳遞給TcpServer,這是第一次傳遞:
UserLogicCallBack::UserLogicCallBack(muduo::net::EventLoop* loop,
const muduo::net::InetAddress& listenAddr)
: server_(loop, listenAddr, "UserLogicCallBack")
{
server_.setConnectionCallback(
boost::bind(&UserLogicCallBack::onConnection, this, _1));
//這裡將onMessage傳遞給TcpServer
server_.setMessageCallback(
boost::bind(&UserLogicCallBack::onMessage, this, _1, _2, _3));
}
TcpServer中的相關細節:
class TcpServer{
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; }
typedef boost::function<void (const TcpConnectionPtr&,
Buffer*,
Timestamp)> MessageCallback;
MessageCallback messageCallback_;
};
TcpServer新建連接時,將用戶層的回調函數繼續往底層傳遞,這是第二次傳遞:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
conn->setConnectionCallback(connectionCallback_);
// 這裡將onMessage()傳遞給TcpConnection
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(boost::bind(&TcpServer::removeConnection, this, _1));
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
}
通過這兩次傳遞,messageCallback_作為成員變數保存在TcpConnection中;
當讀事件到來時,TcpConnection中就可以直接調用業務層的回調邏輯:
void TcpConnection::handleRead(Timestamp receiveTime)
{
//返回緩存區可讀的位置,返回所有讀到的位元組,具體到是否收全,
//是否達到業務需要的數據位元組數,由業務層來判斷處理
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
//回調業務層的邏輯
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{
handleClose();
}
else
{
errno = savedErrno;
handleError();
}
}
完整時序詳見最後一節;源代碼來自muduo庫;
兩者的時序圖對比
Reactor的面向對象編程時序:
Reacotr的函數式編程時序:
結論
在面向對象的設計中,事件底層回調上層邏輯,本來和loop這個發動機沒有任何關係的一件事,卻需要使用它來作為中轉;EventLoop作為回調的中間橋梁,實在是迫不得已的實現;
而muduo的設計中加入了TcpServer這一膠水層,整個架構就清晰多了;
boost::function()+boost::bind()讓我們在回調的實現上有了更大的自由度,不用再依賴於基於虛函數的多態繼承結構;但更大的自由度,也更容易帶來糟糕的設計,使用boost::function()+boost::bind()基於對象的設計,還需要多多體會,多加應用;
Posted by: 大CC | 30DEC,2015
博客:blog.me115.com [訂閱]
Github:大CC