Reactor事件驅動的兩種設計實現:面向對象 VS 函數式編程

来源:http://www.cnblogs.com/me115/archive/2015/12/31/5088914.html
-Advertisement-
Play Games

Reactor事件驅動的兩種設計實現:面向對象 VS 函數式編程這裡的函數式編程的設計以muduo為例進行對比說明;Reactor實現架構對比面向對象的設計類圖如下:函數式編程以muduo為例,設計類圖如下:面向對象的Reactor方案設計我們先看看面向對象的設計方案,想想為什麼這麼做; 拿出Rea...


Reactor事件驅動的兩種設計實現:面向對象 VS 函數式編程

這裡的函數式編程的設計以muduo為例進行對比說明;

Reactor實現架構對比

面向對象的設計類圖如下:

 oo_class

函數式編程以muduo為例,設計類圖如下:

muduo 

面向對象的Reactor方案設計

我們先看看面向對象的設計方案,想想為什麼這麼做;
拿出Reactor事件驅動的模式設計圖,對比來看,清晰明瞭;

 reactor_model 

從左邊開始,事件驅動,需要一個事件迴圈和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就是膠水,作用有二:

  1. 作為最終用戶的介面方,和外部打交道通過TCPServer交互,而業務邏輯處理將回調函數傳入到底層,這種傳遞函數的方式猶如數據的傳遞一樣自然和方便;
  2. 作用Acceptor和TcpConnection的粘合劑,調用Acceptor開始監聽連接並設置回調,連接請求到來後,在回調中新建TcpConnection連接,設置TcpConnection的回調(將用戶的業務處理回調函數傳入,包括:連接建立後,讀請求處理、寫完後的處理,連接關閉後的處理),從這裡可以看到,業務邏輯的傳遞就跟數據傳遞一樣,多麼漂亮;

示例對比

通過一個示例來體會這兩種實現中回調實現的差別;
示例:分析讀事件到來時,底層如何將消息傳遞給用戶邏輯層函數來處理的?

OO實現

channel作為事件的監聽介面,加入到事件迴圈中,當讀事件到來時,需要調用
ConnetionChannel上的handleEvent();而非同步數據的讀請求最終需要業務邏輯層來判斷是否讀到相應的數據,這就需要從ConnetionChannel中調用用戶邏輯層上的OnMessage();
看看這段邏輯的OO實現序列圖:

oo_seq_msg 

代碼層面的實現:
定義用戶邏輯處理類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,將用戶邏輯層的函數傳遞到底層;讀事件到來,回調用戶邏輯;

以下是時序

fun_seq_msg 

代碼層面,我們看看用戶邏輯層的代碼是如何傳入的:
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的面向對象編程時序:

 oo_sequence

 

Reacotr的函數式編程時序:

EchoServer_sequence 

結論

在面向對象的設計中,事件底層回調上層邏輯,本來和loop這個發動機沒有任何關係的一件事,卻需要使用它來作為中轉;EventLoop作為回調的中間橋梁,實在是迫不得已的實現;
而muduo的設計中加入了TcpServer這一膠水層,整個架構就清晰多了;
boost::function()+boost::bind()讓我們在回調的實現上有了更大的自由度,不用再依賴於基於虛函數的多態繼承結構;但更大的自由度,也更容易帶來糟糕的設計,使用boost::function()+boost::bind()基於對象的設計,還需要多多體會,多加應用;

Posted by: 大CC | 30DEC,2015
博客:blog.me115.com [訂閱]
Github:大CC


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • CSS如何設置連接的樣式:網站中,可能需要將鏈接的樣式設置的更為美觀一些,在預設情況下,鏈接在沒有點擊和點擊後的樣式是有所差別的,這就是一個人性化的效果,可以有效的區分鏈接是否已經被點擊過,下麵就介紹一下如何設置連接的樣式。一.樣式屬性:1.a:link:定義鏈接點擊前的樣式。2.a:visited...
  • 如何將checkbox覆選框設置為只讀:覆選框checkbox並沒有readOnly屬性,但是如果將其設置為不可用也就是將它的disabled="disabled",checkbox值不會被髮送,並且外觀呈現灰色,下麵就介紹一下如何模擬實現覆選框的只讀狀態。一.原生javascript代碼:ckOb...
  • 判斷覆選框中是否有被選中的代碼實例:覆選框中一般多項,有時候我們需要判斷這些付選中是否有被選中的項,下麵就通過一個實例簡單介紹一下如何實現此效果。代碼如下:限定覆選框的可選個數-螞蟻部落 螞蟻部落一 螞蟻部落二螞蟻部落三 螞蟻部落四螞蟻部落五 螞蟻部落六螞蟻部落七 螞蟻部落八螞蟻部落九 螞蟻部...
  • CSS實現的相容所有瀏覽器的div懸浮在網頁一側的代碼:固定懸浮在網頁一側的效果應用非常的頻繁,尤其是客服系統或者公告系統,CSS提供了position:fixed屬性即可實現此功能,但是IE6瀏覽器並不支持,雖然IE6的用戶越來越少,但是畢竟還是有用戶在使用,所以最好還是要實現相容效果,下麵就是一...
  • 回到目錄大家好,今天有時間來介紹一下Lind.DDD框架里的消息機制,消息發送這塊一般的實現方法是將Email,SMS等集成到一個公用類庫里,而本身Email和SMS沒什麼關係,它們也不會有什麼介面約定,即你想實現某種消息的多態發送,不需要程式代碼,基本不可能實現,而在Lind.DDD裡面,大叔將它...
  • 你可以從你們現在項目裡面隨便找幾處註釋,看看寫註釋的代碼是不是存在如下兩種毛病之一:1. 命名不准確;2. 方法太長(超過50行)。如果你找到的代碼沒有出現上面兩種毛病而註釋依然存在,那你再看看這個註釋是否有實際意義,是不是這個註釋不要也無所謂呢。註釋是惡魔這個觀點可能你第一次看到,你可能很難接受,...
  • 一、 生成對象的原始模式假定我們把Bob看成一個對象,它有"name"和"age"兩個屬性。 var person = { name : '', age : }現在,我們需要根據這個原型對象的規格(schema),生成兩個實例對象。 var person1 = {}; // 創建...
  • 編者按:本文由PMCAFF產品經理社區原創專欄作者 喬向陽 翻譯自 http://goodui.org/一個好的界面設計應該擁有高轉化率且方便用戶使用,換句話說:既能達到商業目的又能滿足方便易用的要求。有一個設計咨詢公司根據自己的客戶案例,總結了 75 個經過實踐證明的原則:之前國內流傳這篇文章的前...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...