基于C语言与Epoll的Reactor模型

一、引言

在网络编程的世界里,如何高效地处理成千上万个并发连接始终是一个核心挑战。传统的"一个连接一个线程"的模型在连接数较少时简单有效,但当并发量上来后,线程创建和上下文切换的开销会成为系统的瓶颈。

Reactor 模型,作为一种事件驱动(Event-Driven)的设计模式,正是为了解决这个问题而生。它被广泛应用于 Nginx、Redis、Netty 等高性能网络框架中。今天,我们就通过一段 C 语言代码,来剖析 Reactor 模型的内部构造和工作原理。

二、什么是 Reactor 模型?

Reactor,中文意为"反应堆"。它不是一种具体的 API,而是一种设计思想。其核心在于将 I/O 事件的处理流程"反转"过来

  • 传统模式 :应用程序主动调用 API(如 recv)去读取数据,如果没有数据,线程就会阻塞。
  • Reactor 模式:应用程序向 Reactor 注册感兴趣的事件(如"某 Socket 可读")和对应的处理函数(回调函数)。Reactor 负责监听这些事件,一旦事件就绪(如数据到达),Reactor 就会主动调用注册好的回调函数来处理。

简单来说,Reactor 就是一个事件循环和分发器,它把 I/O 多路复用(如 epoll)和业务逻辑解耦,让程序由事件驱动。

三、Reactor 模型的三大核心组件

从宏观上看,一个 Reactor 模型由三个关键角色构成:

  1. 多路复用器 (Multiplexer) :由操作系统提供,如 Linux 下的 epoll。它负责监听大量的文件描述符(fd),并告知哪些 fd 已经就绪(可读/可写)。
  2. 事件分发器 (Dispatcher):这是 Reactor 的核心循环。它从多路复用器那里获取就绪的事件列表,然后遍历这些事件,找到它们对应的处理函数。
  3. 事件处理器 (Handler) :也就是我们常说的回调函数(Callback)。它负责执行具体的业务逻辑,比如 accept 新连接、recv 读取数据、send 发送响应。

四、用代码构建一个 Reactor

理解了概念,我们来看看如何用代码来实现它。我们的目标是把 epoll 这个底层机制封装成一个更易用的 Reactor 框架。

第一步:定义基本单元------事件 (ntyevent)

一个网络连接就是一个文件描述符(fd)。在 Reactor 中,我们不会直接操作 fd,而是将其封装成一个"事件"对象。这个对象不仅包含 fd,还包含了处理该 fd 上事件所需的所有信息。

复制代码
#define BUFFER_SIZE 4096

// 回调函数类型定义
typedef int NCALLBACK(int, int, void*);

struct ntyevent {
    int fd;                             // 文件描述符
    int events;                         // 当前监听的事件类型 (EPOLLIN/EPOLLOUT)
    void* arg;                          // 传递给回调函数的参数,通常是 reactor 本身
    int (*callback)(int fd, int events, void* arg); // 核心:事件处理回调函数

    int status;                         // 事件状态 (0:未添加, 1:已添加)
    char buffer[BUFFER_SIZE];           // 读写缓冲区
    int length;                         // 缓冲区中数据的长度
};

这个 ntyevent 结构体是 Reactor 管理的最小单位。它将 fd、事件类型、回调函数和缓冲区捆绑在一起,使得事件的处理变得非常清晰。

第二步:定义管理者------反应堆 (ntyreactor)

有了事件,就需要一个管理者来统一管理所有的事件。这个管理者就是我们的 Reactor。

复制代码
#define MAX_EPOLL_EVENTS 1024

struct ntyreactor {
    int epfd;                           // epoll 实例的文件描述符
    struct ntyevent* events;            // 事件数组,用于存储和管理所有 ntyevent
};

ntyreactor 结构体非常简单,它主要包含两个成员:

  • epfd:这是调用 epoll_create 创建出来的,是与操作系统内核交互的句柄。
  • events:这是一个 ntyevent 类型的数组(或链表),Reactor 通过它来索引和管理所有的连接事件。

第三步:封装核心操作

接下来,我们需要封装一系列函数来操作这两个核心结构体。

  • 初始化 ( ntyreactor_init) :创建 epoll 实例,并为 events 数组分配内存。
  • 事件设置 ( nty_event_set) :为一个 ntyevent 对象填充 fd、回调函数等初始信息。
  • 事件添加/修改 ( nty_event_add) :将 ntyevent 注册到 epoll 中。这里会用到 epoll_ctl,并根据 status 字段判断是 EPOLL_CTL_ADD 还是 EPOLL_CTL_MOD
  • 事件删除 ( nty_event_del) :从 epoll 中移除一个 ntyevent

这些封装函数将复杂的 epoll 系统调用隐藏起来,提供了更高级、更易用的接口。

第四步:实现事件处理器(回调函数)

这是业务逻辑真正发生的地方。我们主要需要三个回调函数。

  1. accept_cb** (接受新连接)**
    当监听套接字(listen fd)上有 EPOLLIN 事件时,意味着有新的客户端连接请求。accept_cb 会调用 accept 获取新的客户端 fd,然后为这个新 fd 创建一个新的 ntyevent,设置其回调函数为 recv_cb,并将其添加到 epoll 中进行监听。
  2. recv_cb** (接收数据)**
    当某个客户端 fd 上有 EPOLLIN 事件时,recv_cb 被触发。它负责从 fd 中读取数据到 ntyevent 的缓冲区。读取成功后,它会将该事件的回调函数修改为 send_cb,并将监听的事件改为 EPOLLOUT,告诉 Reactor:"数据收到了,准备好发送了,下次可写时叫我"。
  3. send_cb** (发送数据)**
    当客户端 fd 变为可写(EPOLLOUT)时,send_cb 被触发。它将缓冲区中的数据发送出去。发送完成后,它又将回调函数改回 recv_cb,监听 EPOLLIN 事件,等待客户端的下一次请求。

这种在 recvsend 之间切换回调和监听事件的方式,完美地实现了非阻塞 I/O 下的请求-响应循环。

第五步:启动事件循环 ( ntyreactor_run)

最后,我们来到 Reactor 的心脏------事件循环。

复制代码
int ntyreactor_run(struct ntyreactor* reactor) {
    // ... 省略检查 ...
    struct epoll_event events[MAX_EPOLL_EVENTS + 1];

    while (1) {
        // 1. 阻塞等待事件发生
        int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, -1);
        if (nready < 0) continue;

        // 2. 遍历所有就绪的事件
        for (int i = 0; i < nready; i++) {
            // 从内核返回的事件中取出我们封装的 ntyevent 对象
            struct ntyevent* event = (struct ntyevent*)events[i].data.ptr;

            // 3. 事件分发:根据事件类型调用对应的回调函数
            if ((events[i].events & EPOLLIN) && (event->events & EPOLLIN)) {
                event->callback(event->fd, events[i].events, event->arg);
            }
            if ((events[i].events & EPOLLOUT) && (event->events & EPOLLOUT)) {
                event->callback(event->fd, events[i].events, event->arg);
            }
        }
    }
    return 0;
}

这个 while(1) 循环就是 Reactor 的生命线。它不断地调用 epoll_wait 等待事件,一旦有事件就绪,就取出对应的 ntyevent 对象,并执行其 callback 函数。这个过程就是"事件驱动"的精髓。

五、Reactor 模型的优点

通过上面的代码分析,我们可以看到 Reactor 模型的几大优势:

  • 高并发 :使用单线程(或少量线程)配合 epoll 就能处理海量连接,避免了多线程模型中线程创建和切换的巨大开销。
  • 响应快:事件一旦就绪,立刻被处理,没有不必要的阻塞和等待。
  • 编程模型清晰:通过回调函数将复杂的并发逻辑分解为一个个独立的事件处理器,代码结构清晰,易于维护。
  • 高复用性:Reactor 框架本身与具体的业务逻辑无关,可以被复用在各种网络服务中。

六、完整代码实现

复制代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <asm-generic/socket.h>

#define SERVER_PORT         8080
#define BUFFER_SIZE         4096
#define MAX_EPOLL_EVENTS    1024

typedef int NCALLBACK(int, int, void*);

struct ntyevent {
    int fd;
    int events;
    void* arg;
    int (*callback)(int fd, int events, void* arg);

    int status;
    char buffer[BUFFER_SIZE];
    int length;
};

struct ntyreactor {
    int epfd;
    struct ntyevent* events;
};

int recv_cb(int fd, int events, void* arg);
int send_cb(int fd, int events, void* arg);

int init_sock(short port) {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        return -1;
    }
    fcntl(listen_fd, F_SETFL, O_NONBLOCK);

    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(port);
    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("绑定失败");
        return -1;
    }

    if (listen(listen_fd, 5) == -1) {
        perror("监听失败");
        return -1;
    }

    return listen_fd;
}

int ntyreactor_init(struct ntyreactor* reactor) {
    if (reactor == NULL) return -1;
    memset(reactor, 0, sizeof(reactor));

    reactor->epfd = epoll_create(1);
    if (reactor->epfd <= 0) {
        printf("epoll创建失败: %s\n", __func__);
        return -1;
    }
    reactor->events = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
    if (reactor->events == NULL) {
        printf("malloc 失败: %s\n", __func__);
        return -1;
    }

    return 0;
}

void nty_event_set(struct ntyevent* ev, int fd, NCALLBACK callback, void* arg) {
    ev->fd = fd;
    ev->callback = callback;
    ev->status = 0;
    ev->arg = arg;
}

void nty_event_add(int epfd, int events, struct ntyevent* ev) {
    struct epoll_event ep_ev;
    ep_ev.data.ptr = ev;
    ep_ev.events = events;
    ev->events = events;

    int opt = 0;
    if (ev->status == 1) {
        opt = EPOLL_CTL_MOD;
    }else {
        opt = EPOLL_CTL_ADD;
        ev->status = 1;
    }


    if (epoll_ctl(epfd, opt, ev->fd, &ep_ev) == -1) {
        perror("epoll_ctl失败");
        printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		return;
    }

}

void nty_event_del(int epfd, struct ntyevent* ev) {
    if (!ev || ev->status != 1) return;

    struct epoll_event ep_ev;
    ep_ev.data.ptr = ev;
    ev->status = 0;

    // printf("nty_event_del ev->fd: %d\n", ev->fd);

    if (epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, NULL) == -1) {
        printf("nty_event_del epoll_ctl 失败\n");
    }
    
}

int recv_cb(int fd, int events, void* arg) {
    struct ntyreactor* reactor = (struct ntyreactor*)arg;
    struct ntyevent* event = reactor->events + fd;

    int len = recv(fd, event->buffer, BUFFER_SIZE - 1, 0);

    // printf("fd: %d, event->fd: %d\n", fd, event->fd);

    nty_event_del(reactor->epfd, event);

    if (len > 0) {
        event->length = len;
        event->buffer[len] = '\0';

        printf("Recv[fd = %d]: %s\n", fd, event->buffer);

        nty_event_set(event, fd, send_cb, reactor);
        nty_event_add(reactor->epfd, EPOLLOUT, event);
    }else if (len == 0) {
        nty_event_del(reactor->epfd, event);
        close(fd);
		printf("[fd=%d], closed\n", event->fd);
    }else {
        nty_event_del(reactor->epfd, event);
        close(fd);
        printf("error[%d]\n", event->fd);
    }

    return len;
}

int send_cb(int fd, int events, void* arg) {
    struct ntyreactor* reactor = (struct ntyreactor*)arg;
    struct ntyevent* event = reactor->events + fd;

    int len = send(fd, event->buffer, event->length, 0);

    if (len > 0) {
		printf("send[fd = %d]: %s\n", fd, event->buffer);

        nty_event_del(reactor->epfd, event);
        nty_event_set(event, fd, recv_cb, reactor);
        nty_event_add(reactor->epfd, EPOLLIN, event);
    }else {
        nty_event_del(reactor->epfd, event);
        close(event->fd);
        printf("send error\n");
    }

    return len;
}

int accept_cb(int fd, int events, void* arg) {
    struct ntyreactor* reactor = (struct ntyreactor*)arg;
    if (reactor == NULL) return -1;

    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);

    int client_fd = accept(fd, (struct sockaddr*)&addr, &len);

    if (client_fd == -1) {
		if (errno != EAGAIN && errno != EINTR) {
			printf("accept: %s\n", strerror(errno));
		    return -1;
		}
    }

    // 这里只是检查事件有没有满,而不是根据找出来的结果,把新的event放到该位置
    // 每个fd唯一标识一个event,不会存在一个fd的event去覆盖另一个fd的event
    int idx;
    for (idx = 3; idx < MAX_EPOLL_EVENTS; idx++) {
        if (reactor->events[idx].status == 0) {
            break;
        }
    }

    if (idx == MAX_EPOLL_EVENTS) {
        close(client_fd);
        printf("%s: max connect limit[%d]\n", __func__, MAX_EPOLL_EVENTS);
        return -1;
    }

    if (fcntl(client_fd, F_SETFL, O_NONBLOCK) < 0) {
        close(client_fd);
        printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
        return -1;
    }

    // printf("idx: %d, client_fd: %d\n", idx, client_fd);

    nty_event_set(&reactor->events[client_fd], client_fd, recv_cb, reactor);
    nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[client_fd]);

    printf("新连接到来: fd[%d]\n", client_fd);

    return 0;
}

int ntyreactor_addlistener(struct ntyreactor* reactor, int listen_fd, NCALLBACK* acceptor) {
    if (reactor == NULL) return -1;
    if (reactor->events == NULL) return -1;

    nty_event_set(&reactor->events[listen_fd], listen_fd, acceptor, reactor);
    nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[listen_fd]);

    return 0;
}

int ntyreactor_run(struct ntyreactor* reactor) {
    if (reactor == NULL) return -1;
    if (reactor->events == NULL) return -1;
    if (reactor->epfd < 0) return -1;

    struct epoll_event events[MAX_EPOLL_EVENTS + 1];

    while (1) {
        int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, -1);

        if (nready < 0) {
			printf("epoll_wait error, exit\n");
			continue;
		}

        for (int i = 0; i < nready; i++) {
            struct ntyevent* event = (struct ntyevent*)events[i].data.ptr;

            if ((events[i].events & EPOLLIN) && (event->events & EPOLLIN)) {
                event->callback(event->fd, events[i].events, event->arg);
            }
            if ((events[i].events & EPOLLOUT) && (event->events & EPOLLOUT)) {
                event->callback(event->fd, events[i].events, event->arg);
            }
        }
    }

    return 0;
}

int ntyreactor_destroy(struct ntyreactor* reactor) {
    close(reactor->epfd);
    free(reactor->events);
    free(reactor);
    return 0;
}

int main(int argc, char* argv[]) {
    unsigned short port = SERVER_PORT;
    if (argc == 2) {
        port = atoi(argv[1]);
    }

    int sockfd = init_sock(port);
    if (sockfd == -1) {
        perror("init_sock失败");
        return -1;
    }

    struct ntyreactor* reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
    ntyreactor_init(reactor);

    ntyreactor_addlistener(reactor, sockfd, accept_cb);
    ntyreactor_run(reactor);

    ntyreactor_destroy(reactor);
    close(sockfd);

    return 0;
}

七、关键问题

为什么reactor模型要使用非阻塞IO?

  1. 多线程环境下,可能会出现一个listen_fd被放到多个epoll实例中去处理的情况。如果使用阻塞IO,当一个新的连接到来时,操作系统会唤醒所有正在等待这个listen_fd的线程,这个就是惊群现象。所有的线程都去执行accept,但是最终只有一个线程accept成功。其他的线程就会因为没有连接可处理而导致永久阻塞。它们的状态就会变成不可中断睡眠,不在监听新的事件,新的连接到来时,内核也无法唤醒它们。这会导致线程池中的线程逐渐耗尽,最终导致服务假死。所以必须使用非阻塞IO,让竞争失败的线程立即返回。
  2. Reactor模式为了追求高性能,会配合epoll的边缘触发模式使用。边缘触发模式的特点就是,只有在状态变化的时候通知一次,所以我们在收到通知后,必须通过循环一次性将缓冲区中的数据读完,否则可能会导致数据丢失。循环中,如果读完了数据,再次调用read就会导致线程阻塞,直到有新数据到来,这会卡死整个事件循环。
  3. reactor模型可能使用的是select,但是select存在虚假唤醒的问题。它告诉我们数据可读,但实际上去读的时候,并没有数据。如果使用的是阻塞IO,read就会一直阻塞,直到数据到来。

八、总结

Reactor 模型通过对 epoll 等 I/O 多路复用技术的封装,构建了一个高效的事件驱动框架。它将连接(fd)抽象为事件(ntyevent),用一个中心化的反应堆(ntyreactor)来管理和分发事件,通过回调函数来处理具体的业务。

理解 Reactor 模型,不仅是掌握一种高性能服务器的编程技巧,更是理解现代网络框架(如 Netty、Redis)底层原理的关键一步。希望这篇结合代码的分析,能帮助你更好地掌握这一重要的设计模式。

相关推荐
12.=0.4 小时前
【stm32_6.1】串行异步接口USART,串口的原理和应用
c语言·stm32·单片机·嵌入式硬件
啧不应该啊5 小时前
Day1 C与python输入输出语句区别
c语言·开发语言
wuminyu5 小时前
专家视角看Java多态性的底层基石vtable(虚函数表)构建过程解析
java·linux·c语言·jvm·c++
爱编码的小八嘎6 小时前
C语言完美演绎9-10
c语言
孬甭_7 小时前
动态内存管理
c语言
qeen877 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
凉、介7 小时前
C 语言类型强转引发的隐蔽内存破坏问题分析
c语言·开发语言·笔记·学习·嵌入式
mount_myj16 小时前
长长久久【C语言】
c语言
Legendary_00819 小时前
LDR6500:USB‑C DRP PD协议芯片技术详解与应用实践
c语言·开发语言