netty怎么使用Reactor 模型的单线程 Reactor、多线程 Reactor、主从 Reactor
netty怎么使用Reactor 模型的单线程 Reactor、多线程 Reactor、主从 Reactor
在 Netty 中,这三种 Reactor 模型主要通过配置 EventLoopGroup(事件循环组)的数量和组合方式来实现。
EventLoopGroup 本质上就是一个线程池,里面的每个 EventLoop 都是一个死循环(Reactor),负责监听和处理 I/O 事件。
以下是三种模型在 Netty 中的具体实现代码和对比:
单线程 Reactor 模型 (Single Thread)
特点: 一个线程处理所有的连接(Accept)、读写(Read/Write)和业务逻辑。 Netty 实现: 只创建一个 EventLoopGroup,并指定线程数为 1。
// 只有一个线程,既当老板又当员工
EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(group) // Boss 和 Worker 是同一个组
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyHandler());
}
});
缺点: 性能瓶颈极大。如果其中一个连接的业务逻辑卡住了,所有其他连接都会停顿。
适用场景: 仅用于本地测试或极低并发的嵌入式设备。
多线程 Reactor 模型 (Multithreaded)
特点: 一个专门的线程处理连接(Accept),多个线程处理读写和业务。 Netty 实现: 创建两个 EventLoopGroup,但通常我们将它们视为“Boss”和“Worker”的雏形。但在严格的定义中,有时也指 Boss 和 Worker 共用一个多线程池(较少见),或者指早期的 Java NIO 写法。在 Netty 中,我们通常直接进化到主从模型。
注:在 Netty 语境下,通常不单独强调“多线程 Reactor”,因为 Netty 默认就是多线程的。如果非要实现“非主从”的多线程,可以是这样:
// 这种写法在 Netty 中很少见,通常都是主从分离
EventLoopGroup group = new NioEventLoopGroup(4);
b.group(group)
主从 Reactor 多线程模型 (Main-Sub / Boss-Worker) —— Netty 默认推荐
特点:
Boss Group(主): 专门负责监听端口,处理新连接(Accept)。
Worker Group(从): 专门负责已建立连接的读写(Read/Write)和业务处理。
Netty 实现: 创建两个独立的 EventLoopGroup。
// 1. Boss Group:处理连接请求,通常只需要 1 个线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 2. Worker Group:处理 I/O 读写,默认线程数是 CPU 核数 * 2
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup) // 关键点:传入两个不同的组
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyGameHandler());
}
});
为什么这是最佳实践?
分工明确: Boss 线程非常轻量,只负责“接客”,不会被繁重的游戏逻辑阻塞。
高并发: Worker 线程池可以充分利用多核 CPU 并行处理成千上万个玩家的数据包。
无锁化: 每个 Worker 线程负责一部分 Channel,保证了同一个玩家的逻辑在同一个线程里串行执行。
selector的出现,才能事件监听,才出现的reactor模型的吧
Selector(选择器)的出现确实是 Reactor 模型 得以在 Java(以及许多现代操作系统)中高效实现的基石。没有 Selector,Reactor 模型要么无法实现,要么性能会极其低下。
- 没有 Selector 的时代:阻塞与浪费
在 Java NIO 的 Selector 出现之前(或者在早期的 Socket 编程中),服务器处理并发只有两种笨办法:- 方案 A:一个连接一个线程 (Thread-per-Connection)
- 每个 Socket 都阻塞在 read() 方法上。
- 缺点: 线程切换开销极大。如果有 1 万个连接,就需要 1 万个线程,CPU 全花在调度线程上了,而不是处理业务。
- 方案 B:非阻塞轮询 (Non-blocking Polling)
- 把 Socket 设为非阻塞,用一个线程不停地循环遍历所有连接:“你有数据吗?你有数据吗?”
- 缺点: CPU 空转。如果 1 万个连接里只有 1 个有数据,这个线程也要问 9999 次“没有”,浪费了绝大部分 CPU 资源。
- 方案 A:一个连接一个线程 (Thread-per-Connection)
- Selector 的诞生:事件监听的革命
Selector 的核心作用是 “多路复用” (Multiplexing)。它底层调用了操作系统的内核机制(如 Linux 的 epoll、Mac 的 kqueue、Windows 的 IOCP)。- 工作方式:
- 你把 1 万个 Socket 注册到同一个 Selector 上。
- 调用 selector.select(),这个线程就阻塞等待(此时不消耗 CPU)。
- 一旦有任何一个 Socket 发生了事件(如连接成功、数据到达),操作系统会主动通知 Selector。
- Selector 唤醒线程,并告诉你:“嘿,这 5 个连接有数据了,快去处理!”
- 工作方式:
- Reactor 模型的诞生:基于 Selector 的设计模式
有了 Selector 这种“能同时监听成千上万个连接,且只在有事发生时才通知”的神器后,Douglass Schmidt 等人才总结出了 Reactor 模式:- Reactor(反应堆) 本质上就是一个封装了 Selector 循环的线程。
- 流程:
- 注册: 将 Channel 注册到 Selector,并绑定一个 Handler(处理器)。
- 监听: Reactor 线程调用 select() 等待事件。
- 分发: 事件发生后,Reactor 拿到对应的 Channel,找到绑定的 Handler。
- 处理: 调用 Handler 进行读写或业务逻辑。
总结它们的关系
| 概念 | 角色 | 比喻 |
|---|---|---|
| Selector | 底层工具/机制 | 就像一个保安室的监控大屏,能同时显示 1000 个摄像头的画面,只有有人闯入时才会报警。 |
| Reactor | 上层架构/设计模式 | 就像整个安保系统的工作流程:保安(线程)盯着监控大屏(Selector),警报一响(事件),就派对应的警察(Handler)去处理。 |
结论
Selector 是技术基础,Reactor 是基于这个基础构建的架构思想。
- 如果没有 Selector(或类似的 I/O 多路复用机制),我们就只能回到“一个连接一个线程”的老路,根本不可能实现 Netty 这样的高性能服务器。
- 正是因为 Selector 让单线程监听海量连接成为可能,Reactor 模型才能通过“事件驱动”的方式,极大地提升服务器的吞吐量。
- 所以,先有 Selector 这种“事件监听”的能力,才催生了 Reactor 这种“高效响应事件”的模型。
Selector(选择器)的底层是啥
Selector(选择器)本身只是 Java NIO 提供的一个抽象接口。它之所以能实现“一个线程监听成千上万个连接”,是因为它在底层调用了操作系统内核(Kernel)提供的 I/O 多路复用机制。
不同的操作系统,其底层实现是完全不同的:
| 操作系统 | 底层核心技术 | 工作方式 | 性能表现 |
|---|---|---|---|
| Linux | epoll | 事件回调,只返回活跃连接 | 极高 (Netty 默认首选) |
| Windows | IOCP | 异步完成端口,由内核通知结果 | 极高 (Windows 下最强) |
| macOS | kqueue | 类似 epoll 的事件通知 | 极高 |
| 通用/老旧 | select / poll | 每次全量轮询所有连接 | 低 (连接多了会卡死) |
为什么 Netty 这么快?
因为 Netty 在启动时会自动检测你的操作系统:
-
如果是 Linux,它就优先使用基于 epoll 优化的 EpollEventLoop。
-
如果是 macOS,它会尝试使用 KQueue。
-
如果是 Windows,它会使用 IOCP 或优化的 NIO。
一句话总结: Selector 是一个“包工头”,它表面在 Java 层指挥,实际上是把活儿派给了底层的 epoll (Linux) 或 IOCP (Windows) 这些“技术专家”去干。正是这些操作系统级的专家,保证了高并发下的低延迟。
浙公网安备 33010602011771号