C++基础:Reactor模型设计思想与muduo架构理解

2025/11/5:

几乎有一年没发学习的内容了,这里对这一个多月学习muduo的历程做一个阶段性的总结。在这段时间的学习中,同时发现了自己在一些基础的不足之处。


文章目录

Reactor漫谈

作为认识muduo模块的开始,我们首先需要认识Reactor这一公认优秀的网络架构。

IO多路复用

在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用特殊api的才是异步IO。

------陈硕

这里简单讲一下网络IO的一些分类:

网络IO阶段一(操作系统)

  • 数据准备(tcp缓冲区)
    • 阻塞:让调用IO方法的进程进入阻塞状态(recv)
    • 非阻塞:不会改变线程的状态,通过返回值判断数据的读写情况

网络IO阶段二(应用程序)

  • IO的同步和异步
    • 同步:recv往内核中的tcpbuffer向程序中的buffer搬运数据,不由操作系统完成
      • 同步IO接口:select poll epoll recv等基本都是
    • 异步:应用程序做自己的事,当调用异步IO接口时,将sockfd、buffer、通知信号都交给操作系统,操作系统完成数据的搬运,完成后通过信号通知程序。相当于提供一个回调函数,类似于dma通知cpu完成内存的映射
      • 异步IO接口:aioread aiowrite
      • 缺点:编程复杂

​是否异步的关键在于是否存在通知机制,以及任务是否由他人处理。最简单粗暴,也是最有效的判断在于我们上面所引用的话:在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用特殊api的才是异步IO。因此,我们可以认为异步IO是极为特殊的一种IO操作,下面暂时不做讨论。

​ 由上面的话我们也可以区分业务层面的同步和异步操作:

  • 同步操作
    • A等待B做完事并提供返回值再继续处理
  • 异步操作
    • A告诉B感兴趣的事件和处理方式,A继续执行自己的操作逻辑,等B监听到对应事件发生时通知A,A再开始相应的处理逻辑(例:node.js 异步框架)

一个经典的IO操作包含两个过程:数据就绪和数据读写

数据准备:根据系统IO操作的就绪状态,分为阻塞、非阻塞两种,表现形式为线程阻塞或者直接返回。

例:管道读写默认阻塞

数据读写:根据应用程序和内核的交互方式,分为同步、异步两种。

根据上面的信息,我们给出Reactor的分类,Reactor是一个同步非阻塞的IO多路复用模式,不会阻塞在读写(send/recv/read/write)上,而是在一个线程下管理多个用于读写的fd(C++一般使用epoll),根据读写事件的通知来高效进行读写任务。

与之相对的,一个同步阻塞的IO模型,往往使用一个线程来阻塞在一个fd的读写上;一个同步非阻塞的模型也会使用一个线程来进行一个读写任务的轮询。因此,Reactor一次管理多个读写任务的模式相比同步阻塞与非阻塞的模式明显要高效很多。

或者可以这样说:IO多路复用用于阻塞地获取可读取的fd,在每个fd上非阻塞地读入数据以防止在fd上阻塞难以回到epoll wait。

what made a Reactor?

良好的网络服务器设计需要将多线程服务端编程的问题转化为如何设计一个高效且容易使用的eventLoop。

------陈硕

Reactor的核心思想就是设计一个高效且容易使用的eventLoop,具体如下所示:

在图中,Reactor组件下的Event集合循环即为eventLoop,该组件从Demultiplex(事件分发器)下的epollwait循环中获取发生的事件,并调用对应的EventHandler进行事件处理。在muduo的结构中,一个线程中运行一个eventLoop,即 one thread per loop。

一开始可能对Reactor和Demultiplex两个组件的分离有所疑惑,其实这两个组件的分离关系到在epollwait进行阻塞等待时,能通过唤醒等方法在其中加入新fd的操作。

muduo

架构大观

至于muduo库的具体组件,可以参考下图:

此图并不完全反映muduo组件之间的关系。不过从这张图片出发,可以分析muduo中各个组件和Reactor模块的对应关系,其中,Reactor可以看作是eventLoop,Demultiple是poller,而EventHandler可以看作是muduo中的Channel。

理解muduo的每个组件具体做了什么是一件对理解这个架构很重要的事,不过网上有更为详实的记录方便大家理解,这里便不再赘述。

实例结合

在对各个模块有了基本的理解之后,这里我们分析一下下面的代码,看看muduo在这段代码中做了什么事情:

cpp 复制代码
#include <mymuduo/TcpServer.h>
#include <mymuduo/Logger.h>

#include <string>
#include <functional>

class EchoServer
{
public:
    EchoServer(EventLoop *loop,
            const InetAddress &addr, 
            const std::string &name)
        : server_(loop, addr, name)
        , loop_(loop)
    {
        // 注册回调函数
        server_.setConnectionCallback(
            std::bind(&EchoServer::onConnection, this, std::placeholders::_1)
        );

        server_.setMessageCallback(
            std::bind(&EchoServer::onMessage, this,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)
        );

        // 设置合适的loop线程数量 loopthread
        server_.setThreadNum(3);
    }
    void start()
    {
        server_.start();
    }
private:
    // 连接建立或者断开的回调
    void onConnection(const TcpConnectionPtr &conn)
    {
        if (conn->connected())
        {
            LOG_INFO("Connection UP : %s", conn->peerAddress().toIpPort().c_str());
        }
        else
        {
            LOG_INFO("Connection DOWN : %s", conn->peerAddress().toIpPort().c_str());
        }
    }

    // 可读写事件回调
    void onMessage(const TcpConnectionPtr &conn,
                Buffer *buf,
                Timestamp time)
    {
        std::string msg = buf->retrieveAllAsString();
        conn->send(msg);
        conn->shutdown(); // 写端   EPOLLHUP =》 closeCallback_
    }

    EventLoop *loop_;
    TcpServer server_;
};

int main()
{
    EventLoop loop;
    InetAddress addr(8000);
    EchoServer server(&loop, addr, "EchoServer-01"); // Acceptor non-blocking listenfd  create bind 
    server.start(); // listen  loopthread  listenfd => acceptChannel => mainLoop =>
    loop.loop(); // 启动mainLoop的底层Poller

    return 0;
}

首先,server注册了两个回调,然后这个回调会被层层封装,流程如下:

TcpServer=>TcpConnection=>Channel=>Poller=>notify channel=>loop执行

可以看到和上面图中说的一样,channel为所有任务回调最终注册的地方,而这些回调最终都在loop中执行。

在server启动的时候,相应的线程池、Acceptor和相应数量的(thread, loop, poller, channel(封装eventfd))被创建。

当出现一个新的连接时,baseLoop执行pair(TcpConnection, channel)的派发通过轮询来选取一个loop(包括自身),将该pair挂载到一个loop之下。如果baseLoop把该连接任务派发给自身,则立即执行;若派发给subloop,则加入subloop的待执行队列等待执行,此时subloop若还在阻塞等待poller返回事件(poller阻塞中),则baseLoop通过调用subloop的eventfd,通过channel伪造一个可读事件来唤醒poller,以此让subloop接收到poller返回事件后执行连接任务。

当fd上有可读事件发生,poller收集发生的事件并和被激活的channel一起上报给所属的loop。在loop中,channel中被注册的相应回调被执行,该回调的注册流程上面已经提到过。

以上内容就是muduo在运行时包含的基本流程。

相关推荐
straw_hat.2 小时前
32HAL——RTC时钟
stm32·学习
知识分享小能手4 小时前
jQuery 入门学习教程,从入门到精通, jQuery在HTML5中的应用(16)
前端·javascript·学习·ui·jquery·html5·1024程序员节
吃个糖糖4 小时前
Pytorch 学习之Transforms
人工智能·pytorch·学习
常常不爱学习4 小时前
Vue3 + TypeScript学习
开发语言·css·学习·typescript·html
CandyU25 小时前
UE5 C++ 进阶学习 小知识点 —— 01 - 本地化语言
学习·ue5
武陵悭臾5 小时前
Python应用开发学习: Pygame 中实现数字水平靠右对齐和垂直靠底对齐
python·学习·程序人生·游戏·个人开发·学习方法·pygame
Tonya435 小时前
测开学习DAY26
学习
水月wwww6 小时前
vue学习之组件与标签
前端·javascript·vue.js·学习·vue
952366 小时前
数据结构-链表
java·数据结构·学习