
Netty 的架构中有六个核心概念贯穿始终:EventLoopGroup (线程组容器)、EventLoop (事件循环)、Thread (执行线程)、Selector (IO 多路复用器)、Channel (连接通道)和 ByteBuf(数据容器)。它们各自承担独立职责,又通过持有、绑定、注册、监控等多种关系紧密耦合在一起,构成了 Netty 高性能网络编程的完整骨架。理解六者之间的关联关系,是掌握 Netty 架构设计的关键前提。
以下问题构成了理解六者关系的核心线索:
-
EventLoopGroup、EventLoop、Thread三者是什么关系?谁创建谁、谁持有谁、谁绑定谁? -
Channel如何从游离状态进入 Netty 的线程模型?注册到EventLoop后,它与Selector又是什么关系? -
ByteBuf作为数据载体,在Channel到ChannelPipeline再到ChannelHandler的链路中扮演什么角色?读和写两个方向的数据流有何不同? -
Netty 4.2 中
Selector从EventLoop剥离到IoHandler,这层抽象对理解六者关系有什么影响?本文以图示为主线,沿着线程模型骨架、IO 多路复用接管、连接注册绑定、数据高速公路、完整读写链路、Boss/Worker 双组模型的递进逻辑,用 12 张以上图表讲透六者关联。
一、六大概念速览:各自定位与全景关系图
在深入分析六者关系之前,先用一张总图和一组生活类比建立直觉。
EventLoopGroup 是线程组容器,管理一组 EventLoop 的生命周期与负载均衡。EventLoop 是事件循环工人,单线程串行处理所有 IO 事件和任务。Thread 是执行线程,与 EventLoop 一对一绑定,生命周期内不变。Selector 是 IO 多路复用器,一次 select() 返回所有就绪事件。Channel 是连接通道,代表一个网络连接,整个生命周期归属一个 EventLoop。ByteBuf 是数据容器,在 ChannelPipeline 上双向流转。
用生活类比来理解:EventLoopGroup 相当于车间主任,指挥一组工人;EventLoop 相当于流水线工人,负责具体操作;Thread 相当于工人的身体,一个工人对应一个身体,永不更换;Selector 相当于工人的眼睛,同时监控多台机器的运行状态;Channel 相当于一条传送带,代表一条网络连接;ByteBuf 相当于传送带上的货物箱,承载实际数据。
六者的持有和绑定关系可以用下面的总图概括:

上图展示了六者的核心关系链:EventLoopGroup 持有 EventLoop 数组(1:N),EventLoop 绑定 Thread(1:1)并持有 IoHandler(1:1),IoHandler 持有 Selector(1:1),Selector 监控已注册的 Channel(1:N)。注册路径从 EventLoopGroup.register() 委托到 EventLoop,再由 IoHandler.register() 最终注册到 Selector。ByteBuf 在 ChannelPipeline 上流转。后续各节将逐一展开这些关系。
二、线程模型骨架:EventLoopGroup/EventLoop/Thread 三层绑定
在六大概念速览的基础上,首先深入线程模型的三层骨架:EventLoopGroup 如何持有 EventLoop,EventLoop 如何绑定 Thread。

上面的时序图清晰地展示了两个阶段:构造阶段和首次执行阶段。构造阶段,EventLoopGroup 通过 newChild() 循环创建 N 个 EventLoop,此时每个 EventLoop 都已就绪但没有 Thread。首次执行阶段,SingleThreadEventExecutor 调用 startThread(),通过 ThreadPerTaskExecutor 创建并启动 Thread,此后 Thread 与 EventLoop 永恒绑定。
三层关系如下图所示:

EventLoopGroup 与 EventLoop 是 1:N 关系,EventLoop 与 Thread 是 1:1 绑定。这种绑定关系在创建时机上有明确的分界:


构造阶段只创建 EventLoop 实例,不创建 Thread。首次 execute() 时才创建 Thread 并启动事件循环。这种延迟创建策略避免了未使用的 EventLoop 提前消耗线程资源。
以下源码展示了 children 数组的分配和 newChild() 的调用:
java
io.netty.util.concurrent.MultithreadEventExecutorGroup
// children 数组声明,持有所有 EventLoop 实例
private final EventExecutor[] children;
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 若未指定 Executor,默认使用 ThreadPerTaskExecutor(一任务一线程策略)
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
// 分配 children 数组,容量等于线程数
children = new EventExecutor[nThreads];
// 循环创建每个 EventLoop 实例
for (int i = 0; i < nThreads; i ++) {
// newChild() 是抽象方法,由子类(如 NioEventLoopGroup)实现
children[i] = newChild(executor, args);
}
// 创建轮询选择器,按 Round-Robin 策略分配 EventLoop
chooser = chooserFactory.newChooser(children);
}
newChild() 是抽象方法,由子类(如 NioEventLoopGroup)实现。每个 EventLoop 创建后,通过 chooserFactory.newChooser(children) 创建轮询选择器,后续 register() 时由 chooser.next() 按 Round-Robin 选出一个 EventLoop。
Thread 的创建由 ThreadPerTaskExecutor 负责:
java
io.netty.util.concurrent.ThreadPerTaskExecutor#execute
// 每个任务创建一个新线程并立即启动,实现 EventLoop 与 Thread 的一对一绑定
@Override
public void execute(Runnable command) {
threadFactory.newThread(command).start();
}
threadFactory.newThread(command).start() 这行代码是 EventLoop 与 Thread 一对一绑定的根源。每个任务创建一个新线程并立即启动,不涉及线程复用。
之所以不用线程池而用"一任务一线程"策略,原因在于 EventLoop 自带任务队列,单线程串行执行所有任务,不存在线程复用的需求。更关键的是,Thread 被创建为 FastThreadLocalThread,其内部持有 InternalThreadLocalMap,使 FastThreadLocal 读写达到 O(1) 数组索引访问。这是 Netty 无锁化的基础:Channel 所有操作都在其绑定的 EventLoop 线程中执行,通过 inEventLoop() 判定,单线程串行无需加锁。
三、IO 多路复用层:EventLoop/IoHandler/Selector 的可插拔接管
在线程模型骨架就绪后,IO 多路复用层进一步接管了具体的 IO 操作。Netty 4.2 的关键变化是:Selector 不再由 EventLoop 直接持有,而是剥离到 IoHandler。

上面的时序图展示了 IoHandler 的创建链路:SingleThreadIoEventLoop 构造器调用 ioHandlerFactory.newHandler(this) 创建 IoHandler 实例,IoHandler 内部在构造时通过 SelectorProvider.openSelector() 创建 Selector。此后 EventLoop 持有 IoHandler,IoHandler 持有 Selector,形成三层持有链。
层级关系和可插拔传输层如下图所示:

IoHandler 的三个核心职责是:run() 轮询就绪事件并分发、register() 将 Channel 注册到 Selector、wakeup() 唤醒阻塞的 select()。Selector 的角色是监控多个 Channel 的 SelectionKey,一次 select() 返回所有就绪事件,避免每个 Channel 单独轮询的效率问题。
以下源码展示了 IoHandler 的创建过程:
java
io.netty.channel.SingleThreadIoEventLoop
protected SingleThreadIoEventLoop(IoEventLoopGroup parent, Executor executor,
IoHandlerFactory ioHandlerFactory, Queue<Runnable> taskQueue,
Queue<Runnable> tailTaskQueue,
RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, false,
ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported(),
taskQueue, tailTaskQueue, rejectedExecutionHandler);
// 关键:通过工厂创建 IoHandler 实例,此时 Selector 在 IoHandler 内部被创建
this.ioHandler = ioHandlerFactory.newHandler(this);
}
ioHandlerFactory.newHandler(this) 是关键调用。换一个 IoHandlerFactory 就换一个传输层(NIO / Epoll / KQueue / io_uring),EventLoop 代码零修改。这就是 Netty 4.2 将 Selector 从 EventLoop 剥离到 IoHandler 的核心价值:可插拔传输层。
EventLoop 的事件循环主方法也体现了 IoHandler 的地位:
java
io.netty.channel.SingleThreadIoEventLoop#run
// 事件循环主方法:IO 处理与任务执行交替进行
@Override
protected void run() {
assert inEventLoop();
ioHandler.initialize();
do {
// 运行 IO 处理(select + processSelectedKeys)
runIo();
if (isShuttingDown()) {
ioHandler.prepareToDestroy();
}
// 运行所有普通任务,最多执行 maxTaskProcessingQuantumNs 纳秒
runAllTasks(maxTaskProcessingQuantumNs);
// 持续循环直到确认关闭或可挂起
} while (!confirmShutdown() && !canSuspend());
}
run() 方法中,runIo() 委托给 ioHandler.run(context) 执行 IO 处理(select + processSelectedKeys),runAllTasks() 执行普通任务。IO 处理和任务执行交替进行,这就是 EventLoop 名字中"Event Loop"的含义。
四、连接通道与数据流转:Channel/Pipeline/ByteBuf 的双向高速公路
在 IO 多路复用层就绪后,Channel 如何进入线程模型?ByteBuf 如何在 Pipeline 上流转?本节将串联这两条关键链路。

上面的时序图展示了 Channel 注册的完整流程:用户线程调用 EventLoopGroup.register(channel),chooser 按 Round-Robin 轮询选出一个 EventLoop,EventLoop 将注册任务委托给 IoHandler,最终在 Selector 上创建 SelectionKey,建立 Channel 到 SelectionKey 再到 Selector 的引用链。
注册后,Channel 整个生命周期归属这一个 EventLoop,不可更换。基数关系是:EventLoop 1:N Channel(一个工人管多条传送带),但 Channel 1:1 EventLoop(一条传送带只归一个工人管)。
注册入口的源码极其简洁:
java
io.netty.channel.MultithreadEventLoopGroup#register
// 模板方法:通过 chooser 轮询选择 EventLoop,然后委托给它执行注册
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next() 返回 chooser 选中的 EventLoop,然后委托给它执行注册。这是模板方法模式的典型应用:父类定义流程骨架,子类实现具体细节。
注册完成后,Channel 持有一条 ChannelPipeline 双向链表: 
ChannelPipeline 由 HeadContext 到 ChannelHandler 链再到 TailContext 组成。ByteBuf 是 Pipeline 上的"货物":读数据时从 Socket 到 ByteBuf 再到 Inbound 链传递;写数据时从 Outbound 链到 ByteBuf 再到 Socket 发送。两个方向走的是同一条链表,但方向相反。
ByteBuf 的分配来源也值得关注:

Channel 通过 alloc() 获取 ByteBufAllocator,后者通过 MagazineGroup 使用 EventLoop 线程的 FastThreadLocal 缓存实现 O(1) 内存分配。这里体现了 ByteBuf 与 Thread 的隐含关系:高效的内存分配依赖 Thread 与 EventLoop 的 1:1 绑定。
以下是 Channel 获取 ByteBufAllocator 的接口定义:
java
io.netty.channel.Channel#alloc
// Channel 通过 alloc() 获取 ByteBufAllocator,用于分配 ByteBuf
default ByteBufAllocator alloc() {
return config().getAllocator();
}
Pipeline 的双向链表结构在构造时初始化:
java
io.netty.channel.DefaultChannelPipeline
// 双向链表结构:Head 和 Tail 作为哨兵节点
final HeadContext head;
final TailContext tail;
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
// 创建头尾哨兵节点
tail = new TailContext(this);
head = new HeadContext(this);
// 建立双向链表
head.next = tail;
tail.prev = head;
}
HeadContext 同时实现 ChannelOutboundHandler 和 ChannelInboundHandler,是 IO 操作的入口。TailContext 是入站事件的终点,对未消费的事件发出警告。两者作为哨兵节点,构成了 Pipeline 的骨架。
ByteBuf 实现了 ReferenceCounted 接口,retain() 加一、release() 减一,引用计数归零时回收内存。这一机制确保了 ByteBuf 在 Pipeline 传递过程中的生命周期可控。
五、完整读写链路与 Boss/Worker 双组模型
将前四节的内容串联起来,一条完整的数据读链路和写链路就清晰了。

上面的时序图展示了完整的数据读取流程:Selector.select() 返回就绪事件,processSelectedKey() 分发到 AbstractNioUnsafe.read(),后者通过 channel.alloc().allocateBuffer() 分配 ByteBuf(引用计数 = 1),然后通过 pipeline.fireChannelRead(byteBuf) 将 ByteBuf 推入 Inbound 链,从 Head 到 Decoder 解码为业务对象,再到 BusinessHandler 处理,最后调用 byteBuf.release() 回收内存。
ByteBuf 在读链路中的分配时机是 read() 方法内部,使用 EventLoop 线程的 MagazineGroup 实现线程本地缓存的高速分配。释放责任在业务 Handler:处理完后必须调用 byteBuf.release(),否则内存泄漏。

上面的时序图展示了完整的数据写入流程:业务代码调用 channel.writeAndFlush(msg),Outbound 链从 Tail 向 Head 传递,Encoder 将业务对象编码为 ByteBuf,Head 将 ByteBuf 聚合到 ChannelOutboundBuffer,unsafe.flush() 将数据写入 Socket,成功后释放 ByteBuf。
读方向与写方向的关键差异在于:读是 Selector 触发后分配 ByteBuf 再沿 Pipeline 传播;写是沿 Pipeline 传播后由 Encoder 编码为 ByteBuf,暂存到 ChannelOutboundBuffer,最终刷入 Socket。ByteBuf 在读链路中由系统分配,在写链路中由 Encoder 创建,但无论哪个方向,最终都需要 release() 回收。
在服务端场景下,Netty 通常使用两个 EventLoopGroup:

Boss Group 负责 OP_ACCEPT(接受新连接),Worker Group 负责 OP_READ 和 OP_WRITE(数据读写)。新连接的生命周期是:Boss Group 的 EventLoop accept 新 SocketChannel,通过 chooser.next() 从 Worker Group 选一个 EventLoop,将 SocketChannel 注册到 Worker EventLoop 的 Selector。
Boss Group 通常只需 1 个线程,因为一个 ServerSocketChannel 只需一个 EventLoop 监听。Worker Group 默认 CPU 核数 × 2 个线程,每个 Worker EventLoop 管理多个 SocketChannel。
六、整体链路串联:六者关系全景图与设计思想
将前五节的所有关系汇总为一张全景图:

六者之间存在六种关系类型:
-
持有关系:
EventLoopGroup到EventLoop,EventLoop到IoHandler,IoHandler到Selector,Channel到Pipeline -
绑定关系:
EventLoop与Thread(1:1) -
注册关系:
Channel到EventLoop再到Selector -
数据关系:
Pipeline与ByteBuf -
监控关系:
Selector到SelectionKey再到Channel -
分配关系:
EventLoop到ByteBuf(viaByteBufAllocator到MagazineGroup)
完整的关系基数如下表所示:
| 关系 | 基数 | 说明 |
|---|---|---|
| EventLoopGroup 到 EventLoop | 1:N | children 数组持有,chooser 轮询分配 |
| EventLoop 到 Thread | 1:1 | ThreadPerTaskExecutor 创建,生命周期内不变 |
| EventLoop 到 IoHandler | 1:1 | 构造时 ioHandlerFactory.newHandler(this) 创建 |
| IoHandler 到 Selector | 1:1 | 构造时 SelectorProvider.openSelector() 创建 |
| EventLoop 到 Channel | 1:N | register() 注册,一个 EventLoop 可服务多个 Channel |
| Channel 到 EventLoop | 1:1 | 注册后不可更换,整个生命周期归属一个 EventLoop |
| Selector 到 Channel | 1:N | 通过 SelectionKey 监控多个 Channel |
| Channel 到 Pipeline | 1:1 | 每条 Channel 持有一条双向链表 Pipeline |
| Pipeline 到 ByteBuf | 1:N | 一次读写可能产生多个 ByteBuf 在链上传递 |
| EventLoop 到 ByteBuf | 间接 | 通过 ByteBufAllocator 到 MagazineGroup 分配 |
这套架构的设计思想可以凝练为十二个字:线程绑定 + IO 剥离 + 双向流水线 。ThreadPerTaskExecutor 实现线程与 EventLoop 的永恒绑定,是 Netty 无锁化的基础。IoHandler 将 Selector 从 EventLoop 剥离,实现传输层可插拔。ChannelPipeline 双向链表实现 ByteBuf 的入站出站流转,构成数据高速公路。
全文小结
本文聚焦六大核心概念之间的关联关系,从线程模型和数据流转两个维度,深入分析了 Channel、EventLoop、EventLoopGroup、Thread、Selector、ByteBuf 六者如何协作构成 Netty 的高性能网络编程骨架。
在线程模型层面,EventLoopGroup 持有 N 个 EventLoop,每个 EventLoop 与一个 Thread 一对一绑定,形成"组到工人到身体"的三层骨架,ThreadPerTaskExecutor 的"一任务一线程"策略是绑定的根源。在 IO 多路复用层面,Netty 4.2 将 Selector 从 EventLoop 剥离到 IoHandler,形成"EventLoop 到 IoHandler 到 Selector"的持有链,通过 IoHandlerFactory 实现传输层可插拔。在连接绑定层面,Channel 通过 EventLoopGroup.register() 进入模型,由 chooser 轮询分配到一个 EventLoop,并在 Selector 上创建 SelectionKey 建立引用链,此后不可更换。在数据流转层面,Channel 持有 ChannelPipeline 双向链表,ByteBuf 作为数据载体在 Pipeline 上双向传递,入站从 Selector 触发到业务处理,出站从业务编码到 Socket 发送。在服务端架构层面,Boss Group 负责接受新连接,Worker Group 负责数据读写,新连接从 Boss 流向 Worker 完成交接。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。