Reactor反应堆模式

参考代码:https://gitee.com/yegelute/my_-linux-code/tree/master/Reactor

主要以代码为主,下面张贴部分核心结构

附录:

Connection.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include "InetAddr.hpp"

// 封装fd,保证给每一个fd一套缓冲
class Reactor;
class Connection;

using handler_t = std::function<std::string(std::string&)>;

// 基类
class Connection
{
public:
    Connection() :_events(0), _owner(nullptr) {}

    void SetEvent(const uint32_t& events) { _events = events; }

    uint32_t GetEvent() { return _events; }

    void SetOwner(Reactor* owner) { _owner = owner; }

    Reactor* GetOwner() { return _owner; }
    
    virtual void Recver() = 0;
    virtual void Sender() = 0;
    virtual void Excepter() = 0;
    virtual int GetSockFd() = 0;

    void RegisterHandler(handler_t handler)
    {
        _handler = handler;
    }

    ~Connection() {}

private:
    // 关心事件
    uint32_t _events;

    // 回指指针
    Reactor* _owner;

public:
    handler_t _handler; // 基类中定义了一个回调函数
};

Accepter.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Connection;
class TcpServer;
using func_t = std::function<void(Connection *)>;
class Connection
{
public:
    Connection(int sockfd, uint32_t events, TcpServer *R)
        : _sockfd(sockfd), _events(events), _R(R)
    {
    }
    void RegisterCallback(func_t recver, func_t sender, func_t excepter)
    {
        _recver = recver;
        _sender = sender;
        _excepter = excepter;
    }
    void AddInBuffer(std::string buffer)
    {
        _inbuffer += buffer; // 追加到inbuffer中
    }
    void AddOutBuffer(const std::string &buffer)
    {
        _outbuffer += buffer;
    }
    bool OutBufferEmpty()
    {
        return _outbuffer.empty();
    }
    int SockFd()
    {
        return _sockfd;
    }
    uint32_t Events()
    {
        return _events;
    }
    void SetEvents(uint32_t events)
    {
        _events = events;
    }
    void SetClient(const struct sockaddr_in c)
    {
        _client = c;
    }
    std::string &InBuffer()
    {
        return _inbuffer;
    }
    std::string &OutBuffer()
    {
        return _outbuffer;
    }
    void Close()
    {
        ::close(_sockfd);
    }
    ~Connection()
    {
    }

private:
    // 对应的sockfd
    int _sockfd;
    // 对应的缓冲区
    std::string _inbuffer;  // _sockfd 接受缓冲区, 暂时用string代替
    std::string _outbuffer; // _sockfd 发送缓冲区
    // 关心的事件
    uint32_t _events;
    // 维护一下client的ip和port信息
    struct sockaddr_in _client;

public:
    // 对特定connection进行处理的回调函数
    func_t _recver;
    func_t _sender;
    func_t _excepter;
    // TcpServer的回指指针 - TODO
    TcpServer *_R;
};
class ConnectionFactory
{
public:
    static Connection *BuildListenConnection(int listensock, func_t recver,
                                             uint32_t events, TcpServer *R)
    {
        Connection *conn = new Connection(listensock, events, R);
        conn->RegisterCallback(recver, nullptr, nullptr);
        return conn;
    }
    static Connection *BuildNormalConnection(int sockfd,
                                             func_t recver,
                                             func_t sender,
                                             func_t excepter,
                                             uint32_t events,
                                             TcpServer *R)
    {
        Connection *conn = new Connection(sockfd, events, R);
        conn->RegisterCallback(recver, sender, excepter);
        return conn;
    }
};

HandlerConnction.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <cerrno>
#include "Connection.hpp"
#include "Protocol.hpp"
#include "Calculate.hpp"
#include "Log.hpp"
using namespace Protocol;
using namespace CalCulateNS;
const static int buffer_size = 1024;
class HandlerConnection
{
public:
    static void HandlerRequest(Connection *conn)
    {
        std::string &inbuffer = conn->InBuffer();
        std::string message; // 表⽰一个符合协议的一个完整的报文
        Calculate calulate;  // 负责业务处理
        Factory factory;
        auto req = factory.BuildRequest();
        // 1. 明确报文边界,解决粘报问题
        while (Decode(inbuffer, &message))
        {
            // message 一定是一个完整的报文,符合协议的!
            // 2. 反序列化
            if (!req->Deserialize(message))
                continue;
            // 3. 业务处理
            auto resp = calulate.Cal(req);
            // 4. 对相应进行序列化
            std::string responseStr;
            resp->Serialize(&responseStr);
            // 5. 封装完整报文
            responseStr = Encode(responseStr);
            // 6. 将应答全部追加到outbuffer中
            conn->AddOutBuffer(responseStr);
        }
        // 考虑发送的问题了
        if (!conn->OutBufferEmpty())
        {
            conn->_sender(conn); // 对写事件,直接发!!!--- 不代表能全部发完!
        }
    }
    // 在这⾥读取的时候,我们关系数据是什么格式?协议是什么样⼦的吗?
    // 不关心!!!我们只负责把本轮属于完全读取完毕 --- 把读到的字节流数据,交给上层 --- 由上层进行分析处理
    static void Recver(Connection *conn)
    {
        errno = 0;
        // 读取流程
        char buffer[1024];
        while (true)
        {
            ssize_t n = recv(conn->SockFd(), buffer, sizeof(buffer) - 1, 0);
            // ⾮阻塞读取
            if (n > 0)
            {
                buffer[n] = 0;
                conn->AddInBuffer(buffer);
            }
            else
            {
                // std::cout << "................... errno:" << errno <<
                std::endl;
                if (errno == EAGAIN)
                    break;
                else if (errno == EINTR)
                    continue;
                else
                {
                    // 真正的读取错误
                    conn->_excepter(conn); // 直接回调自己的异常处理就可以了!
                    return;
                }
            }
        }
        std::cout << "sockfd# " << conn->SockFd() << ":\n"
                  << conn->InBuffer() << std::endl;
        // 尝试分析处理报文 -- 半个,一个半, 10个,11个半
        HandlerRequest(conn);
    }
    static void Sender(Connection *conn)
    {
        errno = 0;
        std::string &outbuffer = conn->OutBuffer();
        while (true)
        {
            ssize_t n = send(conn->SockFd(), outbuffer.c_str(),
                             outbuffer.size(), 0);
            if (n >= 0)
            {
                outbuffer.erase(0, n); // 已经发给OS的,就直接移除了 // conn-
                > remove(n);
                if (outbuffer.empty())
                    break;
            }
            else
            {
                if (errno == EAGAIN)
                    break; // 只有这⾥,才会正常退出
                else if (errno == EINTR)
                    continue;
                else
                {
                    conn->_excepter(conn);
                    return;
                }
            }
        }
        // ⾛到这⾥,意味着什么?我们本轮发满了,但是数据可能没发完,为什么没发完呢?
        // 开启对conn->SockFd() EPOLLOUT的关心!!!!!, 如何开启对于特定一个
        connection对应的写事件关心呢??? if (!conn->OutBufferEmpty())
        {
            conn->_R->EnableReadWrite(conn->SockFd(), true, true);
        }
        else
        {
            conn->_R->EnableReadWrite(conn->SockFd(), true, false);
        }
    }
    static void Excepter(Connection *conn)
    {
        lg.LogMessage(Info, "connection erase done, who: %d\n", conn - > SockFd());
        errno = 0;
        // 从epoll中移除对conn->Sockfd的关心
        // unordered_map 移除conn
        conn->_R->RemoveConnection(conn->SockFd());
        // 关闭conn->Sockfd
        conn->Close();
        // delete conn
        delete conn;
    }
};

Reactor模式是一种事件驱动的编程模式,用于高效地处理并发 I/O 操作。它通过一个或多个事件循

环(Event Loop)来监听和处理各种事件(如⽹络请求、定时器事件等),从而实现高效的并发处理,而⽆需为每个连接创建一个线程或进程。

附录

OneThreadOneLoop多进程方案:

One Thread One Loop (OTOL)是一种设计模式,通常用于描述基于事件驱动编程(Event Driven Programming)的架构,特别是在使用异步I/O(Asynchronous I/O)框架时。这种模式强调每个线程运行一个独立的事件循环(Event Loop),从而实现高效的并发处理。

核心概念

事件循环(Event Loop):

▪ 事件循环是异步编程的核心,负责监听事件(如I/O操作、定时器事件等)并触发相应的回调函数。

▪ 它通常是一个单线程的执行模型,通过多路复用技术(如 epoll 、 select 或 poll )高效地管理多个I/O操作。

单线程模型:

▪ 在 One Thread One Loop 模式中,每个线程运行一个独立的事件循环。

▪ 这种设计避免了多线程编程中的复杂同步问题,同时利用了现代操作系统高效的I/O多路复 用机制。

• 只要把单Reactor写完,扩展多进程很容易

• 每个进程管理的连接和fd,彼此不要重复

• 每个Reacor,自己处理自己的sockfd的完整⽣命周期,不涉及任何IO穿插,乱序问题

OneThreadOneLoop多线程方案1:

注意:管道也能线程间通信,所以我们可以使用同样的方案进行设计

cpp 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>
#include <cstring>
// 定义管道的读写端
int pipefds[2];
// ⼯作线程函数
void worker_thread()
{
    char buffer[256];
    int n;
    while (true)
    {
        n = read(pipefds[0], buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0'; // 确保字符串结尾
            std::cout << "Worker thread received: " << buffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "No more data to read. Exiting worker thread." << std::endl;
            break;
        }
        else
        {
            std::cerr << "Error reading from pipe." << std::endl;
            break;
        }
    }
}
int main()
{
    // 创建管道
    if (pipe(pipefds) == -1)
    {
        perror("pipe");
        return 1;
    }
    // 创建⼯作线程
    std::thread worker(worker_thread);
    // 主线程向管道写⼊数据
    const char *messages[] = {"Hello from main thread!", "Another message.",
                              "Goodbye!"};
    for (const char *msg : messages)
    {
        sleep(1);
        if (write(pipefds[1], msg, strlen(msg)) < 0)
        {
            perror("write");
            break;
        }
        sleep(1);
        std::cout << "Main thread sent: " << msg << std::endl;
    }
    // 等待⼯作线程结束
    worker.join();
    // 关闭管道(可选)
    close(pipefds[0]);
    close(pipefds[1]);
    return 0;
}
// 可以定义多个管道
// struct pipefd
//{
//  int fds[2]
// };
// std::vector<struct pipefd> pipes;

另外:

• 多线程是可以共享文件fd的,如果我们把⼯作职责变一下(其实就是修改一下connection的回调方法),让master线程,检测并且直接accepter到新的连接fd列表,然后通过管道传递给每一个⼦进程,每个⼦进程拿到sockfd,直接添加到自己的Reactor中,进行IO处理就可以,这样做会更简单。

• 这样,读取管道内容,按照4字节读取,⾃动序列和反序列化

如果不想使用管道,可以又其他做法。

Eventfd事件驱动

• 如果不想用管道,可以选择下列方案进行驱动

• 注意:基于事件驱动,就必须由多路转接,也就是必须基于fd文件描述符,这也就是为什么我们上面用管道的原因。

cpp 复制代码
NAME 
    eventfd - create a file descriptor for event notification 
SYNOPSIS 
    #include <sys/eventfd.h> 
    int eventfd(unsigned int initval, int flags); 
RETURN VALUE 
    On success, eventfd() returns a new eventfd file descriptor. 
    On error, -1 is returned and errno is set to indicate the error. 

• eventfd 是一个轻量级的事件通知机制,基于文件描述符。

• 它可以与 I/O 多路复用机制(如 epoll )结合使用。

• 内核维护一个 64 位的计数器, write 会增加计数器, read 会减少计数器。

进程间事件通知 - 不写

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
int main()
{
    int efd = eventfd(0, EFD_CLOEXEC); // 创建 eventfd
    if (efd == -1)
    {
        perror("eventfd");
        return 1;
    }
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork");
        return 1;
    }
    if (pid == 0)
    { // ⼦进程
        uint64_t value;
        read(efd, &value, sizeof(value)); // 从 eventfd 读取事件
        printf("Child process received event\n");
        close(efd);
        return 0;
    }
    else
    { // ⽗进程
        uint64_t value = 1;
        write(efd, &value, sizeof(value)); // 向 eventfd 写⼊事件
        printf("Parent process sent event\n");
        wait(NULL); // 等待⼦进程结束
        close(efd);
        return 0;
    }
}

EFD_CLOEXEC :确保文件描述符在 exec 时被⾃动关闭,防⽌被新程序继承。

线程间事件通知 - 不写

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <pthread.h>
void *worker_thread(void *arg)
{
    int efd = *(int *)arg;
    uint64_t value;
    read(efd, &value, sizeof(value)); // 从 eventfd 读取事件
    printf("Worker thread received event\n");
    return NULL;
}
int main()
{
    int efd = eventfd(0, EFD_CLOEXEC); // 创建 eventfd[^33^]
    if (efd == -1)
    {
        perror("eventfd");
        return 1;
    }
    pthread_t thread;
    pthread_create(&thread, NULL, worker_thread, &efd); // 创建⼯作线程
    uint64_t value = 1;
    write(efd, &value, sizeof(value)); // 向 eventfd 写⼊事件
    printf("Main thread sent event\n");
    pthread_join(thread, NULL); // 等待⼯作线程结束
    close(efd);
    return 0;
}

特点:

• 低开销: eventfd 内部是一个 64 位计数器,内核维护成本低。

• ⽀持多路复用:可以与 epoll 、 poll 或 select 等 I/O 多路复用机制结合使用。

• 原⼦性:读写操作是原⼦的,适合高并发场景。

• ⼴播通知:可以用于多对多的事件通知,而不仅仅是点对点通信。

• 高效性:相⽐传统管道, eventfd 避免了多次数据拷⻉,且内核开销更⼩。

注意事项:

• 仅用于事件通知: eventfd 不能传递具体的消息内容,仅用于通知事件的发⽣。

• ⾮阻塞模式:建议设置 EFD_NONBLOCK ,避免阻塞操作。

• 信号量语义:如果需要信号量语义(每次读取计数器减 1),需设置 EFD_SEMAPHORE 。

• 资源管理:使用完 eventfd 后,记得关闭文件描述符。

• 结合 I/O 多路复用:在高并发场景下,建议结合 epoll 使用。

Eventfd⼯作模式:

• 普通模式:不设置 EFD_SEMAPHORE ,读取的时候,计数器会清空

• 设置EFD_SEMAPHORE :信号量模式

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
int main()
{
    // 模式1: 创建 eventfd, 不设置EFD_SEMAPHORE
    int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    // 模式2: 创建 eventfd,设置 EFD_SEMAPHORE 和 EFD_NONBLOCK
    // int efd = eventfd(0, EFD_SEMAPHORE | EFD_NONBLOCK | EFD_CLOEXEC);
    if (efd == -1)
    {
        perror("eventfd");
        return 1;
    }
    uint64_t value;
    // 写⼊ 1
    value = 1;
    write(efd, &value, sizeof(value));
    printf("Wrote 1 to eventfd\n");
    // 写⼊ 2
    value = 2;
    write(efd, &value, sizeof(value));
    printf("Wrote 2 to eventfd\n");
    value = 0; // 防⽌⼲扰,value直接清空
    // 读取一次(计数器减 1)
    read(efd, &value, sizeof(value));
    printf("Read value: %llu (counter is now 2)\n", (unsigned long long)value);
    value = 0; // 防⽌⼲扰,value直接清空
    // 再次读取一次(计数器减 1)
    read(efd, &value, sizeof(value));
    printf("Read value: %llu (counter is now 1)\n", (unsigned long long)value);
    value = 0; // 防⽌⼲扰,value直接清空
    // 再次读取一次(计数器减 1)
    read(efd, &value, sizeof(value));
    printf("Read value: %llu (counter is now 0)\n", (unsigned long long)value);
    close(efd);
    return 0;
}

模式1输出:

bash 复制代码
Wrote 1 to eventfd 
Wrote 2 to eventfd 
Read value: 3 (counter is now 2) // 一次读完,直接清空 
Read value: 0 (counter is now 1) 
Read value: 0 (counter is now 0)

模式2输出:

bash 复制代码
Wrote 1 to eventfd 
Wrote 2 to eventfd 
Read value: 1 (counter is now 2) // 每次读取,计数器-1 
Read value: 1 (counter is now 1) 
Read value: 1 (counter is now 0) 

我们后面只需要用eventfd进行事件通知,选择模式1(默认)即可

OneThreadOneLoop多线程方案2:

相关推荐
Codefengfeng9 小时前
分辨压缩包的真加密与伪加密
linux·运维·网络
白太岁9 小时前
通信:(3) 高并发网络通信:epoll + 边沿触发 + 非阻塞 IO + tcp
c语言·网络·c++·网络协议·tcp/ip
duration~11 小时前
DHCP 协议详解
网络·网络协议·tcp/ip
代码改善世界13 小时前
【C语言】线性表之顺序表、单链表、双向链表详解及实现
c语言·网络·链表
嵌入式×边缘AI:打怪升级日志16 小时前
9.2.1 分析 Write File Record 功能(保姆级讲解)
java·开发语言·网络
天荒地老笑话么17 小时前
Bridged 与虚拟机扫描:合规边界与自测范围说明
网络·网络安全
TechubNews17 小时前
燦谷(Cango Inc)入局AI 資本重組彰顯決心
大数据·网络·人工智能·区块链
艾莉丝努力练剑18 小时前
【Linux:文件】进程间通信
linux·运维·服务器·c语言·网络·c++·人工智能