资讯动态

epoll与reactor:Linux高并发网络编程核心模型解析

发布时间:2026/10/15 4:21:03 来源:尧图企业网站定制
1. 为什么reactor会出现在Linux内核视野里先直接回答标题里的那句话——reactor反应堆本质就是谁有动静谁写回调一个循环盯着所有连接有事才处理没事就歇着。这句话说起来很轻巧但里面藏着一个服务端开发者迟早要面对的痛点。我在接触IO多路复用之前写过的网络服务大概是这样的accept一个连接然后启动一个线程或者fork一个进程专门伺候这个连接读写都在里面阻塞着。连接少的时候没问题三五十个连接线程池一开跑得还挺欢。可一旦连接数往上走比如几千、几万这种“一连接一线程”的模型就开始露出狰狞的一面。首先是线程数量的膨胀。一个线程哪怕什么都不干栈空间也要占据实实在在的内存默认8MB的栈一万个线程就是80GB的地址空间开销虽然实际物理内存不会全部分配但光线程调度、上下文切换的代价就足以让性能暴跌。更严重的是大部分连接大部分时间其实是安静的——用户打开了一个页面正在阅读这个TCP连接上根本没有数据流动。你用一个线程去阻塞read本质上是让一个“服务员”陪着一位顾客发呆而这期间可能有一百个顾客同时在呼喊要服务。于是IO多路复用登场了。select、poll、epoll这三兄弟做的事情用一句话讲就是把“谁有数据”这件事从每个连接各自等变成“用一套机制统一等”。而reactor这个模式正是在这套统一的“事件等待”机制之上把业务逻辑重新组织了一遍——不再是你等我、我等你那样串行依赖而是由核心分发器统一管理事件和回调让所有连接在一个线程里获得公平且高效的服务。在Linux上epoll是这个家族里最成熟、性能最强悍的实现。它不是像select那样每次调用把全部fd集合从用户态拷贝到内核态也不是像poll那样用链表在用户态维护fd状态然后在内核态轮询一遍。epoll真正聪明的地方在于它把“关注哪些fd”这件事注册进内核由内核在fd状态变化时主动通知。这个机制恰恰把reactor模式里“分发器”这个角色变成了内核级的支持——你只需要告诉epoll你关心哪些事然后在一个循环里等它告诉你“谁有事了”act之后把对应的回调函数拿出来跑一遍就行。所以reactor不是某种高不可攀的架构它不过是对“IO多路复用怎么组织业务”的标准答案。在任何一份处理高并发网络IO的代码里你最后都会长成它的形状。2. epoll的三次调用先把地基看透要想亲手搭一个基于epoll的reactor绕不开三个系统调用。我的经验是新人在第一次看epoll相关代码时最喜欢干的事是“背API名字”但背完就忘过两周又回来查。真正有效的方式是把这三个调用背后的场景想明白你在管理一群连接你要做的是告诉内核“这群家伙我盯着呢”然后不断问它“谁有动静了”。2.1 epoll_create建一个事件调查局int epfd epoll_create(1);早年间这个参数还要求传一个大于0的期望大小内核版本4.5之后size参数实际上已经没有用途了随便传个正数就行内核会自动扩展结构。但代码里保留这个参数是为了兼容老版本。这里的返回的epfd你可以理解成事件调查局的“局号”。内核为这个epoll实例维护了一棵红黑树——注意是红黑树不是哈希表用来存放你所有登记过的fd另外还有一个就绪链表存放那些确实发生了事件、可以回来领走的fd。树和链表之间是有联动关系的——常态下fd待在树上等待关注一旦事件触发它就会被挪到链表里等待你用epoll_wait把它们领走。合理类比是这样红黑树像是一个巨大的档案柜里面放着所有你关心的人fd以及你关心的方式注册的事件类型就绪链表像是“有情况专人接待”的快速通道内核把有消息的人直接领进通道里等你下班前打开门看一眼登记一下再把它们领出去处理事。2.2 epoll_ctl关注谁怎么关注你来填表struct epoll_event ev; ev.events EPOLLIN; // 关注可读事件 ev.data.fd client_fd; // 携带的私有数据这里直接塞fd epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, ev);这个调用解决的三个问题对谁第三个参数、做什么第二个参数、按什么规则第一个参数指向的结构体。其中EPOLL_CTL_ADD是新增关注EPOLL_CTL_MOD是修改关注方式比如一开始只关心可读、后来也要关心可写EPOLL_CTL_DEL是取消关注比如连接关闭了。epoll_event里的data字段是一个union最常见两种用法放fd数值或者放一个指向某个结构体的指针。在reactor里这个指针的艺术含量极高——它指向的对象里通常携带着“这个fd发生可读事件时该调用哪个回调函数”这正好是反应堆分发的核心依据。补充一个操作细节EPOLL_CTL_MOD经常被忽略但实际应用里几乎躲不开。比如一个fd需要读数据你没有一次读完还剩一些没处理完但你又不想一直阻塞在read上占用这个线程那么你需要把它修改为只关心EPOLLOUT可写事件等缓冲区可写了再继续发剩下的。没有MOD这个操作你只能在ADD还是DEL之间反复横跳性能损失还在其次代码逻辑很容易写成一团乱麻。2.3 epoll_wait这一觉睡得值不值取决于事件密度struct epoll_event events[128]; int n epoll_wait(epfd, events, 128, -1);在reactor的主循环里这个调用就是那个“无限循环盯着所有连接”的引擎。它在等待什么等待内核把就绪链表里的事件拷贝到你的events数组里。返回的n表示有多少个fd产生了事件。最后一个参数是超时时间单位是毫秒。传-1是永远等下去——内核的有趣机制在于只要就绪链表不为空epoll_wait会立刻返回链表为空时它才会让当前进程进入可中断的睡眠等待内核唤醒。传0是一次性查询非阻塞地看看现在有没有事件通常用来实现特殊的分时逻辑。传正数则是限时等待。这里的等待本质上是把“忙轮询”变成了“事件驱动唤醒”功耗和CPU占用率都大幅下降。这也是reactor模式在Linux上有底气成为高性能服务端标配的根基。trivial但容易被忽略的坑epoll_wait返回的事件集合里可能出现同一个fd同时在EPOLLIN和EPOLLOUT两个mask里都置位的情况。处理文件描述符事件时你要养成“先读后写”的习惯——优先处理EPOLLIN因为读事件意味着对方发了数据或者对端关闭了连接后者对你的业务往往比发送缓冲可写更紧急。如果顺序颠倒可能会在连接已经半关闭的情况下仍然试图写数据然后得到一个意想不到的RST。3. reactor反应堆的核心组件四个零件缺一不可现在开始动手设计一个reactor或者更准确地说是设计“你自己业务里的reactor”。我习惯把reactor的组成拆成四个固定角色不管代码怎么写、语言用什么这四个角色最终都会以某种形态出现区别只在于你是否有意识地让它们各司其职。3.1 事件源所有被关注的fd的统称监听socket、已建立的socket、管道读端、定时器fdtimerfd、信号fdsignalfd凡是你能丢进epoll里的都是事件源。在epoll视角里它们不区分三六九等都是一棵树上的叶子节点。但业务上你要分辨清楚角色差异。监听socket的事件类型是EPOLLIN代表“有新的连接请求到达”处理动作是acceptclient socket的EPOLLIN代表“对方发来数据”处理动作是readclient socket的EPOLLOUT代表“发送缓冲区有空位了”处理动作是write剩余数据。把这些角色映射到上层的结构体设计上你会发现最自然的做法是用一个结构体描述“一个事件源”里面存fd、存事件类型、存回调函数指针可能还要存自己的状态。对这就是一个典型的handler它是reactor四零件里的头号主角。3.2 事件多路分发器linux内核里的epoll在这一篇的语境下事件多路分发器就是epoll实例本身。它负责监控所有事件源。用一个非常粗线条的对照传统模式是“每个连接有一个线程在阻塞read”reactor模式是“一个线程通过epoll在阻塞地等所有连接的事件”。后者节省的不仅仅是线程资源更重要的是它改变了“如何分配CPU时间”的方式——传统模式下一个没数据饿着的read会让线程睡死过去即使同一毫秒里有其他连接的数据到了也没人处理epoll模式下只要内核的等待队列里有任何事件闹出动静线程就被唤醒处理完一批再回到epoll_wait里继续等。3.3 事件循环reactor的引擎也叫event loop。一段非常朴素的while(1)循环套在epoll_wait外面先拿事件再根据事件的类型回调对应的处理器。这段代码是整个服务的心跳它往往还有一个别名主循环。事件循环本身极其简单但它有一个隐形的约定所有注册的回调函数都是在这个循环所在线程里执行的。这既是reactor的优点——单线程内无锁共享数据也是它的限制——如果某个回调函数里出现一个耗时的阻塞操作比如同步读一个大文件、同步查询数据库整个循环都被卡住其他所有连接的事件都得不到处理直至这个回调返回。这就是为什么工程里经常把“处理业务逻辑的两个阶段”拆开IO阶段由reactor线程完成业务计算阶段通过线程池异步完成。前者保障网络层的吞吐后者保障耗时操作不拖垮事件循环。3.4 回调注册与分发让“事件”和“处理”拴成一对事件循环拿到一个就绪事件后做的最核心动作就是通过事件源结构体里的回调函数指针调用对应处理函数。这个动作称为“dispatch”或者“分发”。从内核角度看epoll_wait负责把底层事件捞出来从业务角度看回调函数负责把事件转化为具体行为。中间这层映射就是reactor这个模式里“反应”二字的来源——事件来了不被消费掉而是被反应到预设的处理逻辑上。关键设计决策在于回调函数的签名长得像什么。我在项目里常用的签名是typedef void (*event_handler)(event_loop *loop, int fd, void *user_data, uint32_t events);参数里带着event_loop指针的好处是如果这个回调在处理中需要操作事件循环本身比如读完数据后把fd的监听事件从EPOLLIN改成EPOLLOUT、或者把这个fd从epoll里移除可以直接通过loop指针调用相关接口不需要再传递一群人见人烦的全局句柄。做到这一步你手里的reactor已经具备一个基础的“形状”了。接下来最值得花时间打磨的地方是如何在“看起来全对”的代码里踩出几个能让你反省半天的坑。4. 手拆一个基于epoll的最小reactor骨架空谈架构很容易飘直接上代码骨架更实在。这个骨架我故意保持最小化目标是让你一行行看完后能自己复现跑起来而不是看一个几千行的框架然后劝退。4.1 数据结构设计fd和回调和状态锁在一起typedef struct event_loop event_loop; typedef void (*event_handler)(event_loop *loop, int fd, void *user_data, uint32_t events); // 每一个被关注的事件源对应一个n_reactor_item typedef struct n_reactor_item { int fd; uint32_t events; // 当前关注的事件类型 event_handler read_cb; // 可读回调 event_handler write_cb; // 可写回调 void *user_data; // 业务数据例如一个session对象 } n_reactor_item;这里我把读回调和写回调分开而不是合并成一个handler。好处在于你可以在注册阶段就能清晰地表达意图这个fd关心EPOLLIN时设置read_cb关心EPOLLOUT时设置write_cb。分发阶段根据事件类型选择调用谁逻辑更清爽。user_data是万能插槽。业务层可能会准备一个结构体里面存放协议解析的中间状态、发送队列、超时时间戳等你只需要把这个结构体指针塞进去事件分发时什么都不用做就能拿到完整上下文。事件循环本体typedef struct event_loop { int epfd; int stop; struct epoll_event events[128]; n_reactor_item *items[FD_SETSIZE]; // 简化示例用fd直接做数组下标 } event_loop;items数组用fd做下标换来O(1)的定位当epoll_wait返回一个fd事件你直接从items[fd]里取到对应的事件源结构体。在生产级代码里你可能要换成hash map或者更复杂的索引结构因为fd空间可能很稀疏但在教学骨架里数组大法最简单直观。4.2 事件循环核心逻辑注册、取出、分发void event_loop_add(event_loop *loop, n_reactor_item *item, uint32_t events) { struct epoll_event ev; ev.events events; ev.data.ptr item; // 关键epoll_event的data字段塞事件源指针 epoll_ctl(loop-epfd, EPOLL_CTL_ADD, item-fd, ev); loop-items[item-fd] item; item-events events; } void event_loop_del(event_loop *loop, int fd) { epoll_ctl(loop-epfd, EPOLL_CTL_DEL, fd, NULL); loop-items[fd] NULL; } void event_loop_run(event_loop *loop) { while (!loop-stop) { int n epoll_wait(loop-epfd, loop-events, 128, -1); for (int i 0; i n; i) { n_reactor_item *item (n_reactor_item*)loop-events[i].data.ptr; uint32_t active_events loop-events[i].events; if (active_events (EPOLLIN | EPOLLERR | EPOLLHUP)) { if (item-read_cb) item-read_cb(loop, item-fd, item-user_data, active_events); } if (active_events EPOLLOUT) { if (item-write_cb) item-write_cb(loop, item-fd, item-user_data, active_events); } } } }仔细看epoll_wait里从内核捞出来的events[i].data.ptr它直接就是add时塞进去的事件源指针这意味着epoll_wait帮你完成了“从fd事件到业务结构体”的映射不需要再多做一次查找这是一次性到位的高效分发。EPOLLERR错误和EPOLLHUP挂断在多数内核版本里是即使你不注册也会被返回的事件。把它们和EPOLLIN放在一起判断是有讲究的连接异常断开往往伴随着可读事件一起出现如果你只处理了EPOLLIN里的read正常路径忽略了错误分支可能把“对端崩溃”误当成“对方发来数据”read一次后返回0然后你以为连接正常关闭但实际上很多中间状态如TCP的RST在那个场景下需要额外的清理逻辑。4.3 监听socket的接入accept也是一种读事件监听socket在epoll里注册的也是EPOLLIN。当新连接到来时主循环会通过上面那个分发逻辑走到read_cb但这里的read不是recv数据而是accept新连接。void on_accept(event_loop *loop, int listen_fd, void *user_data, uint32_t events) { struct sockaddr_in addr; socklen_t len sizeof(addr); int client_fd accept(listen_fd, (struct sockaddr*)addr, len); if (client_fd 0) { set_nonblocking(client_fd); n_reactor_item *item create_client_item(client_fd); // 业务层构造事件源结构体 event_loop_add(loop, item, EPOLLIN); // 默认先关心读 } }这里有一个我在真实项目里踩过的坑如果不把client_fd设成非阻塞那么read回调里如果对方数据还没到阻塞在read上整个事件循环就死了。所以你在这个环节务必要用fcntl打上O_NONBLOCK标记。之后在read_cb里read返回-1且errno为EAGAIN时不视为错误而是“这次没数据可读”正常返回等下一轮epoll_wait的通知即可。4.4 read回调里的关键动作读完、改关注、决定是否移除void on_read(event_loop *loop, int fd, void *user_data, uint32_t events) { char buf[4096]; n_reactor_item *item lookup_item(loop, fd); ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { // 业务层处理数据比如解析协议头、或者把数据追加到发送缓冲 handle_incoming_data(item, buf, n); } else if (n 0) { // 对端关闭清理 cleanup_connection(loop, fd); return; } else { if (errno EAGAIN || errno EWOULDBLOCK) return; // 非阻塞下正常情况 // 其他错误处理并清理 cleanup_connection(loop, fd); return; } }close一个从内核角度看没有任何事件待处理的fd通常不会立刻出问题但如果在同一个事件轮回里你close的fd随后又被accept分配给了新的连接然后旧的事件指针还残留在处理列表里就会形成一个“幽灵事件源”。所以cleanup_connection里必须做两件事先从epoll里DEL掉这个fd再把items数组里的槽位清空最后才close(fd)。顺序不能反。这个骨架跑起来你已经拥有一个可处理成千上万个连接的单线程reactor了。从吞吐量的角度它的天花板取决于单个事件的处理耗时以及每轮epoll_wait返回的事件数量。剩下的事情基本都是在这个骨架上做精细调节。5. 水平触发与边沿触发真正决定性能上限的分岔口epoll支持两种触发模式水平触发Level-TriggeredLT和边沿触发Edge-TriggeredET。这是reactor实现里绕不开的抉择也是面试官最爱追问的细节。但我发现很多新人对这两种模式的理解停留在“背定义”没有真正想清楚它们对代码结构的影响。这里帮大家梳理一下。5.1 LT水平触发和 ET边沿触发的本质差异水平触发是epoll的默认模式语义是只要fd上有未读的数据每次epoll_wait都会通知你。这意味着你哪怕一次只读1个字节数据剩下100个字节下一轮epoll_wait还是会把EPOLLIN重新报给你。边沿触发则是只有当fd的状态发生跳变时从无数据变成有数据才通知一次之后即使还有数据没读完也不会再通知直到你再次读到EAGAIN。注意ET模式的通知次数和“有多少数据到达”有关而和“还剩多少数据”无关。边界条件在这里非常关键。以read为例如果某个时刻一个连接同时有多个TCP分节到达内核把数据合并成一次跳变通知你。ET模式下你只收到一次事件。如果你在这次事件回调里只read了一次就返回剩下的数据可能永远等不来下一次通知——因为你没有读到EAGAIN内核认为你“没消费完但也没继续读”事件不会再报了。这就是ET的核心约束必须一直循环读直到EAGAIN。5.2 为什么ET省系统调用但工程上难度陡增ET模式的收益很直接相同的数据量你触发的epoll_wait通知次数可能大幅减少。比如一个连接持续不断发数据LT下每轮epoll_wait都会把这个fd报一次即使你的read已经把它读完处理干净了因为对端又有新数据进来状态保持“可读”epoll_wait又会报而ET下内核只关心状态的上升沿每来一波连续数据只报一次你在这个回调里能读多少读多少这样单帧通知的含金量高很多。在高吞吐场景里减少epoll_wait返回的次数意味着减少用户态和内核态切换次数、减少对events数组的遍历成本整体性能确有提升。但代价是代码逻辑要变谨慎。LT模式下你可以在回调里只做“尽量读”然后交给下一轮ET模式下你必须在回调里把能读的都读完直到read返回EAGAIN。写ET的read回调惯用结构是void on_read_et(event_loop *loop, int fd, void *user_data, uint32_t events) { char buf[4096]; n_reactor_item *item lookup_item(loop, fd); for (;;) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { handle_incoming_data(item, buf, n); } else if (n 0) { cleanup_connection(loop, fd); return; } else { if (errno EAGAIN || errno EWOULDBLOCK) { // 读到没数据可读了才算圆满完成 return; } // 真错误 cleanup_connection(loop, fd); return; } } }这里有个容易被人忽略的细节如果没有先用fcntl把fd设置为非阻塞那么在for循环的read里当内核缓冲区已经没数据但你还在读时线程会被阻塞住整个事件循环直接卡死。用ET模式非阻塞是硬前提一个字节都不能妥协。5.3 我的取舍经验默认LT除非你清楚自己的瓶颈在绝大多数业务里LT模式足够优秀性能完全能满足几万连接的需求而且代码心智负担小。选ET的普遍理由其实不是“性能必须翻倍”而是“想尽量减少无效唤醒和系统调用次数”但它引入的编程约束会让代码的长度和出错概率同步上升。我的建议很直白如果你在做一个对改动风险敏感的存量系统LT就是稳定的答案如果你是做一个追求极限吞吐的新服务并且在协议层设计上能容忍“一个事件回调里要把活干完”那上ET但必须配一套完善的测试来覆盖“半包”“粘包”“超大数据包”这些边界场景。真正决定吞吐的往往不是LT还是ET而是每轮epoll_wait里有多少事件是“有效的”。如果连接的数据到达非常稀疏ET和LT差距几乎可以忽略如果连接经常有连续大量数据涌入ET的收益才体现得出来。所以选型前先量化自己的业务模式而不是跟着别人的框架走。5.4 关于EPOLLONESHOT的补充另一个容易搞混的机制是EPOLLONESHOT。它的语义是事件的某个fd被通知过一次后如果没有重新注册就不会再被通知。这通常用在多线程reactor模型里——一个fd的事件被某个工作线程处理后不让其他线程也同时拿到这个fd的事件避免并发处理同一个连接造成数据竞争。Linux下的epoll本身不支持跨线程对同一fd事件的串行仲裁EPOLLONESHOT配合EPOLL_CTL_MOD重新注册能实现“当前线程处理完再把关注权放回池子”的效果。但单线程reactor用不到它。没有并发消费就不需要重放机制。新手如果从开源框架里看到这个东西先要搞清楚对方是多线程模型还是单线程模型再决定要不要在自己的代码里照搬。6. 一个吃透reactor的实践场景改造现有服务理论堆得再高动手过一遍才真正内化。我在这里提供一个很适合自己练手的改造实验用到的都是最常见的网络编程场景。6.1 从阻塞模型迁移到epoll reactor模型假设你手里有一个老式的echo服务代码特征是这样的accept后创建线程recv、send阻塞在线程里连接结束关线程。连接数超过几百就开始拉胯。改造第一步梳理现有逻辑会接触哪些“事件”监听socket上的新连接请求事件accept已连接socket上对端发来数据的事件recv已连接socket上发送缓冲可写的事件send缓冲的滞塞解除连接关闭、错误异常事件cleanup然后把这些事件映射到reactor里的“回调”上。accept的回调你已经看到了关键在read和write回调的配合。Echo服务不需要复杂的协议解析read回调里读到数据后最简单的做法是直接把它写回发送缓冲然后立刻把这个fd从关注EPOLLIN改成关注EPOLLOUT——因为写回数据这件事需要在“可写”时才去执行。下一次事件循环发现这个fd可写了调用write回调把数据写完后再把关注事件改回EPOLLIN。read事件回调recv到数据 → 追加到发送队列 → MOD关心EPOLLOUT write事件回调把发送队列里的数据写出去 → 如果队列空了 → MOD关心EPOLLIN这种“读-写-读-写”的事件关注切换熟悉之后你对reactor的理解会非常通透。它就是让fd的关注方式随着程序的意图动态变化循环往复。6.2 改造过程中的典型错误清单我按踩到的概率排序列几个坑第一忘记给client fd设置非阻塞。这是新手改造时最常犯的错而且现象特别迷惑程序明明没崩溃但处理的连接数稍微一多整个服务就像僵住一样。原因是某个read在还有数据时阻塞住了事件循环不转了。第二read和write回调里出现了耗时的同步操作。比如把收到的数据写到磁盘、做复杂的JSON解析。事件循环被拖住其他连接全部饿死表现是服务间歇性抖动。正确做法是把耗时逻辑异步化放到线程池里做。第三回调里缺失对EAGAIN的处理。LT下其实不太容易踩因为LT总是重新通知但如果你想起“优化一下”而切到ET之前没写EAGAIN分支的代码就会在特定时序下读不全数据。我见过心跳包正常、但大数据包偶尔失效的诡异问题最后定位就是这里。第四清理连接时对epoll和数组的移除顺序搞反。先close(fd)再DEL在fd数量少的时候问题不大但在fd复用频繁的高并发场景可能造成刚被accept分配的fd意外被移出监听列表。这个改造实验做完你手里就不只是“看懂”reactor而是拥有了“把一个老项目改成reactor”的第一手经验。过程中踩过的每一个坑都对应一个被老手们反复强调的规定。6.3 进一步扩展多线程reactor的两种走向单线程reactor的扩展天花板很明显一个线程既负责accept又负责全部连接的IO和回调即使LT模式和高效处理CPU的利用上限就是单核。两个常见演进方向一种是主reactor线程只accept然后把client fd转交给多个子reactor线程每个子reactor线程跑自己的事件循环另一种是保持单一事件循环但把耗时业务回调丢到线程池执行。前者的瓶颈在全是在“移交fd”时如何避免惊群和锁竞争后者则要小心业务回调里对共享状态的并发访问。我自己的经验是大多数服务连单线程reactor的极限都没碰到先把单线程做到极致观测到CPU确实打满再决定动不动多线程。过早引入多线程各种锁和同步问题会盖过你原本要优化的业务问题得不偿失。7. 回看“一句话总结”里的每个词回到最开始的那句话谁有动静谁写回调一个循环盯着所有连接有事才处理没事就歇着。现在可以逐词对一遍了。“谁有动静”——这是epoll在做的判断。内核帮你跟踪所有fd的状态变化你不需要自己轮询每个连接。“谁写回调”——每个事件源结构体里挂着对应的read_cb、write_cb这是reactor模式里业务逻辑的挂载点。“一个循环盯着所有连接”——事件循环epoll_wait这个阻塞式调用本身赋予了“等”的能力线程不会空转烧CPU。“有事才处理”——epoll_wait返回了才执行回调无事时线程挂在内核等待队列里几乎零消耗。“没事就歇着”——等同于最后那个-1超时参数带来的效果。其实很多术语本质上就是一句大白话换了一身术语西装。reactor反应堆字面上的“反应”指的就是“事件到来时被动的触发动作”就像核反应堆里的链式反应你丢进去一个中子它会持续触发一系列反应。在网络服务语境里一个TCP分节的到达触发accept或者read回调read回调里可能又触发write回调的关注切换层层推进。逻辑上它就是“IO多路复用回调函数”的标准组织形态换一层皮而已。对我个人来说理解这层关系之后再看任何基于事件循环的网络框架——无论叫Netty、叫Redis的ae事件循环、还是叫libevent——都会有一种“天下武功出少林”的通透感。它们底层各自有各自的内存管理、缓存策略和跨平台抽象但核心骨架无一例外都长成reactor的样子。如果你正在学这部分我的建议很明确别看太多剖析文章也不要急着引入框架。开一个终端用C语言或者你熟悉的其他语言从零搭一个最小可用的epoll reactor然后在这个基础上加业务、加协议、压测、观察瓶颈。这个过程中遇到的所有疑问都会比任何教程讲得更清楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑