Reactor事件驱动的两种设计实现:面向对象 VS 函数式编程

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传递给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中就可以直接调用业务层的回调逻辑:

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()基于对象的设计,还需要多多体会,多加应用;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/509651.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ElasticSearch 快照 备份、恢复数据

文章目录ElasticSearch 设置备份文件地址注册快照存储库查看快照存储库保存结果创建快照异步创建指定索引进行快照查看全部快照在服务器查看备份的数据恢复数据本机恢复其他服务器恢复常见问题报错 doesnt match any of the locations specified by path.repo because this set…

java中LinkedList类的操作

LinkedList类是双向链表,单向队列,双向队列,栈的实现类: LinkedList类实现单向队列和双向队列的接口,自身提高了栈操作的方法,链表操作的方法. 在LinkedList类中存在很多方法,但是功能都是相同的.LinkedList表示了多种数据结构的实现,每一种数据结构的操作名字不同. 面试题:编…

数据结构实验之栈七:出栈序列判定

题目描述 给一个初始的入栈序列&#xff0c;其次序即为元素的入栈次序&#xff0c;栈顶元素可以随时出栈&#xff0c;每个元素只能入栈依次。输入一个入栈序列&#xff0c;后面依次输入多个序列&#xff0c;请判断这些序列是否为所给入栈序列合法的出栈序列。 例如序列1&#x…

FileBeat + Pipeline 解析日志 保存至ElasticSearch(实战)

文章目录FileBeat Pipeline 解析日志 保存至ElasticSearch&#xff08;实战&#xff09;下载地址目的日志数据模拟Pipeline创建pipeline查看Pipeline是否创建成功创建FileBeat配置文件 filebeat.yml创建自定义字段 FileBeat fields.yml执行 FileBeatfilebeat 启动命令说明测试…

网络编程中的关键问题总结

内容目录&#xff1a; 连接建立连接断开消息到达发送消息消息发送完毕其它问题参考 网络编程中的关键问题总结 总结下网络编程中关键的细节问题&#xff0c;包含连接建立、连接断开、消息到达、发送消息等等&#xff1b; 连接建立 包括服务端接受 (accept) 新连接和客户端成功发…

List实现类性能和特点分析

面向接口编程: 接口类型 变量 new 实现类(); List list new ArrayList(); List实现类特点和性能分析: 三者共同的特点(共同遵循的规范): 1):允许元素重复. 2):记录元素的先后添加顺序. Vector类: 底层才有数组结构算法,方法都使用了synchronized修饰,线程安全,但是性能…

数据结构实验之栈八:栈的基本操作

题目描述 堆栈是一种基本的数据结构。堆栈具有两种基本操作方式&#xff0c;push 和 pop。push一个值会将其压入栈顶&#xff0c;而 pop 则会将栈顶的值弹出。现在我们就来验证一下堆栈的使用。 输入 首先输入整数t&#xff08;1 < t < 10&#xff09;&#xff0c;代表测…

F5 BIGip 负载均衡 IP算法解密工具

BIGip是对负载均衡的实现&#xff0c;主要通过Virtual Server、iRules、Pool、Node、Monitor和Persistent&#xff08;会话保持&#xff09;实现。BIGip在实现会话保持机制时会在用户首次发起请求时&#xff0c;会为用户设置一个cookie&#xff0c;即服务端会添加set-cookie响应…

Java集合框架-重构设计

根据Vector类,ArrayList类,LinkedList类所有具有的存储特点以及拥有的方法入手,发现共性就往上抽取. 共同的特点: 1):允许元素重复的. 2):会记录先后添加的顺序. 共同的方法: 如下图. 根据他们的特点,我就可以指定规范: 遵循该规范的实现类,无论底层算法如何,都必须保证允…

回文串判定

题目描述 输入一串字符&#xff08;长度小于100&#xff09;&#xff0c;判断该串字符是否是回文串&#xff08;正序读与逆序读内容相同&#xff09;。 输入 输入一串字符&#xff08;长度小于100&#xff09;。 输出 若该串字符是回文串输出“yes"&#xff0c;否则输出”…

Canal Mysql binlog 同步至 Hbase ES

文章目录一、Canal介绍工作原理canal 工作原理二、下载三、安装使用Mysql准备canal 安装解压缩 canal-deployer配置修改启动查看server日志查看instance日志服务停止canal-client使用Canal Adapter数据同步Hbase数据同步ElasticSearch一、Canal介绍 早期阿里巴巴因为杭州和美国…

java中集合的迭代操作

集合的迭代操作: 把集合做的元素一个一个的遍历取出来. 迭代器对象: Iterator: 迭代器对象,只能从上往下迭代. boolean hasNext(); 判断当前指针后是否有下一个元素 Object next():获取指针的下一个元素,并且移动指针. ListIterator: 是Iterator接口的子接口,支持双向迭代…

Canal同步ES报错,java.lang.ClassCastException: com.alibaba.druid.pool.DruidDataSource cannot be cast to c

Canal同步ES报错 提示类型转换失败 2021-09-20 13:10:54.094 [main] ERROR c.a.o.canal.adapter.launcher.loader.CanalAdapterLoader - Load canal adapter: es7 failed java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ClassCastException: com.alibab…

C语言实验——数组逆序

题目描述 有n个整数&#xff0c;使其最后m个数变成最前面的m个数&#xff0c;其他各数顺序向后移m&#xff08;m < n < 100)个位置。输入 输入数据有2行&#xff0c;第一行的第一个数为n&#xff0c;后面是n个整数&#xff0c;第二行整数m。输出 按先后顺序输出n个整数。…

用C++11的std::async代替线程的创建

转自&#xff1a;http://www.cnblogs.com/qicosmos/p/3534211.html c11中增加了线程&#xff0c;使得我们可以非常方便的创建线程&#xff0c;它的基本用法是这样的&#xff1a; void f(int n); std::thread t(f, n 1); t.join(); 但是线程毕竟是属于比较低层次的东西&#xf…

HashSet类

Set是Collection子接口&#xff0c;模拟了数学上的集的概念。 Set集合存储特点: 1):不允许元素重复. 2):不会记录元素的先后添加顺序. Set只包含从Collection继承的方法&#xff0c;不过Set无法记住添加的顺序&#xff0c;不允许包含重复的元素。当试图添加两个相同元素进Se…

Mysql写入数据时,adapter 日志报ES连接错误

Mysql写入数据时&#xff0c;adapter 日志报ES连接错误 日志如下&#xff1a; 2021-09-20 13:51:03.795 [pool-1-thread-1] ERROR c.a.otter.canal.adapter.launcher.loader.AdapterProcessor - NoNodeAvailableException[None of the configured nodes are available: [{#tr…

扩展框架分析

在服务器端启动通信服务器程序CommunicateServer.exe,接受客户端的连接&#xff1b;在客户端启动GameStart.exe&#xff0c;连接至服务器端&#xff0c;在接收到“Start”消息时启动GameEntryPoint.exe&#xff0c;GameEntryPoint.exe 将根据配置文件&#xff08;引导文件&…

走迷宫

题目描述 一个由n * m 个格子组成的迷宫&#xff0c;起点是(1, 1)&#xff0c; 终点是(n, m)&#xff0c;每次可以向上下左右四个方向任意走一步&#xff0c;并且有些格子是不能走动&#xff0c;求从起点到终点经过每个格子至多一次的走法数。 输入 第一行一个整数T 表示有T 组…

Canal Mysql同步至ES/Hbase只有新增时生效,修改删除不生效

问题描述 新增Mysql数据时&#xff0c;ES、Hbase数据会同步成功&#xff1b;当删除Mysql数据&#xff0c;或者修改Mysql数据时同步ES、Hbase数据无变化(PS:修改和删除加上LIMIT xxx 就可以成功。) 问题分析 通过查看日志发现新增和删除记录的日志区别&#xff1a;新增data有…