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,这样才能保证事件循环的高可用性。

相关推荐
两个人的幸福15 小时前
Windows 桌面应用自研 PHP 队列(下):完整代码与六大工程化优化
php
zzzzzz31019 小时前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
BingoGo3 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack3 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户3074596982074 天前
PHP 扩展——从入门到理解
php
霜落长河4 天前
抛弃TCP改用UDP,HTTP3怎么了?
http
鹏仔先生4 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
大树884 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
LDR0064 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术4 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript