Reactor、epoll下设计一个简单的网络版本计算器

Reactor 模式详解

Reactor 是一种事件驱动的设计模式,特别适用于处理大量并发 I/O 操作的场景。在 Linux 系统中,Reactor 通常基于 epollselectpoll 等 I/O 多路复用机制实现。下面从概念、组件、工作流程、实现示例、优缺点以及与 Proactor 的对比等方面进行详细说明。

1. 什么是 Reactor 模式

Reactor 模式的核心思想是:将服务端对请求的处理过程分为"事件等待"和"事件处理"两个阶段。一个或多个输入源(如 socket 连接)同时将事件(可读、可写等)注册到一个中心调度器(Reactor)上,Reactor 使用 I/O 多路复用技术监听这些事件,当事件发生时,将事件分发给预先注册的事件处理器(Handler)执行。

典型应用场景

  • 网络服务器(如 Nginx、Redis、Netty 等)
  • 图形用户界面事件循环
  • 任何需要高并发 I/O 处理的系统

2. Reactor 在 Linux 中的实现基础

Linux 提供了多种 I/O 多路复用机制:

  • select:最古老,有最大文件描述符限制(通常 1024),效率随描述符数量线性下降。
  • poll:无固定上限,但仍需遍历所有描述符。
  • epoll:Linux 特有的高效机制,采用事件驱动,仅返回就绪的描述符,支持边缘触发(ET)和水平触发(LT),适合高并发场景。

现代 Linux 下的 Reactor 实现几乎都基于 epoll,因此下文重点以 epoll 为例。

3. Reactor 的核心组件

|----------------------------------|-------------------------------------------------------|
| 组件 | 说明 |
| 事件源(Event Source) | 通常是文件描述符(fd),如监听 socket、已连接 socket、定时器 fd 等。 |
| 事件多路分发器(Event Demultiplexer) | 即 I/O 多路复用函数(epoll_wait),负责等待事件发生。 |
| 反应器(Reactor) | 事件循环的调度器,负责注册/注销事件,并调用 demultiplexer 等待事件,然后分发到对应处理器。 |
| 事件处理器(EventHandler) | 定义处理事件的方法(如 handle_input、handle_output),通常由用户实现。 |

4. Reactor 的工作流程

  1. 初始化 Reactor:创建 epoll 实例,初始化事件循环。
  2. 注册事件:将需要监听的 fd(如 listen socket)及其对应的处理器注册到 Reactor。
  3. 事件循环
    • 调用 epoll_wait 等待事件就绪。
    • 对于每个就绪的事件,根据 fd 找到对应的 EventHandler。
    • 调用该 Handler 的特定方法(如 handle_readhandle_write)处理事件。
  1. 事件处理:在 Handler 中执行具体业务逻辑(如接受新连接、读取数据、发送响应等),处理过程中可能向 Reactor 注册新的事件(例如读完后注册写事件)。

5. Reactor 的变体

根据事件处理线程模型的不同,Reactor 通常有三种变体:

5.1 单 Reactor 单线程

  • 所有事件(accept、read、write、业务处理)都在一个线程中完成。
  • 实现简单,但无法利用多核 CPU,且业务处理可能阻塞 I/O 循环。

5.2 单 Reactor 多线程

  • Reactor 线程负责事件分发,具体业务处理(如解码、计算)交给线程池。
  • 需要注意线程安全,适合计算密集型场景。

5.3 多 Reactor 多线程(主从 Reactor)

  • 主 Reactor 只负责处理新连接的 accept,将已连接 socket 分配给子 Reactor。
  • 子 Reactor 负责处理该连接上的读写事件,每个子 Reactor 可运行在独立线程中。
  • 这是 Nginx、Netty 等高性能服务器的典型架构。

6. Reactor 的优缺点

优点

  • 高并发:单线程可处理成千上万连接,避免了多线程切换开销。
  • 可扩展性:可以通过增加 Reactor 线程数(多 Reactor)或工作线程池来提升处理能力。
  • 模块化:事件与处理分离,易于维护和测试。
  • 非阻塞:配合非阻塞 I/O,不会因为某个连接的慢速 I/O 阻塞整个系统。

缺点

  • 编程复杂:需要将业务逻辑拆分为异步回调,容易陷入"回调地狱"。
  • 调试困难:异步流程难以用传统单步调试方式跟踪。
  • 不适合 CPU 密集型任务:如果业务处理耗时,会阻塞事件循环,此时应使用线程池或专用线程。

代码

Mutex.hpp

复制代码
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard
{
public:
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};

Logger.hpp

复制代码
#pragma once

#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"

// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

// 日志转换成为字符串
std::string Level2String(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "Debug";
    case LogLevel::INFO:
        return "Info";
    case LogLevel::WARNING:
        return "Warning";
    case LogLevel::ERROR:
        return "Error";
    case LogLevel::FATAL:
        return "Fatal";
    default:
        return "Unknown";
    }
}

// 根据时间戳,获取可读性较强的时间信息
// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);

    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 转换成为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

// 策略模式,策略接口
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    // 不同模式核心是刷新方式的不同
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    // 显示器也是临界资源,保证输出线程安全
    Mutex _lock;
};

// 默认路径和日志名称
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件日志策略
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    // 构造函数,建立出来指定的目录结构和文件结构
    FileLogStrategy(const std::string &dir = logdefaultdir,
                    const std::string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    // 将一条日志信息写入到文件中
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            // 追加方式
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 具体的日志类
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    // 内部类,实现RAII风格的日志格式化和刷新
    // 这个LogMessage,表示一条完整的日志对象
    class LogMessage
    {
    public:
        // RAII风格,构造的时候构建好日志头部信息
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),
              _level(level),
              _pid(getpid()),
              _filename(filename),
              _line(line),
              _logger(logger)
        {
            // stringstream不允许拷贝,所以这里就当做格式化功能使用
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
            _loginfo = ss.str();
        }
        // 重载 << 支持C++风格的日志输入,使用模版,表示支持任意类型
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }
        // RAII风格,析构的时候进行日志持久化,采用指定的策略
        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time; // 日志时间
        LogLevel _level;        // 日志等级
        pid_t _pid;             // 进程pid
        std::string _filename;
        int _line;

        std::string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
    };
    // 故意拷贝,形成LogMessage临时对象,后续在被<<时,会被持续引用,
    // 直到完成输入,才会自动析构临时LogMessage,至此也完成了日志的显示或者刷新
    // 同时,形成的临时对象内包含独立日志数据
    // 未来采用宏替换,进行文件名和代码行数的获取
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }

private:
    // 写入日志的策略
    std::unique_ptr<LogStrategy> _strategy;
};

// 定义全局的logger对象
Logger logger;

// 使用宏,可以进行代码插入,方便随时获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

InetAddr.hpp

复制代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include "Logger.hpp"

using namespace std;

#define Conv(addr) ((struct sockaddr *)&addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        // _ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64];
        inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        // _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
    }

public:
    InetAddr(){}
    // 默认ip为INADDR_ANY(0.0.0.0)
    InetAddr(uint16_t port, const string ip = "0.0.0.0")
        : _ip(ip),
          _port(port)
    {
        Host2Net();
    }

    InetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        Net2Host();
    }

    void Init(const struct sockaddr_in peer)
    {
        _addr = peer;
        Net2Host();
    }

    struct sockaddr *Addr()
    {
        return Conv(_addr);
    }

    string IP()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    bool operator==(const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }

    socklen_t Length()
    {
        return sizeof(_addr);
    }

    string ToString()
    {
        return _ip + "-" + to_string(_port);
    }

    ~InetAddr()
    {
    }

private:
    // 网络风格地址
    struct sockaddr_in _addr;

    // 主机风格地址
    string _ip;
    uint16_t _port;
};

Socket.hpp

复制代码
#pragma once
#include <iostream>
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"
#include"Utils.hpp"
using namespace std;

static int gbacklog = 16; // 设置默认的backlog
static const int gsockfd = -1;

// 设置错误码
enum
{
    OK,
    CREATE_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

// 定义一个抽象类,父类中定义算法的骨架,将某些步骤的具体实现延迟到子类中
class Socket
{
public:
    // 编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,
    // 所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写
    // 基类必须有虚析构函数
    virtual ~Socket() {}
    // 纯虚函数强制派生类重写虚函数
    virtual void CreatSocket() = 0;
    virtual void BindSocket(int port) = 0;
    virtual void ListenSocket() = 0;
    //virtual shared_ptr<Socket> Accept(InetAddr *clientaddr) = 0;
     virtual int Accept(InetAddr *clientaddr) = 0;
    virtual bool Connect(InetAddr &peer) = 0;
    virtual int SockFd() = 0;
    virtual void Close() = 0;
    virtual ssize_t Recv(std::string *out) = 0;
    virtual ssize_t Send(const std::string &in) = 0;

public:
    void BuildListenSocketMethod(int port)
    {
        CreatSocket();
        BindSocket(port);
        ListenSocket();
    }

    void BuildClientSocketMethod()
    {
        CreatSocket();
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(gsockfd)
    {
    }
    TcpSocket(int sockfd) : _sockfd(sockfd)
    {
    }
    // override帮助用户检测是否重写
    void CreatSocket() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "creat socket error";
            exit(CREATE_ERR);
        }
        //注意!!!  Reactor(ET)模式下listensocket必须设为非阻塞
        SetNonBlock(_sockfd);
        LOG(LogLevel::DEBUG) << "creat socket success"<<" _sockfd:"<<_sockfd;
    }
    void BindSocket(int port) override
    {
        InetAddr local(port);
        if (bind(_sockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";
    }
    void ListenSocket() override
    {
        if (listen(_sockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }
    int Accept(InetAddr *clientaddr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int clientfd = accept(_sockfd, (sockaddr *)&peer, &len);
        if (clientfd < 0)
        {
            LOG(LogLevel::FATAL) << "accept error";
            perror("accept error");
            return -1;
        }
        LOG(LogLevel::DEBUG)<<"accept success";
        clientaddr->Init(peer);
        return clientfd;
    }
    bool Connect(InetAddr &peer) override
    {
        if (connect(_sockfd, peer.Addr(), peer.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "connect error";
            return false;
        }
        LOG(LogLevel::DEBUG) << "connect " << peer.ToString() << " success";
        return true;
    }
    int SockFd() override
    {
        return _sockfd;
    }
    void Close() override
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }
    // 今天重点放在读,通过读,理解序列和反序列和自定义协议的过程
    ssize_t Recv(std::string *out) override
    {
        // 只读一次
        char buffer[1024];
        ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            *out += buffer; // 故意+=
        }
        return n;
    }
    ssize_t Send(const std::string &in)
    {
        // send 用于 已建立连接 的套接字(典型如 TCP),数据将自动发送给连接的对端。
        // sendto 主要用于 无连接 的套接字(典型如 UDP),每次发送都需要明确指定目标地址。
        return send(_sockfd, in.c_str(), in.size(), 0);
    }

    ~TcpSocket() {}

private:
    int _sockfd;
};

Connection.hpp

复制代码
#pragma once
#include <string>
#include "InetAddr.hpp"

//头文件需要交叉包含时:只能一个头文件A包含B,如果B要用A里面的,只能声明
class Reactor;

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

class Connection
{
public:
    Connection() : _events(0), _owner(nullptr)
    {
    }
    virtual void Recver() = 0;
    virtual void Sender() = 0;
    virtual void Excepter() = 0;
    virtual ~Connection() {}
    int Sockfd() { return _sockfd; }
    void SetSocketfd(int sockfd) { _sockfd = sockfd; }
    void SetEvents(uint32_t events) { _events = events; }
    uint32_t Events() { return _events; }
    void SetAddr(const InetAddr &addr) { _peer = addr; }
    Reactor *Owner() { return _owner; }
    void SetOwner(Reactor *r) { _owner = r; }
    void SetCallback(callback_t cb) { _cb = cb; }
protected:
    int _sockfd;
    uint32_t _events;
    string _inbuffer;
    string _outbuffer;
    InetAddr _peer;
    Reactor *_owner; // 回指指针,方便我们添加或则使用Reactor中的方法 !
    callback_t _cb;
};

Reactor.hpp

复制代码
#pragma once
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Connection.hpp"
static const int gsize = 128;

class Reactor
{
private:
   bool IsExist(std::shared_ptr<Connection> &connection)
   {
      auto iter = _connections.find(connection->Sockfd());
      return iter != _connections.end();
   }
   bool IsExist(int sockfd)
   {
      auto iter = _connections.find(sockfd);
      return iter != _connections.end();
   }

public:
   Reactor() : _epoller(make_unique<Epoller>())
   {
   }
   void AddConnection(shared_ptr<Connection> connection)
   {
      if (IsExist(connection))
      {
         LOG(LogLevel::DEBUG) << "This connection is already in the reactor";
         return;
      }
      // 1.让connection回指当前reactor
      connection->SetOwner(this);
      // 2.添加到_connections中
      _connections[connection->Sockfd()] = connection;
      // 3.写到内核中,epoller
      _epoller->AddEvent(connection->Sockfd(), connection->Events());
      LOG(LogLevel::DEBUG) << "this connection add to reactor";
   }
   void DelConnection(int sockfd)
   {
      if (!IsExist(sockfd))
      {
         LOG(LogLevel::WARNING) << sockfd << " conn not in Reactor[delete]!";
         return;
      }
      // 1. 从内核中移除对该sockfd的关心
      _epoller->DelEvent(sockfd);

      // 2. _connections移除到kv
      _connections.erase(sockfd);

      // 3. close关闭fd
      close(sockfd);

      LOG(LogLevel::INFO) << "remove conn " << sockfd << " from reactor success";
   }
   // 使能读写
   void EnableReadWrite(int sockfd, bool enableread, bool enablewrite)
   {
      if (!IsExist(sockfd))
      {
         LOG(LogLevel::WARNING) << sockfd << " conn not in Reactor, bug!";
         return;
      }
      // 1. 修改connection对象
      uint32_t events = (enableread ? EPOLLIN : 0) | (enablewrite ? EPOLLOUT : 0) | EPOLLET;
      _connections[sockfd]->SetEvents(events);

      // 2. 写透到内核
      // _epoller->ModEvent(_connections[sockfd]->Sockfd(), _connections[sockfd]->Events());
      _epoller->ModEvent(sockfd, events);
   }
   //LoopOnce处理一批就绪的事件
    void LoopOnce(int timeout)
    {
        int n = _epoller->WaitEvent(_ready, gsize, timeout);
        for (int i = 0; i < n; i++)
        {
            int sockfd = _ready[i].data.fd; // 就绪的sockfd
            uint32_t events = _ready[i].events;

            // 统一报错的处理方法!
            //EPOLLHUP:文件描述符被挂断->对端关闭连接、管道写端关闭、终端挂断
            if (events & EPOLLHUP)
                events = (EPOLLIN | EPOLLOUT);
            //EPOLLERR:文件描述符发生错误
            if (events & EPOLLERR)
                events = (EPOLLIN | EPOLLOUT);
            // 增加代码的鲁棒性!
            if ((events & EPOLLIN) && IsExist(sockfd))
            {
                _connections[sockfd]->Recver();
            }
            if ((events & EPOLLOUT) && IsExist(sockfd))
            {
                _connections[sockfd]->Sender();
            }
        }
    }
    void Dispatcher()
    {
        int timeout = 3000;
        while (true)
        {
            // 处理事件
            LoopOnce(timeout);
            // ShowConnection();
            // CheckTimeOut();
            // 连接管理
            // CheckTimeOut();
        }
    }

   ~Reactor() {}

private:
   unordered_map<int, shared_ptr<Connection>> _connections;
   unique_ptr<Epoller> _epoller;
   struct epoll_event _ready[gsize];
};

Epoller.hpp

复制代码
#pragma once
#include<stdio.h>
#include <iostream>
#include <sys/epoll.h>
#include "Utils.hpp"
#include "Logger.hpp"
#include"Utils.hpp"

using namespace std;

class Epoller
{
    private:
    int OperEvent(int op,int fd,uint32_t events)
    {
        struct epoll_event ev;
        ev.data.fd=fd;
        ev.events=events;
        //LOG(LogLevel::DEBUG)<<"Oper _epfd:"<<_epfd<<" fd:"<<fd;
        return epoll_ctl(_epfd,op,fd,&ev);
    }
    public:
    Epoller()
    {
        _epfd=epoll_create(1);
        if(_epfd<0)
        {
            LOG(LogLevel::ERROR)<<"epoll_create error";
            exit(-1);
        }
        LOG(LogLevel::DEBUG)<<"_epfd:"<<_epfd<<" epoll_create success";
    }
    void AddEvent(int fd,uint32_t events)
    {
        int n=OperEvent(EPOLL_CTL_ADD,fd,events);
        if(n<0)
        {
            LOG(LogLevel::ERROR)<<fd<<":add "<<Event2String(events)<<"to eopller fail";
            perror("add error");
            return;
        }
        LOG(LogLevel::DEBUG)<<fd<<":add "<<Event2String(events)<<"to eopller success";
    }
    void DelEvent(int fd)
    {
        int n=epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,nullptr);
        if(n<0)
        {
            LOG(LogLevel::ERROR)<<fd<<":del "<<"from eopller fail";
            return;
        }
        LOG(LogLevel::DEBUG)<<fd<<":del "<<"from eopller success";
    }
    void ModEvent(int fd,uint32_t events)
    {
        int n=OperEvent(EPOLL_CTL_MOD,fd,events);
        if(n<0)
        {
            LOG(LogLevel::ERROR)<<fd<<":mod "<<Event2String(events)<<"from eopller fail";
            return;
        }
        LOG(LogLevel::DEBUG)<<fd<<":mod "<<Event2String(events)<<"from eopller success";
    }
    int WaitEvent(struct epoll_event ready[],int maxevents,int timeout)
    {
        int n=epoll_wait(_epfd,ready,maxevents,timeout);
        if(n>0)
        {
            LOG(LogLevel::DEBUG)<<n<<"个fd就绪";
        }
        else if(n==0)
        {
            LOG(LogLevel::DEBUG)<<"timeout";
        }
        else
        {
            LOG(LogLevel::ERROR)<<"epoll wait fail";
        }
        return n;
    }
    ~Epoller()
    {
        if(_epfd>=0)
        close(_epfd);
    }
    private:
    int _epfd;
};

Channel.hpp

复制代码
#pragma once
#include"Epoller.hpp"
#include"Connection.hpp"

static const int buffersize = 1024;

class Channel:public Connection
{
public:
    Channel(int sockfd, InetAddr &addr)
    {
        SetSocketfd(sockfd);
        SetAddr(addr);
        SetEvents(EPOLLIN | EPOLLET);
    }
     void Recver() override
    {
        while (true)
        {
            char buffer[buffersize];
            ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0); // 非阻塞的
            if (n > 0)
            {
                buffer[n] = 0;
                _inbuffer += buffer;
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client quit, client is : " << _peer.ToString();
                Excepter();
                return;// 注意这里是return
            }
            else
            {
                if (errno == EAGAIN)
                {
                    break;
                }
                else if (errno == EINTR)
                {
                    continue;
                }
                else
                {
                    LOG(LogLevel::INFO) << "recv error, client is : " << _peer.ToString();
                    Excepter();
                    return; // 注意这里是return
                }
            }
        }

        //我一定把本轮数据全部读完了!
        // 缓冲区内部有没有完整的报文??不知道,不应该知道!
        // 谁知道,谁应该知道??协议知道!
        _outbuffer += _cb(_inbuffer);
        std::cout << "_outbuffer: " << _outbuffer << std::endl;
        // 如何发送??方案二:使能写关心!
        if(!_outbuffer.empty())
            Owner()->EnableReadWrite(_sockfd, true, true);

        // 如何发送??方案一:最佳实践,直接发!
        // if(!_outbuffer.empty())
        //     Sender();
    }
    void Sender() override
    {
        while(true)
        {
            ssize_t n = send(_sockfd, _outbuffer.c_str(), _outbuffer.size(), 0);
            if(n > 0)
            {
                _outbuffer.erase(0, n);
                if(_outbuffer.size() == 0)
                    break;
            }
            else if(n == 0)
            {
                break;
            }
            else if( n < 0 )
            {
                if(errno == EAGAIN)
                {
                    break;
                }
                else if(errno == EINTR)
                {
                    continue;
                }
                else{
                    Excepter();
                    return;
                }
            }
        }

        
        if(!_outbuffer.empty()) 
        {
            //2. 缓冲区被写满了,需要对端读取一部分数据后才能继续发
            // 开启sockfd对写事件的关心!
            Owner()->EnableReadWrite(_sockfd, true, true);
        }
        else
        {
             //1. 发完了
            // 关闭对sockfd写事件的关心
            Owner()->EnableReadWrite(_sockfd, true, false);
        }
    }
    void Excepter() override
    {
        // 统一错误处理!
        Owner()->DelConnection(_sockfd);
    }

    ~Channel(){}
};

Listener.hpp

复制代码
#pragma once
#include "Socket.hpp"
#include"Reactor.hpp"
#include "Connection.hpp"
#include"Channel.hpp"
#include"Utils.hpp"
class Listener : public Connection
{
public:
    Listener(int port) : _listensocket(make_unique<TcpSocket>()), _port(port)
    {
        _listensocket->BuildListenSocketMethod(_port);
        SetSocketfd(_listensocket->SockFd());
        SetEvents(EPOLLET | EPOLLIN);
    }
    void Recver()override
    {
        // 获取新连接,非阻塞循环获取,因为我们是ET模式,要保证本次获取连接,都必须处理完
        while (true)
        {
            InetAddr clientaddr;
            int error = 0;
            //因为listensocket设置为非阻塞,而accept又一直在循环调用
            //所以出现accept success后在出现accept error很正常因为没有新的连接获取了
            //"Resource temporarily unavailable" 是操作系统返回的错误,
            //对应错误码 EAGAIN(或 EWOULDBLOCK,在许多系统上值相同)。它表示当前资源暂时不可用,请稍后重试
            int sockfd = _listensocket->Accept(&clientaddr);
            if(sockfd < 0)
            {
                //EAGAIN:资源暂时不可用,请稍后重试,系统资源不足
                if(errno == EAGAIN)
                    break;
                //EINTR:系统调用被信号中断
                else if(errno == EINTR)
                    continue;
                else
                    break;
            }
            // 获取新链接成功, 能直接读取吗?不能!应该干什么?添加到Reactor中!
            // 1. 构建新的连接
            SetNonBlock(sockfd);//设为非阻塞
            std::shared_ptr<Connection> conn = std::make_shared<Channel>(sockfd, clientaddr);
            conn->SetCallback(_cb);
            // 2. 添加到Reactor中?
            Owner()->AddConnection(conn);
        }
        
    }
    void Sender()override
    {
        //.....
    }
    void Excepter()override
    {
        //....
    }

    ~Listener() {}

private:
    unique_ptr<Socket> _listensocket;
    uint16_t _port;
};

Protocol.hpp

复制代码
#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
using namespace std;

// 协议就是双方约定好的结构化的数据
// Requst:客户端发送的请求
class Request
{
public:
    Request()
    {
        _x = _y = _oper = 0;
    }
    // 对要发送的数据进行序列化
    bool Serialize(string *out)
    {
        // 1. 构建 Json::Value 对象
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;
        // 2. 使用 StreamWriterBuilder
        Json::StreamWriterBuilder writer; // StreamWriter 的工厂
        writer["emitUTF8"] = true;        // 允许输出UTF-8
        *out = Json::writeString(writer, root);
        if (out->empty())
            return false;
        return true;
    }
    // 对要接受的数据进行反序列化
    bool Deserialize(string &s)
    {
        // 解析JSON 字符串
        Json::Reader reader;
        Json::Value root;
        // 从字符串中读取JSON 数据
        bool ret = reader.parse(s, root);
        if (!ret)
            return false;
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }
    int X()
    {
        return _x;
    }
    int Y()
    {
        return _y;
    }
    char Oper()
    {
        return _oper;
    }
    ~Request() {}
//不做private:,方便客户端访问
//private:
    int _x;
    int _y;
    char _oper;
};

// Response:服务器的应答
class Response
{
public:
    Response()
    {
        _result = _code = 0;
    }
    bool Serialize(string *out)
    {
        // 1. 构建 Json::Value 对象
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        // 2. 使用 StreamWriterBuilder
        Json::StreamWriterBuilder writer; // StreamWriter 的工厂
        writer["emitUTF8"] = true;        // 允许输出UTF-8
        *out = Json::writeString(writer, root);
        if (out->empty())
            return false;
        return true;
    }
    // 对要接受的数据进行反序列化
    bool Deserialize(string &s)
    {
        // 解析JSON 字符串
        Json::Reader reader;
        Json::Value root;
        // 从字符串中读取JSON 数据
        bool ret = reader.parse(s, root);
        if (!ret)
            return false;
        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
    }
    void SetResult(int r)
    {
        _result = r;
    }
    void SetCode(int c)
    {
        _code = c;
    }
    void Print()
    {
        cout << _result << "[" << _code << "]" << endl;
    }
    ~Response() {}

private:
    int _result;
    int _code; // 判断_result是否可信
};
// 定义json中的分隔符
static const string sep = "\r\n";

class Protocol
{
public:
    // 包装jsonstr便于后续解析
    //  jsonstr -> len\r\njsonstr\r\n
    static string Package(const string &jsonstr)
    {
        if (jsonstr.empty())
            return string();
        int jsonstr_len = jsonstr.size();
        return to_string(jsonstr_len) + sep + jsonstr + sep;
    }
    // 检查长度字符串是否符合标准
    static bool DigitSafeCheck(const std::string str)
    {
        for (int i = 0; i < str.size(); i++)
        {
            if (!(str[i] >= '0' && str[i] <= '9'))
                return false;
        }
        return true;
    }

    // 收到的报文有任意种可能
    //  len\r\njsonstr\r\n
    //  len\r\njsonstr\r\nlen\r\njsonstr\r\n
    //  len\r\njsonstr\r\nlen\r
    //  len\r\n
    //  len\r\njs
    //  len\r
    //  len
    //  origin_str: 从网络中读取上来的字符串,输入输出
    //  package: 输出参数,如果有完整的json报文,就返回
     // UnPack对收到的报文进行解析以获得完整的json字符串
    static int UnPack(string &origin_str, string *package)
    {
       //解析工作完成时返回jsonstr的长度,没有完成时返回0 解析出错返回-1
        if (!package)
            return -1;
        auto pos=origin_str.find(sep);// 从左向右
        if(pos==string::npos)
        return 0;
        //此时,我们至少获得了jsonstr的长度
        string json_len=origin_str.substr(0,pos);
        if(!DigitSafeCheck(json_len))
        return -1;
        int length=stoi(json_len);
        // 如果我得到了当前报文的长度
        // 根据协议,我可以推测出,一个完整报文的长度是多少
        // len\r\njsonstr\r\n
        int target_len=json_len.size()+sep.size()*2+length;
        //如果没有一个完整的报文,就返回继续读,直至获得完整的的报文
        if(origin_str.size()<target_len)
        return 0;
    ////////////////////////////////////////////////////////////
    //截止目前,可以确定origin_str内部,一定有一个完整的报文请求!且我们没有对origin_str做任何修改
        *package=origin_str.substr(pos+sep.size(),length);
        origin_str.erase(0, target_len);
        return package->size();
    }
};

Parse.hpp

复制代码
#pragma once
#include <functional>
#include "Protocol.hpp"
#include "Logger.hpp"

using handler_t = function<Response(Request &req)>;

// 只负责对报文进行各种解析工作
class Parser
{
public:
    Parser(handler_t handler)
        : _handler(handler)
    {
    }
    // 解析报文
    string Parse(string &inbuffer)
    {
        LOG(LogLevel::DEBUG) << "inbuffer: \r\n"
                             << inbuffer;
        string send_str;
        // 接受到的字符串可能包含多条报文请求
        while (true)
        {
            std::string jsonstr;
            // 1. 解析报文
            int n = Protocol::UnPack(inbuffer, &jsonstr);
            // 解析出错,直接退出
            if (n < 0)
                break;
            // 未获得一条完整的报文或已经将inbuffer内所有完整报文处理完毕,退出循环
            else if (n == 0)
                break;
            else
            {
                // 解析出的一条完整jsonstr
                LOG(LogLevel::DEBUG) << "jsonstr: \r\n"
                                     << jsonstr;
                Request req;
                // 2. 反序列化
                if (!req.Deserialize(jsonstr))
                {
                    return string();
                }
                // 3. 根据response, 处理具体的业务
                Response resp = _handler(req);

                // 4. 对resp在进行序列化
                string resp_json;
                if (!resp.Serialize(&resp_json))
                {
                    return string();
                }

                // 5. 打包
                send_str += Protocol::Package(resp_json);
            }
        }
        // 若未获得一条完整的报文,返回的是空串
        // 若已经将inbuffer内所有完整报文处理完毕,返回的是打包好的全部答复
        return send_str;
    }

    ~Parser() {}

private:
    handler_t _handler;
};

Utils.hpp

复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

// EPOLLIN EPOLLOUT EPOLLET
std::string Event2String(uint32_t events)
{
    std::string s;
    if (events & EPOLLIN)
    {
        s = "EPOLLIN";
    }
    if (events & EPOLLOUT)
    {
        s += "|EPOLLOUT";
    }
    if (events & EPOLLET)
    {
        s += "|EPOLLET";
    }
    return s;
}

Calculator.hpp

复制代码
#pragma once

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

class Calculator
{
public:
    Calculator() {}
    //Request进,Response出
    Response Exec(Request &req)
    {
        Response resp;
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(1); // 1:div 0
            }
            else
            {
                resp.SetResult(req.X() / req.Y());
            }
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(2); // 2:mod 0
            }
            else
            {
                resp.SetResult(req.X() % req.Y());
            }
        }
        break;
        default:
            resp.SetCode(3); // 3: 非法操作
            break;
        }
        return resp;
    }
    ~Calculator() {}
};

ReactorServer.cc

复制代码
#include <iostream>
#include "Calculator.hpp"
#include "Protocol.hpp"
#include "Parser.hpp"
#include "Logger.hpp"
#include "Reactor.hpp"
#include "Listener.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t serverport = std::stoi(argv[1]);
    // 日志服务
    EnableConsoleLogStrategy();

    // 1. 业务对象
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2. 协议和解析协议对象
    std::unique_ptr<Parser> parse_protocol = std::make_unique<Parser>(
        [&cal](Request &req) -> Response
        {
            return cal->Exec(req);
        });

    // 3. 连接管理器 - Listener
    std::shared_ptr<Connection> listener = std::make_shared<Listener>(serverport);
    listener->SetCallback([&parse_protocol](std::string &inbuffer) -> std::string
                          { return parse_protocol->Parse(inbuffer); });

    // 4. 构建一个Reactor容器
    std::unique_ptr<Reactor> R = std::make_unique<Reactor>();

    // 给reactor中,把连接管理器添加到Reactor
    R->AddConnection(listener);

    // 启动Reactor
    R->Dispatcher();

    return 0;
}

Client.cpp

复制代码
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
#include "InetAddr.hpp"
#include "Protocol.hpp"
using namespace std;
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

// ./Client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    EnableConsoleLogStrategy();

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);
    // 创建socket
    unique_ptr<Socket> sockptr = make_unique<TcpSocket>();
    sockptr->BuildClientSocketMethod();
    InetAddr server(serverport, serverip);
    // 连接
    if (sockptr->Connect(server))
    {
        string inbuffer;
        while (true)
        {
            // 1. 构建请求
            Request req;
            cout << "Please Enter X: ";
            cin >> req._x;
            cout << "Please Enter Y: ";
            cin >> req._y;
            cout << "Please Enter Operator: ";
            cin >> req._oper;

            // 2. 序列化
            string jsonstr;
            req.Serialize(&jsonstr);

            cout << "jsonstr: \r\n"
                 << jsonstr << endl;

            // 3. 打包
            // len\r\njsonstr\r\n
            string sendstr = Protocol::Package(jsonstr);

            cout << "sendstr:\r\n"
                 << sendstr << endl;

            // 4. 发送
            sockptr->Send(sendstr);

            // 5. 接收
            sockptr->Recv(&inbuffer);

            // 6. 报文解析
            string package;
            int n = Protocol::UnPack(inbuffer, &package);
            if (n > 0)
            {
                Response resp;
                // 7. 反序列化
                bool r = resp.Deserialize(package);
                if (r)
                {
                    resp.Print();
                }
            }
        }
    }
    return 0;
}

Reactor模式下为什么listen套接字没设置为非阻塞时服务器收不到消息,它设置里非阻塞和不设置非阻塞有什么区别?

在 Linux 的 Reactor 模式(基于 epollselect)中,将监听套接字设置为非阻塞,并不是为了"让接收消息更快",而是为了防止事件循环被意外阻塞,导致服务器"假死"或"收不到后续的消息"。

要理解为什么"不设置非阻塞"可能导致"收不到消息",需要深入分析 Reactor 模式的工作机制。

1. 核心原因:Reactor 是单线程/单事件循环驱动的

在典型的 Reactor 模式中(如 Redis、Nginx 的某些阶段,或你自己编写的 epoll 服务器),通常只有一个线程在运行事件循环:

  1. 调用 epoll_wait 等待事件。
  2. 当监听套接字(Listen Socket)有事件时,说明有新的客户端连接到达。
  3. 调用 accept() 接受连接。
  4. 将新连接的客户端套接字加入事件监听。

问题的关键出在第 3 步 **accept()**上。

2. 阻塞(默认)模式下发生了什么?

如果监听套接字是阻塞 的,当 epoll_wait 返回说"监听套接字可读"时,你调用 accept()

  • 正常情况(99.9%): 内核的 accept 队列里确实有一个连接。accept() 立即取出连接,返回新的文件描述符,耗时极短,一切正常。
  • 极端情况(边缘触发 + 多连接风暴):
    如果你使用的是 epoll****的 EPOLLET**(边缘触发)模式** ,或者系统发生了特殊的竞争条件(例如:客户端在 accept 之前发起了 RST 包,或者使用了 SO_ATTACH_FILTER 等过滤机制),就会出现一种情况:内核通知你有连接,但当你调用 **accept()**时,队列里已经没有连接了。

这时,阻塞的 **accept()**会做什么?

它会将当前线程挂起(阻塞),直到下一个客户端连接进来。在 Reactor 模式下,这个线程就是整个服务器的"心脏"。一旦这个线程被挂起:

    • 它无法再调用 epoll_wait
    • 它无法处理已经连接上的客户端发送的数据(收不到消息)。
    • 它无法接收新的连接。
    • 服务器完全卡死。

这就是你遇到的"收不到消息"的根本原因。

3. 非阻塞模式下有什么区别?

当你将监听套接字设置为非阻塞fcntl(sock, F_SETFL, O_NONBLOCK)):

  • 即使内核通知了可读事件,当你调用 accept() 时,如果队列里没有连接(由于竞争条件或边缘触发漏处理),accept() 不会阻塞 ,而是立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  • Reactor 的应对: 代码检测到 EAGAIN,不会继续死等,而是直接返回主循环,继续调用 epoll_wait
    • 这样,其他已连接客户端的读写事件就能被正常处理。
    • 服务器虽然暂时没拿到新连接,但功能依然是正常的。

4. 为什么很多文章说"listen 可以不设置非阻塞"?

这是一个需要区分的点:

  • 对于 select/poll****或者 epoll****的 水平触发(LT )模式:即使 accept 返回了 EAGAIN,下次 epoll_wait 还会立刻返回该事件。如果你忘了处理 EAGAIN,可能会造成 busy loop (CPU 100%),但不会 阻塞主线程。因此,很多新手代码在 LT 模式下省略了设置非阻塞,也能跑通。
  • 对于 epoll****的 边缘触发(ET )模式:必须 设置非阻塞。因为 ET 只通知一次,你必须循环调用 accept 直到返回 EAGAIN 为止。如果 accept 是阻塞的,在循环最后一次调用时,队列已空,线程就会永久阻塞在 accept 上,导致灾难。

5. 总结对比表

|----------------|----------------------------------------------------|------------------------------------|
| 特性 | 阻塞 Listen Socket | 非阻塞 Listen Socket |
| 常规连接 | 正常工作 | 正常工作 |
| 高并发/ET模式 | 致命风险:当 accept 队列为空时,线程挂起,导致 Reactor 无法处理任何 I/O | 安全:返回 EAGAIN,事件循环继续运行 |
| 多线程 Accept | 惊群效应下,多个线程可能有一个被阻塞在内核 | 配合 SO_REUSEPORT或互斥锁,表现更稳健 |
| 编程复杂度 | 简单,无需处理 EAGAIN | 需要循环 accept 直到 EAGAIN,且需要正确设置非阻塞标志 |

结论

"收不到消息"的原因是:在 Reactor 模式下,如果监听套接字是阻塞的,一旦触发了罕见的竞争条件(边缘触发漏处理或惊群后的空队列), **accept()**会永久阻塞事件循环线程。事件循环停了,不仅收不到新消息,已有的连接消息也无法处理。

在 Reactor 模式中,"永不阻塞"是铁律 。因此,监听套接字必须设置为非阻塞 ,并配合循环调用 accept() 直到返回 EAGAIN,这样才能保证事件循环的高可用性。

相关推荐
小王不爱笑1322 小时前
JVM 核心面试题全解析
java·开发语言·jvm
啥咕啦呛2 小时前
跟着AI学Java第1天:Java Lambda与Stream试学包
java·开发语言·python
雪碧聊技术2 小时前
如何查看、登录服务器上的redis服务?Redis 运维速查:从连接认证到数据查询的全链路解析
linux·服务器·命令行·缓存数据库
乐思项目管理2 小时前
OpenClaw 在一次服务器入侵应急中的实战复盘
运维·服务器
小周学学学2 小时前
vmware的python自动化:批量克隆虚拟机
运维·服务器·python·自动化·vmware
嵌入式×边缘AI:打怪升级日志3 小时前
TCP 网络编程学习笔记
开发语言·php
minglie13 小时前
lean4环境安装
开发语言·前端
chh5633 小时前
从零开始学习C++ -- 基础知识
开发语言·c++·windows·学习·算法
Lzh编程小栈3 小时前
【数据结构与算法】C语言实现双向链表 (Double Linked List) 全解析
c语言·开发语言·数据结构·链表