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