Reactor事件驱动的设计模式

目录

1.reactor核心思想

1.1reactor主要分为两点:

1.2事件与处理逻辑解耦:

1.3状态管理更清晰

1.4代码判断细节

2..整个reactor工作流程:

1. 初始化:注册监听,绑定行为

2. 事件循环:统一分发,按需响应

3. 动态调整:状态驱动,灵活切换


1.reactor核心思想

  • 使用一个单线程或少量线程来监听多个输入源(如网络连接)。
  • 当某个输入源就绪(例如有数据可读),系统会通知对应的处理器进行处理。
  • 避免为每个连接创建一个线程,从而节省资源并提高效率。

例子:

从例子可以知道不同的io可以对应相同的event,所以我们可以从之前对io的管理转换为对事件的管理。

1.1reactor主要分为两点:

1.event与callback的匹配。

2.每个io与之对应的参数。

优势:不同事件做不同处理、I/O 解耦、事件与业务分离

1.2事件与处理逻辑解耦:

在epoll 用法中,事件处理逻辑(比如 accept 建立新连接、recv 接收数据、send 发送数据)与 epoll 核心事件循环紧密耦合在同一个 main 函数里 —— 当 epoll 检测到 EPOLLIN 事件时,必须在循环内部通过硬编码判断:触发事件的是监听套接字(此时执行 accept),还是已连接的客户端套接字(此时执行 recv),所有逻辑混杂在一起,可读性和可维护性极差。

而 reactor 模式通过一个 struct conn 结构体,对每个连接的关键信息进行统一封装:既包含连接对应的文件描述符(fd)、数据读写所需的缓冲区,也包含与该连接绑定的回调函数(如 recv_callback 接收回调、send_callback 发送回调等)。核心思路是将 “事件类型” 与 “对应的业务处理逻辑” 提前绑定,epoll 的事件循环只需专注于 “检测事件” 这一核心职责:当某个事件触发时,无需在循环中硬编码判断逻辑,直接根据事件类型(如 EPOLLIN、EPOLLOUT),调用该连接 struct conn 中预设的回调函数即可。

这种设计彻底实现了 “事件触发” 与 “业务处理” 的解耦:事件循环只负责事件的监听和分发,不关心具体的业务逻辑;而业务处理逻辑(accept、recv、send 等)被封装在独立的回调函数中,与事件循环分离,代码结构更清晰、扩展性更强。

1.3状态管理更清晰

在原始 epoll 用法中,客户端连接的缓冲区(buffer)大多是临时变量—— 要么在事件循环里临时定义,要么在 recv/send 这类处理函数中局部声明。这种设计的核心缺陷是连接状态完全无法持久化,本质上是把 “连接的 IO 状态” 和 “单次事件处理的生命周期” 绑在了一起,完全没考虑 IO 操作的 “非原子性”。

要知道,网络 IO 本身就存在不确定性:比如发送数据时,内核发送缓冲区可能突然满了,一次 send 调用根本发不完所有数据;接收数据时,也可能因为网络分片、数据未完全到达等原因,一次 recv 只能拿到 “半包” 数据。但临时 buffer 的生命周期只限于当前代码块 —— 事件循环走完一轮、处理函数执行结束,临时 buffer 就会被系统回收,里面未发完的半包数据、未拼接完整的半包消息,都会直接丢失。这不仅导致数据传输异常,更让粘包、半包处理变成 “不可能完成的任务”—— 连基础的中间状态都存不住,后续根本没法拼接完整消息、重试发送剩余数据。

而 reactor 模式的核心优势,在于它为每个连接建立了 “专属状态容器”—— 通过 struct conn 结构体,把连接的 “身份标识(fd)、IO 缓冲区、状态元数据” 打包成一个整体,让状态和连接的生命周期强绑定:只要连接没断开,struct conn 就不会被释放,里面的读缓冲区(rbuffer)、写缓冲区(wbuffer)以及对应的已用长度(rlength、wlength),就能一直保持有效,实现了状态的 “持久化”。

这种设计背后,其实是对 “网络 IO 本质” 的深刻理解 —— 网络 IO 不是 “一次调用就能完成” 的原子操作,而是 “多次事件触发 + 逐步完成” 的过程,必须有持续的状态存储来衔接这些碎片化的步骤。具体来说:

  • 处理半包接收时:第一次 recv 拿到部分数据,直接存入该连接的 rbuffer 并更新 rlength,后续再次触发 EPOLLIN 事件时,新接收的数据会追加到 rbuffer 末尾,直到凑够完整消息再进行业务处理 —— 这相当于给每个连接配了一个 “专属消息暂存区”,不用再担心中间数据丢失;
  • 处理半包发送时:一次 send 没发完的数据,会留在 wbuffer 中,通过 wlength 记录剩余长度,之后只需要监听 EPOLLOUT 事件(内核发送缓冲区空闲时触发),就能从 wbuffer 中读取剩余数据继续发送,直到全部发完 —— 这相当于给每个连接配了一个 “发送等待队列”,避免了数据丢失和重复发送。

除此之外,这种状态管理方式还让代码逻辑更 “可控”:每个连接的 IO 状态(比如当前读了多少数据、还有多少数据没发)都集中在 struct conn 里,不用再通过全局变量、临时参数传递来追踪状态,排查问题时也能直接定位到具体连接的缓冲区数据,大大降低了复杂场景的调试难度。

1.4代码判断细节

            if(events[i].events & EPOLLIN){
                conn_list[connfd].r_action.recv_callback(connfd);
            }
            if(events[i].events & EPOLLOUT){
                conn_list[connfd].send_callback(connfd);
            }

为什么这段是两个if而不是elseif呢?

  因为同一个fd在同一时间可能既有可读事件也有可写事件,两种事件都需要判断。

2..整个reactor工作流程:

1. 初始化:注册监听,绑定行为

  • 创建监听套接字(sockfd),绑定端口并开始监听。
  • 初始化 epoll 实例(epfd),作为事件多路复用的核心。
  • 将监听套接字加入 epoll,关注 EPOLLIN 事件(有新连接到来)。
  • 关键一步:为该 socket 绑定回调函数 accept_cb —— 即“一旦可读,就执行 accept”。

2. 事件循环:统一分发,按需响应

  • 调用 epoll_wait() 阻塞等待,直到任意 fd 上有事件就绪。
  • 遍历返回的就绪事件列表,获取每个触发事件的 fd 和事件类型(EPOLLIN / EPOLLOUT)。
  • 根据 fd 找到对应的连接对象(如 conn_list[fd]),调用其预设的回调:
    • 若是监听 fd + EPOLLIN → 执行 accept_cb
    • 若是客户端 fd + EPOLLIN → 执行 recv_cb
    • 若是客户端 fd + EPOLLOUT → 执行 send_cb

3. 动态调整:状态驱动,灵活切换

  • 在处理完一次操作后,通过 set_event(fd, new_events, 0) 修改 epoll 关注的事件:
    • recv_cb 读取请求后 → 改为监听 EPOLLOUT,准备发送响应;
    • send_cb 发送完成或出错后 → 改回监听 EPOLLIN,继续等待下一次请求;
  • 使用 EPOLL_CTL_MOD 实现事件的实时更新,确保只在合适时机被唤醒。

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值