reactor
为什么有reactor,reactor解决了什么问题?
因为reactor好。
2.1.1讲的传统一请求一线程有下面几个问题
每个连接消耗一个线程
线程栈通常占用 8MB(ulimit -s 查看),1000 个连接就需要 8GB 内存。
线程切换(上下文切换)需要内核保存/恢复寄存器、页表等,CPU 开销随连接数线性增长。
阻塞 I/O 浪费 CPU
当
read没有数据时,线程会进入内核睡眠状态,直到数据到达。此时线程无法做任何其他工作。即使连接上有少量数据,大部分时间线程都在等待,CPU 利用率极低。
占着坑不干活
无法支撑高并发
在 C10K(一万并发)问题中,创建一万个线程既不现实也不稳定。
因此,必须寻找一种单线程处理大量连接的方法,这就是 Reactor 诞生的原因。
Reactor 模式的核心思想
Reactor 模式的核心是:
使用一个 I/O 多路复用器(如 epoll、poll、select)同时监听多个文件描述符(fd)上的事件。当某个 fd 就绪(可读、可写等)时,epoll返回就绪的 fd 集合,然后 Reactor 调用预先准备好的相应回调函数处理该事件。整个过程是非阻塞的。
其关键组件:
事件多路复用器:
epoll_wait/select/kqueue事件处理器:针对不同事件(读、写、异常)的回调函数acceptcb,recv cb, send cb
事件注册/注销接口:
epoll_ctl recv callback accpet cb事件循环(Reactor):不断调用多路复用器,获取就绪事件并分发
通过这种方式,一个线程可以同时管理成千上万个连接,且每个连接上的 I/O 操作都不会阻塞线程,因为只有当数据已经在内核缓冲区中时(即可读),回调函数才会被调用;当 socket 发送缓冲区有空间时(即可写),才会触发写回调。
结合代码:
#define CONNECTION_SIZE 1048576 // 支持 100 万连接 struct conn conn_list[CONNECTION_SIZE]; // 每个 fd 对应一个 conn 结构 int epfd; // epoll 文件描述符reactor设置一个conn结构体为每一个连接保存状态。同时加入到数组当中。该数组一fd为索引,实现o(1)时间的访问。
开始工作后,如果有新的连接进来,fd按照数值放入数组中对应的下标中。结构体内部的recv callback回调函数指向accpet cb。分配clientfd。调用event register函数,分配好revc cb,send cb,调用set event,把该fd添加进epoll中。
进入while循环后,epoll ctl会监听。
如果有,事件会进入就绪链表。直接把链表里的fd放到epoll event里面。
如果是epoll in。那么会调用recv callback回调函数。该函数指向recv cb。接收完数据后会把数据从rbuffer传给wbuffer,该fd的事件会被设置为epoll out。recv调用时,因为 epoll 已经保证该 fd 可读,内核中已经有数据了,recv只是把数据从内核拷贝出来而已。所以recv会立即返回读取到的数据,不会阻塞。
如果是epoll out 事件。调用 send callback 回调函数。该函数指向send cb。发送完数据后fd会被设置为epoll in。而且发送数据时,如果一次send没有发完(例如 socket 发送缓冲区满),传统的阻塞模型会一直等待。在 Reactor 中,我们可以只发送一部分,然后继续保持EPOLLOUT监听,待下次可写时继续发送。通过事件驱动,避免阻塞。
如果没有reactor的话,
每个连接一个线程:线程数等于连接数,内存爆炸。
阻塞 I/O:
recv会一直等到对方发来数据,该线程在此期间什么都做不了。即使连接空闲,线程也占用内核资源。发送阻塞:如果客户端接收窗口为 0,
send会阻塞,导致该线程卡死,影响其他连接(虽然其他线程仍可运行,但线程资源已被浪费)。
而在 Reactor 模式下,所有这些阻塞点都变成了事件驱动:没有数据时recv_cb不会被调用;发送缓冲区满时EPOLLOUT不会触发,send_cb不会执行,线程去处理其他就绪的连接。因此,单线程可以高效支撑成千上万的连接。
五、Reactor 模式带来的具体好处(结合代码)
| 优势 | 在代码中如何体现 |
|---|---|
| 高并发 | conn_list大小 1048576,理论上支持百万连接,而epoll_wait一次返回多个就绪事件。 |
| 低内存 | 每个连接只占用一个struct conn(约几 KB,因为缓冲区固定大小),没有额外线程栈。 |
| 非阻塞 I/O | recv/send在触发事件时才会在回调中调用,epoll 保证可读/可写,不会阻塞。 |
| 动态事件切换 | set_event(fd, EPOLLOUT, 0)和set_event(fd, EPOLLIN, 0)实现读/写状态的精确转换。 |
| 业务解耦 | ws_request不涉及网络 I/O,只操作缓冲区和协议状态,可独立测试和修改。 |
总结:为什么要有 Reactor?
Reactor 是解决 C10K/C1000K 问题的标准模式。
它通过事件多路复用和非阻塞 I/O,使得一个线程能够管理成千上万个连接,避免了传统多线程模型的内存爆炸和上下文切换开销。同时,它将业务逻辑与网络 I/O 分离,让开发者可以专注于协议实现,而底层的高性能并发由 Reactor 框架保证。
在代码中,main+epoll_wait是 Reactor 的心脏;accept_cb、recv_cb、send_cb是负责具体 I/O 的肢体;ws_request、handshark、编解码函数则是业务大脑。它们各司其职,共同构成了一个简洁但完整的 Reactor 模式 WebSocket 服务器。
如果没有 Reactor,需要为每个 WebSocket 连接创建独立线程,这在小规模测试中或许可行,但在生产环境中面对几千个浏览器客户端时,系统会迅速崩溃。因此,要有Reactor 。
什么是reactor?
reactor是一种事件驱动的网络架构。核心思想是:用一个或尽量少的线程配合io多路复用(epoll)同时监听十万以上的文件描述符事件。当事件连接,可读,可写时,自动调用相应的回调函数来做处理。
ractor什么时候使用?
1.连接数量巨大。如果需要处理成千上万的链接,一线程一连接会消耗大量的资源
2.很多io压根没干活。建立连接后,很多连接都没有数据来回传,只有几个io会传数据,造成资源的浪费。而reactor可以来回切换,有数据就绪的话加进就绪队列。只处理就绪队列。
那么reactor是否真的实现百万并发?reactor的测试:
服务器虚拟机ubuntu 2204.1 4核心 内存7.5g
客户端1:ubuntu 2204.1 2核心 内存4g
客户端2:ubuntu 2204.1 2核心 内存4g
客户端3:ubuntu 1604.6 2核心 内存4g
得到验证:
reactor用在哪里?
Web服务器:如Nginx(采用多阶段事件处理模型)、Node.js(基于事件驱动、非阻塞I/O模型)。
反向代理与负载均衡:如Nginx。
应用服务器:如Tomcat、Jetty等,其内部的NIO模块通常都基于Reactor模式实现。
即时通讯(IM)与聊天服务器:需同时处理大量用户的长连接,Reactor是理想选择。
游戏服务器:许多高性能游戏服务器引擎(如Skynet)都基于Reactor模式构建,以支持大量玩家的同时在线和实时交互。
基于reactor 实现了webserver和websocket
使用wrk -c50 -t10 -d10s http://192.168.:2000/来测试
wrk:命令行工具,用于发起 HTTP 请求,测量服务器的吞吐量和延迟。-c50:设置并发连接数为50,即模拟 50 个客户端同时与服务器建立连接。-t10:设置线程数为10,wrk 会启动 10 个线程来发送请求(通常每个线程管理一部分连接)。-d10s:设置测试时长为10 秒(10s表示 10 seconds)。http://192.168.:2000/:目标服务器的地址,使用 HTTP 协议,IP 为192.168.137.132,端口2000,根路径
用 10 个线程、50 个并发连接,持续 10 秒访问指定的 HTTP 服务(运行在 192.168. 的 2000 端口),最终输出请求总数、每秒请求数(QPS)、延迟分布等性能指标。
线程统计(Thread Stats)
2.1 延迟(Latency)
| 指标 | 值 | 说明 |
|---|---|---|
| Avg | 556.37 µs | 平均每个请求的响应延迟约0.56 毫秒,极低 |
| Stdev | 147.61 µs | 标准差较小,延迟分布比较集中 |
| Max | 8.13 ms | 最大延迟约 8 毫秒,没有出现长尾延迟 |
| +/- Stdev | 82.37% | 约 82% 的请求落在平均值 ±1 个标准差范围内(即 408.76 µs ~ 704.04 µs),大部分请求响应非常稳定 |
结论:服务延迟表现优秀,处于微秒级到毫秒级,适合对延迟敏感的业务。
2.2 单线程每秒请求数(Req/Sec)
| 指标 | 值 | 说明 |
|---|---|---|
| Avg | 8.88k | 平均每个线程每秒处理8880个请求 |
| Stdev | 1.29k | 各线程之间的吞吐量差异不大 |
| Max | 18.02k | 某个线程在某个瞬时秒内最高处理了 18020 个请求 |
| +/- Stdev | 75.92% | 约 76% 的“线程-秒”采样落在平均值附近 |
3. 总体统计
总请求数:
887441(约 88.7 万次请求)测试时长:
10.10s(实际耗时略多于设定的 10 秒,通常是 wrk 完成所有请求并收尾的时间)读取数据量:
173.50 MB(假设每个响应包体大小约 200 字节左右)
关键性能指标
Requests/sec (QPS):
87864.91
整体每秒处理约8.79 万个请求,非常高。Transfer/sec:
17.18 MB/s
网络吞吐约17 MB/秒。
4. 性能评估
| 维度 | 表现 | 评价 |
|---|---|---|
| 延迟 | 平均 < 1ms,最大 8ms | 极低,适合实时性要求高的服务 |
| 吞吐量 | QPS ≈ 8.8 万 | 非常高,常见于高效 HTTP 服务器(如 Nginx、Node.js 异步模型、Rust、Go 等) |
| 稳定性 | 延迟标准差小,最大/平均比值 ≈ 14.6 | 波动小,性能稳定 |
| 资源利用 | 10 线程 + 50 连接,处理 88 万请求 | 并发模型合理,没有成为瓶颈 |
