Netty 框架设计
约 1888 字大约 6 分钟
2026-03-11
总体设计
- 通用工具类
- 自定义的内存池
- 编解码框架及各种协议层的支持
- DNS 解析
- 高性能的本地传输
IO 设计
Reactor 模式
常见的 Reactor 有三种方案
- 单 Reactor 单进程 / 线程
- 单 Reactor 多线程 / 进程
- 多 Reactor 多进程 / 线程
单 Reactor 单线程
Reactor 中有三个角色
- Reactor:负责监听并分发事件
- Acceptor:负责获取连接
- Handler:负责处理具体业务 流程: Reactor 通过 Select 监听事件,当收到事件后根据事件类型决定分发至 Acceptor 还是 Handler
- 若是连接事件,Acceptor 使用 accept 方法获取连接,并创建一个 Handler 对象用于处理后续的响应事件
- 若不是连接事件,则交由当前连接对应的 Handler 进行响应。具体包括:读取-处理-发送至客户端 缺陷:
- 单线程无法充分利用多核 CPU
- 若 Handler 的处理耗时较长,将会增大整体的响应延迟
单 Reactor 多线程
为了克服单线程的缺陷,引入子线程负责具体的业务处理,当子线程完成处理后将结果传回主线程并对客户端进行响应 引入多线程后,主线程不必因长时的业务处理而阻塞而是可以继续处理其他连接事件 缺陷:
- 虽然事件的处理使用了多线程,但所有事件的监听与响应依旧为单线程,但瞬时高并发的场景下容易成为性能瓶颈
多 Reactor 多线程
单 Reactor 多线程中只是将业务处理的部分交给多线程处理,而主线程也就是 Reactor 依旧需要负责所有事件的监听与响应 多 Reactor 方案下主 Reactor 只负责监听事件,当出现新的连接便创建新的 SubReactor 并将连接完全交由这个子线程,由子线程负责该连接接下来的监听、业务处理与响应
核心分发:Boss 与 Worker 的协作
1. 精准分发机制 (Socket 到 Epoll 的路径)
连接的分配不是随机的,而是经过了严格的“所有权转换”:
- 监听绑定:内核根据端口绑定(Port Binding)确保只有发往特定端口的 SYN 包会触发 Boss 线程。
- 分配(Accept):Boss 线程通过
accept系统调用产生新 Socket 后,通过Chooser(默认轮询next())选定一个 Worker。 - 注册(Register):Boss 线程将新 Socket 的文件描述符(FD)通过
epoll_ctl显式注册到 Worker 专属的epoll实例中。 - 锁定:一旦注册完成,该 Socket 的回调函数就与该 Worker 的
epoll实例绑定,实现了线程无锁化。
2. 工作线程不平衡的解决方案
当轮询算法导致某些 Worker 过载时,可采用以下策略:
- 业务线程池剥离:将 Handler 中的重计算逻辑移出 Worker 线程,确保 Worker 只负责极速的读取和分发。
- SO_REUSEPORT (内核负载均衡):
- 允许多个 Boss 线程同时监听同一端口。
- 由内核协议栈(四层)实现连接的负载均衡,减少单个 Boss 的分发压力。
- 自定义 Chooser:实现“最小连接数”优先分配算法,替换默认的轮询策略。
Netty 的方案
Netty 基于多 Reactor 多线程方案,相较于原始方案进行了改进 Netty 中分为两个组
- BossGroup:负责接收新的客户端连接
- WorkerGroup:负责处理已建立的 IO 读写事件
流程
- 客户端完成连接,进入
accept队列 - Boss 线程的
Selector检测到OP_ACCEPT事件,调用accept()方法得到一个SocketChannel,将该SocketChannel注册到 WorkerGroup 中的某个 EventLoop 中 - 被选中的 Worker 线程将
SocketChannel注册到自己的Selector中并监听OP_READ/OP_WRITE事件 - Boss 线程回到监听状态,准备接受下一个连接
EventLoop
在 WorkerGroup 中,一个 EventLoop 对象会绑定一个线程 EventLoop 顾名思义——事件循环,事实也确实如此,EventLoop 是一个运行在单线程上的事件循环,负责处理其注册的Channel中的所有 IO 事件以及提交给它的普通任务(Runnable) EventLoop 内部结构
public final class NioEventLoop extends SingleThreadEventLoop {
private Selector selector; // Java NIO 的多路复用器
private final Queue<Runnable> taskQueue; // 普通任务队列
private final Queue<ScheduledFutureTask<?>> scheduledTaskQueue; // 定时任务队列
private volatile Thread thread; // 执行该 EventLoop 的唯一线程
}前面提到,Boss 线程会将 channel 绑定至某个 EventLoop 中,即channel.eventLoop() == this,如此一来 EventLoop 的selector.select()阻塞调用将能够监听到对应channel的事件 EventLoop 与 Channel 的关系为一对多,即一个 EventLoop 用于处理多个 Channel,此外因为 EventLoop 又是和线程一一对应的,因此同一连接将不存在并发问题 EventLoop 中的循环内容包含
selector.select(selectTimeout);:发现事件并存在超时时间processSelectedKeys();:处理select()发现的事件runAllTasks();:处理所有提交的任务,包含普通任务、定时任务、Netty 内部任务,任务处理与 IO 事件的处理相对独立 Netty 主要根据是否有定时任务以及定时任务的到期时间来决定selectTimeout的值,目的是尽可能不影响定时任务的按时执行。如发现定时任务已到期,将会设为 0 即不再阻塞等待;若定时任务还有 200ms 到期,则将值设为Math.min(定时任务剩余时间, 100)
1. 运行核心:死循环与 O(1) 调度
每个 EventLoop 绑定一个线程,其循环内部逻辑遵循:
select()(等待阶段):线程调用epoll_wait进入阻塞,等待内核通过中断回调将就绪 Socket 放入rdllist(就绪链表)。这确保了处理复杂度为 O(1),与总连接数 N 无关。processSelectedKeys()(IO 阶段):只处理内核“推”上来的活跃连接。runAllTasks()(任务阶段):处理非 IO 任务(如用户自定义逻辑、定时任务)。
2. 性能保障:ioRatio 机制
Netty 引入 ioRatio(默认 50)来平衡 IO 处理和业务任务的执行时间:
- 记录 IO 阶段耗时 T。
- 计算业务任务可分配的最大时间为 T×ioRatio100−ioRatio。
- 第一性原理:这种时间片分配机制防止了耗时业务逻辑导致 IO 响应延迟,确保了 Reactor 模型的响应性。
核心组件
ServerBootstrap:服务端启动引导类Bootstrap:客户端启动引导类 ServerBootstrap/Bootstrap通过建造者模式进行配置,包括绑定EventLoopGroup、选择Channel类型、childOption通道配置和childHandler通道的处理器 Netty 服务端配置样例
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 请求处理管道(或者叫流水线)
ch.pipeline()
.addLast(new HttpServerCodec()) // HTTP协议编解码器
.addLast(new HttpObjectAggregator(64 * 1024))
.addLast(new ChunkedWriteHandler())
.addLast(jwtAuthHandler)
.addLast(new WebSocketServerProtocolHandler(path, null, true))
.addLast(messageHandler);
}
});其中的childHandler用于为每个连接的 Channel 进行处理,包括 HTTP 解码、请求分片的合并、数据分块发送等,同时可以添加自定义的 Handler 用于业务处理,如鉴权验证、消息处理等。处理器将按照流水线的形式对 Channel 进行处理
完成配置后,通过绑定端口bootstrap.bind(port)便能启动服务器,将会返回ChannelFuture作为服务器的异步结果
#review
