Reactor事件驱动的两种设计实现:面向对象 VS 函数式编程
內容目錄:
- Reactor實現架構對比
- 面向對象的Reactor方案設計
- 函數式編程的Reactor設計
- 示例對比
- 兩者的時序圖對比
- 結論
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與Channel的派生類關聯上;?
這樣,就形成的最終的設計類圖,在main方法中:
而網絡層調用業務層代碼時,則通過eventloop_的過渡調用到業務邏輯的函數;?
比如ConnetionChannel中數據到達的處理:
函數式編程的Reactor設計
函數式編程中,類之間的關系主要通過組合來實現,而不是通過派生實現;?
整個類圖中僅有Poller處使用了繼承關系;其它的都沒有使用;?
這也是函數式編程的一個設計理念,更多的使用組合而不是繼承來實現類之間的關系,而支撐其能夠這樣設計的根源在于function()+bind()帶來的函數自由傳遞,實現回調非常簡單;?
而OO設計中,只能使用基于虛函數/多態來實現回調,不可避免的使用繼承結構;
下面再看看各個類的實現;?
事件循環EventLoop和IO分發器沒有區別;?
Channel的職責也和上面類似,封裝事件,所不同的是,Channel不再是繼承結構中的基類,而是作為一個實體;?
這樣,handleEvent方法就不再是一個純虛函數,而是包含具體的邏輯處理,當然,只有最基本的事件判斷,然后調用上層的讀寫回調:
這樣的關鍵是設置一堆回調函數,通過boost::function()+boost::bind()可以輕松的做到;
Acceptor 和TcpConnection
Acceptor類,這個對應到上面的AcceptChannel,但實現不是通過繼承,而是通過組合實現;?
Acceptor用于監聽,關注連接,建立連接后,由TCPConnection來接管處理;?
這個類沒有業務處理,用來處理監聽和連接請求到來后的邏輯;?
所有與事件循環相關的都是channel,Acceptor不直接和EventLoop打交道,所以在這個類中需要有一個channel的成員,并包含將channel掛到事件循環中的邏輯(listen());?
TcpConnection,處理連接建立后的收發數據;業務處理回調完成;
TCPServer
TCPServer就是膠水,作用有二:
示例對比
通過一個示例來體會這兩種實現中回調實現的差別;?
示例:分析讀事件到來時,底層如何將消息傳遞給用戶邏輯層函數來處理的?
OO實現
channel作為事件的監聽接口,加入到事件循環中,當讀事件到來時,需要調用?
ConnetionChannel上的handleEvent();而異步數據的讀請求最終需要業務邏輯層來判斷是否讀到相應的數據,這就需要從ConnetionChannel中調用用戶邏輯層上的OnMessage();?
看看這段邏輯的OO實現序列圖:
?
代碼層面的實現:?
定義用戶邏輯處理類UserLogicCallBack,接收消息的處理函數為onMessage();?
我們關注最終底層是如何調用到業務邏輯層的onMessage()的;
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的對象;
在構造函數中,將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傳遞給TcpServerserver_.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()傳遞給TcpConnectionconn->setMessageCallback(messageCallback_); conn->setWriteCompleteCallback(writeCompleteCallback_);conn->setCloseCallback(boost::bind(&TcpServer::removeConnection, this, _1)); ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn)); }通過這兩次傳遞,messageCallback_作為成員變量保存在TcpConnection中;?
當讀事件到來時,TcpConnection中就可以直接調用業務層的回調邏輯:
完整時序詳見最后一節;源代碼來自muduo庫;
兩者的時序圖對比
Reactor的面向對象編程時序:
?
?
Reacotr的函數式編程時序:
?
結論
在面向對象的設計中,事件底層回調上層邏輯,本來和loop這個發動機沒有任何關系的一件事,卻需要使用它來作為中轉;EventLoop作為回調的中間橋梁,實在是迫不得已的實現;?
而muduo的設計中加入了TcpServer這一膠水層,整個架構就清晰多了;?
boost::function()+boost::bind()讓我們在回調的實現上有了更大的自由度,不用再依賴于基于虛函數的多態繼承結構;但更大的自由度,也更容易帶來糟糕的設計,使用boost::function()+boost::bind()基于對象的設計,還需要多多體會,多加應用;
總結
以上是生活随笔為你收集整理的Reactor事件驱动的两种设计实现:面向对象 VS 函数式编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Docker 使用Dockerfile构
- 下一篇: ros(2) 发布者publisher的