Netty从0到1系列之Netty逻辑架构【上】
文章目录
推荐阅读:
【01】Netty从0到1系列之I/O模型
【02】Netty从0到1系列之NIO
【03】Netty从0到1系列之Selector
【04】Netty从0到1系列之Channel
【05】Netty从0到1系列之Buffer(上)
【06】Netty从0到1系列之Buffer(下)
【07】Netty从0到1系列之零拷贝技术
【08】Netty从0到1系列之整体架构、入门程序
【09】Netty从0到1系列之EventLoop
【10】Netty从0到1系列之EventLoopGroup
【11】Netty从0到1系列之Future
【12】Netty从0到1系列之Promise
【13】Netty从0到1系列之Netty Channel
【14】Netty从0到1系列之ChannelFuture
【15】Netty从0到1系列之CloseFuture
【16】Netty从0到1系列之Netty Handler
【17】Netty从0到1系列之Netty Pipeline【上】
【18】Netty从0到1系列之Netty Pipeline【下】
【19】Netty从0到1系列之Netty ByteBuf【上】
【20】Netty从0到1系列之Netty ByteBuf【中】
【21】Netty从0到1系列之Netty ByteBuf【下】
一、Netty逻辑架构【上】
Netty 的逻辑处理架构为典型网络分层架构设计
:
- 网络通信层
- 事件调度层
- 服务编排层
1.1 网络通信层
网络通信层的职责是执行网络 I/O 的操作
。它支持多种网络协议和 I/O 模型的连接操作。当网络数据读取到内核缓冲区后,会触发各种网络事件,这些网络事件会分发给事件调度层进行处理。
网络通信层的核心组件包含BootStrap、ServerBootStrap、Channel
三个组件
1.1.1 BootStrap & ServerBootStrap
Bootstrap 是引导
的意思,它主要负责整个 Netty 程序的启动、初始化、服务器连接等过程,它相当于一条主线,串联了 Netty 的其他核心组件。
- 用于客户端引导的 Bootstrap
- 用于服务端引导的 ServerBootStrap
在 Netty 中,所有网络应用的启动都是通过 Bootstrap
(客户端)或 ServerBootstrap
(服务端)来完成的。它们是工厂类和配置容器的结合体,负责创建、配置并最终启动 Channel
。
Bootstrap
: 用于客户端。它只需要一个EventLoopGroup
,因为它只需要创建一个单独的Channel
来与远程服务器进行通信。ServerBootstrap
: 用于服务端。它需要两个EventLoopGroup
(通常如此),一个通常被称为 “Boss”,负责接收传入的连接;另一个被称为 “Worker”,负责处理已被接受的连接的流量。
类名 | 用途 | 对应场景 |
---|---|---|
Bootstrap | 用于启动 Netty 客户端 | 发起 TCP 连接(如 HTTP 客户端、RPC 调用方) |
ServerBootstrap | 用于启动 Netty 服务端 | 监听端口,接收客户端连接(如 Web 服务器、RPC 服务提供方) |
✅ 本质:它们是 Netty 的“启动器”,封装了复杂的 NIO 套接字创建、线程模型配置、Channel 初始化等流程,提供流畅式 API(Fluent API),极大简化了网络编程。
1. ✅核心方法链与配置
两者都通过流畅的链式调用(Fluent API)进行配置,核心方法包括:
- .group(): 设置
EventLoopGroup
。 - .channel(): 指定要实例化的
Channel
类(如NioSocketChannel
,NioServerSocketChannel
)。 - .handler(): 为 Bootstrap 或 ServerBootstrap 本身的
Channel
设置一个ChannelHandler
。 - .childHandler(): (ServerBootstrap 特有) 为 ServerBootstrap 接收的 子 Channel(即代表客户端连接的
SocketChannel
)设置ChannelHandler
。 - .option(): 为 Bootstrap 或 ServerBootstrap 本身的
Channel
设置底层 TCP 参数。 - .childOption(): (ServerBootstrap 特有) 为 ServerBootstrap 接收的 子 Channel 设置底层 TCP 参数。
2. ✅ 架构与流程
ServerBootstrap
和Bootstrap
的启动流程及其核心组件关系:
3. ✅ 底层实现与原理分析
启动过程分析:
ServerBootstrap.bind()
为例,其底层过程非常精妙
-
创建和初始化Channel
- 通过反射调用
channelFactory.newChannel()
创建NioServerSocketChannel
实例。 - 调用
init(channel)
方法初始化该 Channel。这是最关键的一步:- 将
.option()
设置的参数应用到 Channel 的 config 上。 - 将
.handler()
设置的ChannelHandler
添加到 Channel 的Pipeline
中。 - 在 Pipeline 的尾部自动添加一个
ServerBootstrapAcceptor
。这个 Handler 是 Netty 内部的“连接接收器”
- 将
- 通过反射调用
-
注册到 EventLoop
-
将初始化好的
NioServerSocketChannel
异步地注册到BossGroup
中的一个EventLoop
上。这个过程会提交一个任务到 EventLoop 的线程中执行。 -
在 EventLoop 线程中,真正的注册操作是将 Java NIO 的
ServerSocketChannel
注册到 EventLoop 的Selector
上,并设置兴趣操作为OP_ACCEPT
。
-
-
绑定端口
-
注册成功后,继续在 EventLoop 线程中调用
Channel
的bind()
方法,最终调用底层 Java NIO 的ServerSocketChannel.bind()
。 -
绑定成功后,会触发
channelActive
事件,Pipeline 中的处理器会相应地将OP_ACCEPT
事件再次注册到 Selector,确保开始接收连接。
-
-
接收连接(
ServerBootstrapAcceptor
的作用)- 当 BossGroup中的 EventLoop轮询到 OP_ACCEPT事件时,会调用 NioServerSocketChannel的 read() 方法,其内部通过 ServerSocketChannel.accept()创建一个 Java NIO 的 SocketChannel。
- Netty 随后会包装这个 SocketChannel 为 NioSocketChannel。
- 关键一步: ServerBootstrapAcceptor的
channelRead()
方法会被调用,这个NioSocketChannel
作为参数传入。 - ServerBootstrapAcceptor 会做以下工作:
- 将
.childOption()
和.childAttr()
设置的参数应用到子 Channel。 - 将
.childHandler()
中设置的ChannelInitializer
添加到子 Channel 的 Pipeline 中。 - 将这个子 Channel 注册到
WorkerGroup
中的一个EventLoop
上(负载均衡)
- 将
至此,新连接的建立和处理职责就从 BossGroup
完全移交给了 WorkerGroup
。
Bootstrap.connect()
的过程类似,但更简单,因为它没有子 Channel 的概念,创建好的 NioSocketChannel
会直接注册到其唯一的 EventLoopGroup
上,然后发起连接操作。
4. ✅ 实践经验与总结
- 线程池配置
- BossGroup 通常设置为 1。一个线程足以处理所有的连接接收请求,设置过多纯属浪费。
- WorkerGroup 默认大小是
CPU核心数 * 2
。这是一个很好的起点,应根据实际业务是 I/O 密集型 还是 CPU 密集型 进行调整。I/O 密集型可以调大一些。
- ChannelOption 配置
- SO_BACKLOG (Server): 决定了完全建立连接队列的长度。在高并发场景下需要适当调大。
- TCP_NODELAY (Client): 建议设置为
true
,禁用 Nagle 算法,减少小数据包的发送延迟。 - SO_KEEPALIVE (Child): 开启 TCP 层的心跳机制,保证连接存活。
- SO_RCVBUF/SO_SNDBUF: 根据网络状况调整发送和接收缓冲区大小。
- 资源管理
- 始终使用 shutdownGracefully(): 在 finally 块中优雅关闭
EventLoopGroup
,它会等待任务执行完毕并释放资源。 - ChannelInitializer 的使用: 在
initChannel
方法中添加 Handler 后,这个ChannelInitializer
会将自己从 Pipeline 中移除,所以它可以被多个 Channel 安全共享。
- Handler 的组织
- 在 ServerBootstrap 中,.handler() 用于添加处理服务端本身状态的处理器(如日志、监控),而
.childHandler()
用于添加处理业务逻辑的处理器。 - 客户端的 .handler()等同于服务端的
.childHandler()
。
1.1.2 Channel
表示通道
的意思,它是网络的载体.Channel提供了基本的 API 用于网络 I/O 操作,如 register、bind、connect、read、write、flush
等.
Netty 自己实现的 Channel
是以 JDK NIO Channel
为基础的,相比较于 JDK NIO,Netty 的 Channel 提供了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性.
NioServerSocketChannel 异步 TCP 服务端
。NioSocketChannel 异步 TCP 客户端
。- OioServerSocketChannel 同步 TCP 服务端。
- OioSocketChannel 同步 TCP 客户端。
- NioDatagramChannel 异步 UDP 连接。
- OioDatagramChannel 同步 UDP 连接。
同时Channel
还有很多种状态:
- 连接建立
- 连接注册
- 数据读写
- 连接销毁
- ….
随着状态的变化,Channel 处于不同的生命周期,每一种状态都会绑定相应的事件回调
事件 | 描述 |
---|---|
channelRegistered | Channel 创建后被注册到 EventLoop 上 |
channelUnregistered | Channel 创建后未注册或者从 EventLoop 取消注册 |
channelActive | Channel 处于就绪状态,可以被读写 |
channelInactive | Channel 处于非就绪状态 |
channelRead | Channel 可以从远端读取到数据 |
channelReadComplete | Channel 读取数据完成 |
BootStrap 和 ServerBootStrap 分别负责客户端和服务端的启动,它们是非常强大的辅助工具类;
Channel 是网络通信的载体,提供了与底层 Socket 交互的能力
1.2 事件调度层
1.2.1 事件调度层概述
事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理
,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。
事件调度层的核心组件包括
- EventLoopGroup
- EventLoop
EventLoopGroup 本质是一个线程池
,主要负责接收 I/O 请求,并分配线程执行处理请求
.
- 一个 EventLoopGroup 往往包含一个或者多个 EventLoop。EventLoop 用于处理 Channel 生命周期内的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件
- EventLoopGroup可以包含一个或者多个EventLoop
- 负责处理
Channel
生命周期内所有的I/O事件EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel
每新建一个 Channel
,EventLoopGroup 会选择一个 EventLoop 与其绑定
。
- 该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑。
EventLoopGroup
的实现类是 NioEventLoopGroup,NioEventLoopGroup 也是 Netty 中最被推荐使用的线程模型
。
NioEventLoopGroup 继承于 MultithreadEventLoopGroup,是基于 NIO 模型开发的,可以把 NioEventLoopGroup 理解为一个线程池,每个线程负责处理多个 Channel,而同一个 Channel 只会对应一个线程。
EventLoopGroup 是 Netty 的核心处理引擎
,其实 EventLoopGroup 是 Netty Reactor 线程模型的具体实现方式
,Netty 通过创建不同的 EventLoopGroup 参数配置,就可以支持 Reactor 的三种线程模型
:
- 单线程模型
EventLoopGroup 只包含一个 EventLoop
,Boss 和 Worker 使用同一个
EventLoopGroup;
- 多线程模型
- EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用
同一个
EventLoopGroup
- EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用
- 主从多线程模型
- EventLoopGroup 包含多个 EventLoop,
Boss 是主 Reactor
,Worker 是从 Reactor
- 它们分别使用不同的 EventLoopGroup,
主 Reactor 负责新的网络连接 Channel 创建
,然后把Channel 注册到从 Reactor。
- EventLoopGroup 包含多个 EventLoop,
事件调度层负责监听网络连接和读写操作
,然后触发各种类型的网络事件
,需要一种机制管理这些错综复杂的事件;
1.2.2 EventLoop【事件循环】
EventLoop
是 Netty 的核心抽象,它代表了一个持续运行的、处理所有分配给它的 I/O 事件和任务的线程。你可以将其理解为一个忠诚的管家,它一生只做一件事:在一个无限循环中,不断地检查自己负责的“领地”(Channel)上是否有事情需要处理(I/O 事件),或者主人(用户代码)是否吩咐了新的任务。
它的核心职责可以简化为一个公式:
- EventLoop = Thread + Selector + Task Queue
EventLoop的分配策略:
当 Channel 注册到 EventLoopGroup 时,EventLoopGroup 会选择一个 EventLoop 来处理该 Channel 的所有事件:
默认的分配策略是轮询(Round-Robin),保证 EventLoop 之间的负载均衡。
1.2.3 EventLoopGroup【事件循环组】
EventLoopGroup
初始化过程:
- 创建线程池:根据指定的线程数创建线程池
- 初始化 EventLoop:为每个线程创建对应的 EventLoop
- 启动线程:启动所有线程,每个线程运行 EventLoop 的 run () 方法
// 简化的EventLoopGroup初始化逻辑
public NioEventLoopGroup(int nThreads) {this(nThreads, (Executor) null);
}public NioEventLoopGroup(int nThreads, Executor executor) {this(nThreads, executor, SelectorProvider.provider());
}public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider) {super(nThreads, executor, selectorProvider);
}// 父类MultithreadEventLoopGroup的初始化
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}// 父类MultithreadEventExecutorGroup的初始化
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {// 如果未指定线程数,使用默认值:CPU核心数 * 2if (nThreads <= 0) {throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));}// 如果未指定执行器,创建默认执行器if (executor == null) {executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}// 创建EventLoop数组children = new EventExecutor[nThreads];// 初始化每个EventLoopfor (int i = 0; i < nThreads; i ++) {boolean success = false;try {// 创建EventLoopchildren[i] = newChild(executor, args);success = true;} catch (Exception e) {// 处理异常} finally {// 处理初始化失败的情况}}// 创建EventLoop选择器chooser = new GenericEventExecutorChooser(children);// 添加终止监听器terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);terminationFuture.addListener(new FutureListener<Object>() {@Overridepublic void operationComplete(Future<Object> future) throws Exception {for (EventExecutor e : children) {e.shutdownGracefully();}}});
}
EventLoopGroup
包含多个 EventLoop
,它的主要作用是分配 EventLoop
给新创建的 Channel
。它本质是一个 EventLoop
的池子,遵循一种特定的分配策略(通常是轮询 round-robin
)。
- 一个 EventLoopGroup包含多个
EventLoop
。- 每个 EventLoop绑定一个线程,永不更改。
- 一个 EventLoop可以注册多个
Channel
,但一个Channel
只能注册到一个EventLoop
。- 关键:Channel 的所有 I/O 操作都在同一个线程中执行,线程安全,无需额外同步。
- 每个 EventLoopGroup 包含多个 EventLoop
- 每个 EventLoop 绑定一个 Thread 和一个 Selector
- EventLoop 的数量默认等于 CPU 核心数
- 服务端: 通常有两个
EventLoopGroup
。- BossGroup: 只包含一个或少数几个
EventLoop
,专门负责接收客户端的连接请求(OP_ACCEPT
事件),并将建立好的连接“转交”出去。 - WorkerGroup: 包含多个
EventLoop
,负责处理被“转交”过来的连接上的所有后续 I/O 操作(如OP_READ
,OP_WRITE
)。
- BossGroup: 只包含一个或少数几个
- 客户端: 只需要一个
EventLoopGroup
,负责处理所有连接的事件。
- 一个
EventLoopGroup
包含多个EventLoop
。- 一个
EventLoop
在它的生命周期内只绑定一个线程(Thread
),这个线程负责处理所有逻辑。- 一个
EventLoop
可以被分配给多个Channel
(即一个线程管理多个连接)。- 一个
Channel
在它的生命周期内只注册到一个EventLoop
(即一个连接只由一个线程管理)。这条黄金规则从根本上避免了 Channel 层面的并发问题,无需使用同步锁。- 每个
EventLoop
都拥有自己的Selector
和Task Queue
。
1.2.4 底层实现原理
我们以最常用的 NioEventLoop
为例来分析其工作原理。
NioEventLoop 的工作循环:
EventLoop
是一个无限循环,不断执行以下步骤:
轮询(
select
)I/O 事件(如 Socket 可读、可写)。处理就绪的 I/O 事件。
执行任务队列中的任务(如
channel.write()
提交的任务)。执行定时任务(Scheduled Tasks)。
✅ 这就是 Netty 高性能的根源:单线程处理多个 Channel,避免线程上下文切换开销。
NioEventLoop
的核心是run()
方法,它是一个永不终止的循环,其工作内容可以精炼为三大步骤:
public final class NioEventLoop extends SingleThreadEventLoop {private final Selector selector; // JDK NIO Selectorprivate final SelectStrategy selectStrategy; // 选择策略private final Queue<Runnable> taskQueue; // 普通任务队列private final Queue<ScheduledFutureTask<?>> scheduledTaskQueue; // 定时任务队列// 简化后的代码示例, 只包含核心功能protected void run() {for (;;) { // 无限循环try {// 1. 轮询策略:检查是否有就绪的I/O事件或任务int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());switch (strategy) {case SelectStrategy.CONTINUE: ... case SelectStrategy.SELECT:// 如果没有任务,就阻塞地等待I/O事件,超时时间为定时任务最早时间strategy = select(timeoutMillis); ... break;default:}// 2. 处理I/O事件:processSelectedKeys()// 3. 处理所有排队的任务:runAllTasks()} catch (Throwable t) {... // 处理异常}}}
}
事件循环主流程
for (;;) {try {// 1. 轮询 I/O 事件int selectedKeys = selector.select(timeoutMillis);// 2. 处理 I/O 事件if (selectedKeys != 0) {processSelectedKeys();}// 3. 执行任务队列runAllTasks();// 4. 执行定时任务runAllTasks(timeoutMillis);} catch (Throwable t) {handleException(t);}
}
✅ 关键点:
selector.select()
是阻塞调用,直到有事件就绪或超时。runAllTasks()
执行用户提交的任务(如channel.write()
)。- 单线程串行执行,保证了
Channel
的线程安全。
-
轮询I/O事件【Select】
-
智能阻塞: Netty 会检查任务队列是否为空。如果队列为空,它就会调用
selector.select(timeout)
,进入阻塞状态,等待 I/O 事件。final class DefaultSelectStrategy implements SelectStrategy {static final SelectStrategy INSTANCE = new DefaultSelectStrategy();private DefaultSelectStrategy() { }@Overridepublic int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {// 如果有任务, 则取出,如果没有,则阻塞return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;} }
-
这个
timeout
是基于定时任务队列中最近要执行的任务来计算的,非常智能,既不会错过 I/O 事件,也不会耽误执行定时任务。private final IntSupplier selectNowSupplier = new IntSupplier() {@Overridepublic int get() throws Exception {return selectNow();} };
-
如果队列非空: 为了避免任务被延迟,它会调用
selector.selectNow()
,这是一个非阻塞调用,立刻返回。- 优化: Netty 对
selectedKeys
集合进行了优化,使用数组替代了 JDK 的HashSet
,减少了迭代过程中的垃圾产生。
- 优化: Netty 对
-
-
处理 I/O 事件 (processSelectedKeys)
- 遍历由
Selector
提供的就绪的SelectionKey
集合。 - 根据 key 的兴趣事件(
OP_ACCEPT
,OP_READ
,OP_WRITE
等),调用对应的Channel
和Pipeline
上的方法。 - 例如,当有数据可读时(
OP_READ
),会触发ChannelRead
事件,数据会从 Pipeline 的头部开始流动,被一个个ChannelInboundHandler
处理。
- 处理异步任务队列 (runAllTasks)
- 这是 Netty 保证线程安全和高性能的关键。所有用户提交的异步任务(如
ctx.write()
、channelActive
事件触发后的业务逻辑等)都会被放入这个任务队列。 EventLoop
线程会一次性将队列中的所有任务取出来并执行。- 这样做的好处是:保证了所有对同一个 Channel 的操作都在同一个线程中顺序执行,完全避免了多线程并发访问的问题,无需同步。
线程分配策略
- 当一个新的
Channel
被创建并注册到EventLoopGroup
时,EventLoopGroup
会使用next()
方法选择一个EventLoop
来管理它。默认策略是轮询(round-robin),即均匀分配,以保证负载均衡。
- 任务队列的实现
EventLoop 维护了两个任务队列:
- 普通任务队列:存放通过 execute ()、submit () 提交的任务
- 定时任务队列:存放通过 schedule () 等方法提交的定时任务
// 简化的任务处理逻辑
protected boolean runAllTasks() {// 从定时任务队列中移动已到期的任务到普通任务队列fetchFromScheduledTaskQueue();// 获取普通任务队列中的任务Runnable task = pollTask();if (task == null) {return false;}// 处理所有任务for (;;) {try {// 执行任务task.run();} catch (Throwable t) {// 处理任务执行异常}// 获取下一个任务task = pollTask();if (task == null) {// 所有任务处理完毕lastExecutionTime = ScheduledFutureTask.nanoTime();return true;}}
}// 从定时任务队列中获取已到期的任务
private void fetchFromScheduledTaskQueue() {long nanoTime = AbstractScheduledEventExecutor.nanoTime();Runnable scheduledTask = pollScheduledTask(nanoTime);while (scheduledTask != null) {// 将已到期的定时任务添加到普通任务队列taskQueue.add(scheduledTask);scheduledTask = pollScheduledTask(nanoTime);}
}
1.2.5 最佳实践与经验总结
✅ 正确用法(Best Practices)
- 线程数设置:
bossGroup
:通常 1 个线程足够。WorkerGroup
: 默认值是CPU核心数 * 2
。这是一个很好的经验值。对于纯 I/O 密集型应用(如代理、聊天服务器),可以适当调大。对于计算密集型业务,建议保持默认或甚至更少,并将耗时业务提交到独立的业务线程池,避免阻塞EventLoop
线程。
- EventLoop 复用:
- 一个
Channel
一旦注册到EventLoop
,终身绑定,不会迁移。 - 所有 I/O 操作都在该
EventLoop
线程中执行,无需加锁。
- 一个
- 避免阻塞 EventLoop
- ❌ 不要在
ChannelHandler
中执行耗时操作(如数据库查询、文件读写)。 - ✅ 应提交到业务线程池:
private final EventExecutorGroup businessGroup = new DefaultEventExecutorGroup(10);
pipeline.addLast(businessGroup, new BusinessHandler()); // 耗时操作放这里
- Channel 与 EventLoop 的绑定关系:
- 牢记 “一个 Channel 一生只由一个 EventLoop 处理” 的原则。利用这一点,可以在
ChannelHandler
中使用ThreadLocal
变量而无需担心线程安全问题。
- 优雅关闭:
- 必须调用
shutdownGracefully()
,等待正在进行的任务完成。 - 它会先平滑地关闭所有
Channel
,然后不再接收新任务,最后处理完任务队列中的任务后再终止线程。shutdownGracefully()
还支持设置安静期和超时时间。
- 监控 EventLoop:
- 可通过
EventLoop
的pendingTasks()
方法监控任务积压情况。
EventLoop
和 EventLoopGroup
是 Netty 的心脏和引擎。它们不仅仅是线程池,更是 Netty 实现高效、有序、线程安全的异步事件处理的核心机制。
EventLoopGroup
是工厂和调度器,负责分配EventLoop
。EventLoop
是勤劳的工作单元,一人一线程,负责其生命周期内所有的事件和任务。