一、引言
在网络编程的世界里,如何高效地处理成千上万个并发连接始终是一个核心挑战。传统的"一个连接一个线程"的模型在连接数较少时简单有效,但当并发量上来后,线程创建和上下文切换的开销会成为系统的瓶颈。
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 模型由三个关键角色构成:
- 多路复用器 (Multiplexer) :由操作系统提供,如 Linux 下的
epoll。它负责监听大量的文件描述符(fd),并告知哪些 fd 已经就绪(可读/可写)。 - 事件分发器 (Dispatcher):这是 Reactor 的核心循环。它从多路复用器那里获取就绪的事件列表,然后遍历这些事件,找到它们对应的处理函数。
- 事件处理器 (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 系统调用隐藏起来,提供了更高级、更易用的接口。
第四步:实现事件处理器(回调函数)
这是业务逻辑真正发生的地方。我们主要需要三个回调函数。
accept_cb** (接受新连接)**
当监听套接字(listen fd)上有EPOLLIN事件时,意味着有新的客户端连接请求。accept_cb会调用accept获取新的客户端 fd,然后为这个新 fd 创建一个新的ntyevent,设置其回调函数为recv_cb,并将其添加到epoll中进行监听。recv_cb** (接收数据)**
当某个客户端 fd 上有EPOLLIN事件时,recv_cb被触发。它负责从 fd 中读取数据到ntyevent的缓冲区。读取成功后,它会将该事件的回调函数修改为send_cb,并将监听的事件改为EPOLLOUT,告诉 Reactor:"数据收到了,准备好发送了,下次可写时叫我"。send_cb** (发送数据)**
当客户端 fd 变为可写(EPOLLOUT)时,send_cb被触发。它将缓冲区中的数据发送出去。发送完成后,它又将回调函数改回recv_cb,监听EPOLLIN事件,等待客户端的下一次请求。
这种在 recv 和 send 之间切换回调和监听事件的方式,完美地实现了非阻塞 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?
- 多线程环境下,可能会出现一个listen_fd被放到多个epoll实例中去处理的情况。如果使用阻塞IO,当一个新的连接到来时,操作系统会唤醒所有正在等待这个listen_fd的线程,这个就是惊群现象。所有的线程都去执行accept,但是最终只有一个线程accept成功。其他的线程就会因为没有连接可处理而导致永久阻塞。它们的状态就会变成不可中断睡眠,不在监听新的事件,新的连接到来时,内核也无法唤醒它们。这会导致线程池中的线程逐渐耗尽,最终导致服务假死。所以必须使用非阻塞IO,让竞争失败的线程立即返回。
- Reactor模式为了追求高性能,会配合epoll的边缘触发模式使用。边缘触发模式的特点就是,只有在状态变化的时候通知一次,所以我们在收到通知后,必须通过循环一次性将缓冲区中的数据读完,否则可能会导致数据丢失。循环中,如果读完了数据,再次调用read就会导致线程阻塞,直到有新数据到来,这会卡死整个事件循环。
- reactor模型可能使用的是select,但是select存在虚假唤醒的问题。它告诉我们数据可读,但实际上去读的时候,并没有数据。如果使用的是阻塞IO,read就会一直阻塞,直到数据到来。
八、总结
Reactor 模型通过对 epoll 等 I/O 多路复用技术的封装,构建了一个高效的事件驱动框架。它将连接(fd)抽象为事件(ntyevent),用一个中心化的反应堆(ntyreactor)来管理和分发事件,通过回调函数来处理具体的业务。
理解 Reactor 模型,不仅是掌握一种高性能服务器的编程技巧,更是理解现代网络框架(如 Netty、Redis)底层原理的关键一步。希望这篇结合代码的分析,能帮助你更好地掌握这一重要的设计模式。