4、Reactor模式:基于epoll的事件驱动模型,手写简易Reactor

好,咱们今天来聊聊Reactor模式。说实话,这个模式在网络编程里太重要了。你想想看,一个服务器要同时处理成千上万个连接,总不能每个连接开一个线程吧?那资源消耗太吓人了。Reactor模式就是解决这个问题的经典方案。

我个人习惯把Reactor理解成一个「事件分发中心」。它就像一个大堂经理,专门负责监听各种事件——谁发数据过来了、谁断开了、谁有新连接了。然后它把这些事件分发给对应的处理函数。嗯,说白了就是「来了活,分给对应的人干」。

为什么是epoll?

在Linux下做高并发,epoll几乎是唯一的选择。select和poll的瓶颈很明显——它们需要遍历所有fd,而且每次都要把fd集合从用户态拷贝到内核态。我曾在项目中遇到过一个问题,连接数到了5000左右,select就开始明显卡顿,CPU飙升得厉害。后来换成epoll,同样的硬件配置,轻松撑到几万连接。

epoll的优势在于:

  • 事件驱动:只返回就绪的事件,不用遍历全部
  • O(1)复杂度:不管多少连接,检查效率基本不变
  • 内存映射:避免了用户态和内核态的数据拷贝

核心要点:epoll_wait返回的是「发生了什么事」,而不是「有哪些fd」。这个区别决定了性能的天壤之别。

Reactor的核心组件

一个简易的Reactor,说白了就三个东西:

  1. 事件循环(Event Loop):不断调用epoll_wait,拿到就绪事件
  2. 事件处理器(Handler):处理具体的事件逻辑,比如读数据、写数据
  3. 事件分发器(Dispatcher):把事件和处理器关联起来

你想想看,这三者配合起来,就是一个完整的Reactor。事件循环负责「等」,分发器负责「找」,处理器负责「干」。

手写简易Reactor

好,咱们直接上代码。我会用C++写一个最简版本,去掉所有无关细节,让你一眼看明白Reactor的本质。

// 事件处理器基类
class EventHandler {
public:
    virtual void handleRead() = 0;
    virtual void handleWrite() = 0;
    virtual int getFd() = 0;
};

// Reactor核心类
class Reactor {
private:
    int epollFd;
    std::map<int, EventHandler*> handlers;
    
public:
    Reactor() {
        epollFd = epoll_create1(0);
        if (epollFd == -1) {
            perror("epoll_create1 failed");
            exit(1);
        }
    }
    
    void registerHandler(EventHandler* handler, uint32_t events) {
        int fd = handler->getFd();
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;
        
        epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &ev);
        handlers[fd] = handler;
    }
    
    void eventLoop() {
        const int MAX_EVENTS = 1024;
        struct epoll_event events[MAX_EVENTS];
        
        while (true) {
            int nfds = epoll_wait(epollFd, events, MAX_EVENTS, -1);
            
            for (int i = 0; i < nfds; i++) {
                int fd = events[i].data.fd;
                EventHandler* handler = handlers[fd];
                
                if (events[i].events & EPOLLIN) {
                    handler->handleRead();
                }
                if (events[i].events & EPOLLOUT) {
                    handler->handleWrite();
                }
            }
        }
    }
};

个人经验:我刚开始写Reactor时,犯过一个低级错误——在handleRead里直接调用了epoll_ctl删除fd。结果导致事件循环里还在用这个fd,直接崩溃。后来我改成「先标记,后删除」的策略,在事件循环外面统一处理。这个坑,你一定要注意。

一个具体的例子:EchoServer

光有框架不行,咱们得跑起来。下面是一个简单的EchoServer,客户端发什么,它就回什么。

class EchoHandler : public EventHandler {
private:
    int clientFd;
    char buffer[1024];
    
public:
    EchoHandler(int fd) : clientFd(fd) {}
    
    int getFd() override { return clientFd; }
    
    void handleRead() override {
        int n = read(clientFd, buffer, sizeof(buffer) - 1);
        if (n <= 0) {
            // 客户端断开或出错
            close(clientFd);
            delete this;
            return;
        }
        buffer[n] = '\0';
        printf("收到: %s", buffer);
        // 直接写回去
        write(clientFd, buffer, n);
    }
    
    void handleWrite() override {
        // 这里可以处理写事件,但EchoServer不需要
    }
};

int main() {
    Reactor reactor;
    
    // 创建监听socket
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);
    // ... 绑定、监听等代码省略 ...
    
    // 注册监听socket的读事件
    reactor.registerHandler(new ListenHandler(listenFd, &reactor), EPOLLIN);
    
    // 启动事件循环
    reactor.eventLoop();
    
    return 0;
}

注意:上面的代码为了简洁,省略了错误处理和内存管理。实际项目中,你还需要考虑:

  • 智能指针管理Handler生命周期
  • 非阻塞socket的设置
  • 边缘触发(ET)和水平触发(LT)的选择
  • 线程安全问题

ET vs LT:一个绕不开的话题

epoll有两种触发模式,这个必须讲清楚。我曾经在面试中被问到过,当时没答好,后来专门研究了一下。

模式 特点 使用场景
水平触发(LT) 只要缓冲区有数据,就会一直通知 简单、不容易漏事件,适合新手
边缘触发(ET) 只有状态变化时才通知一次 高性能,但需要一次性读完所有数据

我个人建议:刚开始用LT就够了。ET虽然性能更好,但处理起来很麻烦——你必须用循环把数据读完,否则就丢了。我见过不少新手在ET模式下漏数据,排查半天才发现是没读完。

Reactor的变体

咱们这个简易Reactor是单线程的。实际生产环境中,Reactor有很多变体:

  • 单Reactor单线程:就是咱们写的这个,适合CPU密集型任务少的场景
  • 单Reactor多线程:事件循环在一个线程,业务处理丢到线程池
  • 多Reactor多线程:每个CPU核心一个Reactor,比如Netty的boss/worker模型

你想想看,如果你的业务逻辑很重,比如要解密、解压缩、写数据库,那单线程肯定扛不住。这时候就得把业务处理丢到线程池里,Reactor只负责IO事件的分发。

一句话总结:Reactor模式的核心思想就是「把IO等待和业务处理解耦」。epoll负责高效地等,Reactor负责聪明地分,Handler负责专心地干。

好了,这一章的内容就到这里。下一章咱们会在这个基础上,加入线程池,实现一个真正能扛高并发的Reactor模型。到时候你会看到,单线程Reactor和线程池Reactor的性能差距有多大。