整体架构
Netty 的核心不是“把 Selector 包了一层”,而是把线程归属、事件传播、内存所有权和异步结果统一到 Channel 这条对象链上。
先给答案:Channel 串起线程、事件与内存所有权
Section titled “先给答案:Channel 串起线程、事件与内存所有权”Netty 把一个 Channel 固定到一个 EventLoop,让 I/O 事件和普通任务在同一线程有序推进;Pipeline 再按入站向后、出站向前传播事件,Unsafe 把统一操作落到具体传输实现,ByteBuf 则显式表达数据生命周期。
这套架构的免锁优势来自线程归属,而不是任意并行。阻塞 EventLoop 会拖住同组连接,Handler 顺序会改变协议语义,引用计数释放过早或遗漏都会破坏 ByteBuf 所有权;跨线程操作必须回到对应 EventLoop 排队。
Bootstrap └─ Channel ├─ EventLoop / IoHandler I/O 与任务执行 ├─ Pipeline 入站向后、出站向前 │ └─ HandlerContext -> Handler ├─ Unsafe 将抽象操作落到传输实现 └─ ByteBuf / FileRegion 数据与所有权外部线程: bind/register/write │ execute(task) ▼EventLoop ── runIo() ── fireChannelRead(ByteBuf) │ │ └─ runAllTasks() ◄────────┘ │ ▼ Pipeline Handler 链| 抽象 | 责任 | 源码坐标 |
|---|---|---|
Channel |
连接、地址、配置、异步操作入口 | transport/.../Channel.java:76 |
EventLoop |
一个 I/O 执行上下文,负责注册和任务 | transport/.../SingleThreadIoEventLoop.java:192 |
ChannelPipeline |
组织入站/出站事件传播 | transport/.../DefaultChannelPipeline.java:1030 |
Unsafe |
只供框架内部把抽象操作落地 | transport/.../AbstractChannel.java:324 |
ByteBuf |
reader/writer index、容量、引用计数 | buffer/.../AbstractByteBuf.java:1 |
主流程一:启动与 bind
Section titled “主流程一:启动与 bind”ServerBootstrap.bind │ ├─ channelFactory.newChannel() ├─ init(channel) ├─ group.register(channel) │ └─ EventLoop.execute(register0) └─ EventLoop.execute(channel.bind(localAddress))AbstractBootstrap#initAndRegister创建 Channel、调用init,再执行config().group().register(channel),坐标为transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java:324-340。AbstractChannel.AbstractUnsafe#register检查 EventLoop 兼容性;调用线程不是 EventLoop 时,把register0投递到 EventLoop,坐标为transport/src/main/java/io/netty/channel/AbstractChannel.java:324-363。- 注册成功后,
register0先执行pipeline.invokeHandlerAddedIfNeeded(),再触发channelRegistered和首次channelActive,坐标为AbstractChannel.java:366-390。 AbstractBootstrap#doBind不直接在调用线程 bind,而是把channel.bind投递到同一个 EventLoop,坐标为AbstractBootstrap.java:291-320与371-386。
为什么注册和 bind 必须绑定同一线程
Section titled “为什么注册和 bind 必须绑定同一线程”替代方案是让调用线程直接操作底层 SelectableChannel。这会让 Handler 安装、注册状态、bind 和事件通知出现竞态。Netty 的注释明确说明:注册、bind、connect 都绑定到同一线程,因此即使注册任务尚未执行,后续 bind 任务也会排在它之后执行,见 AbstractBootstrap.java:349-356。
主流程二:请求处理
Section titled “主流程二:请求处理”Selector / epoll ready │ ▼AbstractNioByteChannel.read │ alloc ByteBuf + read ▼pipeline.fireChannelRead(buf) │ find next inbound context ▼decoder -> business handler -> tail │ └─ write(response) -> outbound handlers -> head -> UnsafeAbstractNioByteChannel 在读异常时仍会先把可读 ByteBuf 传播给 Pipeline,再触发 readComplete 和异常,见 transport/.../AbstractNioByteChannel.java:116-140。这体现了 Netty 的资源原则:异常路径同样必须处理已经成功读取的数据。
出站从 DefaultChannelPipeline#write 的 tail.write 开始,向前寻找出站 Handler;AbstractChannelHandlerContext#write 根据 executor 是否属于当前线程决定直接调用还是创建 WriteTask,见 transport/.../AbstractChannelHandlerContext.java:780-835。
| 扩展点 | 接口/基类 | 触发时机 | 典型用途 |
|---|---|---|---|
| 传输实现 | AbstractChannel |
注册、读写、bind | NIO、epoll、kqueue |
| I/O 驱动 | IoHandler |
EventLoop 初始化与 runIo |
Selector 或 native poller |
| 入站处理 | ChannelInboundHandler |
read、active、exception | 解码、鉴权、业务 |
| 出站处理 | ChannelOutboundHandler |
write、flush、close | 编码、限流、TLS |
| 分配器 | ByteBufAllocator |
创建 ByteBuf | 池化、堆外、泄漏检测 |
- Handler 中的阻塞调用会占住 EventLoop;应使用独立业务线程池,并通过
ctx.executor()或显式 executor 回到正确线程。 Channel的线程安全不等于 Handler 业务状态安全;跨线程修改业务字段仍需同步。- bind 失败时
doBind0使用CLOSE_ON_FAILURE,只记录 Future 不处理失败会造成资源泄漏或假启动。
“对象归属线程 + 任务队列串行化”可以用于 WebSocket 会话、Actor、GUI 事件线程和分片状态机。关键不是线程少,而是让同一份可变状态拥有稳定的执行者。
面试锚点:Netty 如何保证注册和 bind 的顺序?为什么 Pipeline 能免锁?