《Netty 源码剖析与应用》详细总结:写给从未接触过 Netty 的人
- 约 25 分钟读完 - 全文 9638 字《Netty 源码剖析与应用》详细总结:写给从未接触过 Netty 的人
参考书籍:刘耀林《Netty 源码剖析与应用》(电子工业出版社,2020 年 11 月) 目标读者:会 Java、听说过 Netty 但没系统学过,想一篇文章建立 Netty 核心知识框架的人。
〇、这本书讲什么?为什么学 Netty?
普通开发在工作中很少直接碰 Netty,但只要翻开任何一个主流分布式框架的底层源码,通信模块几乎都是它:Dubbo、gRPC、Spark、Flink、Elasticsearch、HBase(HBase 从 2.0 开始默认用 Netty RPC Server 替代原生实现)。不管是做 Java Web 后台还是大数据,Netty 都是绕不过去的地基。
为什么不直接用 JDK 的 NIO?作者的回答很直白:用原生 NIO 写一套高效稳定的通信模块非常难——要处理多路复用、粘包拆包、线程模型、内存管理、断线重连等一堆坑。Netty 把这些全部封装好,让普通人也能快速开发高性能、面向协议的服务端和客户端。
全书的结构是循序渐进的四部分:
flowchart LR
A["第一部分:实战入门<br/>第1章 Netty基础篇<br/>第2章 原理部分<br/>第3章 分布式RPC"] --> B["第二部分:核心源码剖析<br/>第4章 核心组件<br/>(NioEventLoop/Channel/ByteBuf)<br/>第5章 读/写请求源码"]
B --> C["第三部分:内存管理<br/>第6章 jemalloc内存池<br/>(全书最难)"]
C --> D["第四部分:高级应用与调优<br/>第7章 时间轮高级应用<br/>第8章 问题分析与性能调优"]
两个贯穿全书的高级项目:一套类似 Dubbo 的分布式 RPC 框架(第 3 章写、第 8 章压测调优),以及一套基于时间轮的 10 亿级任务调度监控引擎(第 7 章)。
一、Netty 基础篇:先看长连接通信长什么样(第 1 章)
1.1 服务端五步法
Netty 服务端程序的固定套路:
- 创建两个线程组:Boss 线程组专门接收客户端连接;Worker 线程组处理已接收连接的 I/O 读写;
- 用启动辅助类
ServerBootstrap配置启动参数(如ChannelOption.SO_RCVBUF/SO_SNDBUF设置接收/发送缓冲区大小); - Boss 把接收到的连接注册到 Worker 后,交给
ChannelInitializer初始化 Handler 链; - 编写业务 Handler(继承
ChannelInboundHandlerAdapter,重写channelRead); - 绑定端口——绑定由 Boss 线程完成,所以主线程要执行同步阻塞方法等待绑定完成。
这就是经典的主从 Reactor 模型:
flowchart TD
subgraph BossGroup["Boss 线程组(接收连接)"]
B1["Boss NioEventLoop<br/>监听 OP_ACCEPT"]
end
subgraph WorkerGroup["Worker 线程组(处理 I/O)"]
W1["Worker 线程 1<br/>管理多个 Channel"]
W2["Worker 线程 2<br/>管理多个 Channel"]
end
C1["客户端连接 1"] --> B1
C2["客户端连接 2"] --> B1
C3["客户端连接 3"] --> B1
B1 -->|"注册 SocketChannel"| W1
B1 -->|"注册 SocketChannel"| W2
W1 --> P1["ChannelPipeline<br/>Decoder → 业务Handler → Encoder"]
W2 --> P2["ChannelPipeline<br/>Decoder → 业务Handler → Encoder"]
1.2 一个值得琢磨的示例:客户端怎么"同步等"异步响应
第 1 章的客户端示例藏着一个 Netty 编程的核心范式。客户端连续发 100 个请求,服务端异步返回。问题是:channel.writeAndFlush() 是异步的,主线程怎么拿到响应结果?
书里的解法是自实现一套 RequestFuture 机制(这正是 Dubbo 等 RPC 框架的原型):
- 每个请求带唯一 id,发送前把
RequestFuture存入以 id 为 key 的 Map,主线程在future.get()上阻塞等待; - 服务端 Handler 处理完后,把结果带上请求 id 写回;
- 客户端
channelRead收到响应,按 id 找到对应的 Future,设置结果并唤醒主线程。
书中的思考题也很有意思:示例里客户端输出是有序的(按 id 逐个 get),如何修改代码让输出无序?——答案是不再按顺序 get,而是在响应到达的回调里直接输出。这个小实验能帮你真正理解"异步 + 回调"与"同步等待"的差别。
二、原理部分:绕不开的六块基石(第 2 章)
2.1 多路复用器:select、poll、epoll
BIO 时代每个 Socket 连接要一条线程同步阻塞,1 万连接就要 1 万条线程,根本扛不住。NIO 的**多路复用器(Selector)**让一条线程可以管理成千上万个连接的接入和读写事件。
三种经典模型的区别(面试高频):
| 模型 | 遍历方式 | 连接数上限 | 触发方式 |
|---|---|---|---|
| select | 轮询全部 fd | 1024 左右 | 水平触发 |
| poll | 轮询全部 fd | 无硬上限 | 水平触发 |
| epoll | 回调就绪列表,只处理活跃连接 | 无硬上限 | 支持边缘触发 |
一个容易混淆的点:JDK 自带的 epoll 封装是水平触发(LT)的,而 Netty 通过 JNI 自己重写的 epoll 是边缘触发(ET)的,这也是 Netty 在 Linux 上推荐 EpollEventLoop 的原因之一。
2.2 Netty 的线程模型
Netty 的 Boss/Worker 默认线程数与 CPU 核数挂钩(默认 核数 × 2)。Boss 只关心 OP_ACCEPT,接收连接后把 SocketChannel 按策略轮询注册到某个 Worker 的 Selector 上。关键约束:一个 Channel 在其生命周期内只绑定一个 EventLoop 线程,所以处理 Channel 的所有事件都不需要加锁——这是 Netty 高并发能力的重要来源。
2.3 编码和解码:TCP 粘包/半包问题
TCP 是面向字节流的协议,它把数据看成无结构的字节流,根本不知道"一条消息"的边界在哪。于是产生了经典问题:
- 粘包:两条消息被合并成一次发送/接收;
- 半包(拆包):一条消息被拆成多次接收。
flowchart LR
subgraph 发送方
A["业务对象"] --> B["编码器 Encoder<br/>对象 → 字节"]
B --> C["TCP 输出缓冲区<br/>SO_SNDBUF"]
end
C -->|"② TCP 网络传输<br/>(受滑动窗口/拥塞控制影响)"| D["TCP 输入缓冲区<br/>SO_RCVBUF"]
subgraph 接收方
D --> E["解码器 Decoder<br/>处理粘包/半包"]
E --> F["业务对象"]
end
Netty 的解法是在 Pipeline 里插解码器:固定长度解码器、分隔符解码器、长度字段解码器(LengthFieldBasedFrameDecoder,最常用)——协议头里放一个长度字段,解码器攒够一条完整消息才往下游传。书中第 3 章手写 RPC 时就是用自定义协议(协议头 + 长度 + 内容)解决的。
2.4 序列化:Protobuf 与 Kryo
- Protobuf(Google):跨语言、压缩率高(相同数据比 JSON 小 3~5 倍)、需要预定义 schema,适合 RPC 服务间通信;
- Kryo:专注 Java,使用简单、速度快,但不跨语言,适合 Java 生态内部。
2.5 零拷贝:少搬一次数据是一次
零拷贝指操作数据时无须把 Buffer 从一个内存区域拷贝到另一个区域。Netty 里主要有三种场景:
- 堆外直接内存收发:用堆内存做 Socket 读写时,JVM 会先把堆内数据拷贝到堆外再写入 Socket;Netty 收发直接使用堆外内存,省掉这次拷贝;
- CompositeByteBuf 组合视图:一条消息被拆成多个数据包时,把多个 Buffer 逻辑上组合成一个(类似数据库视图),而不是物理拷贝拼接;
- FileChannel.transferTo():文件传输直接在内核空间完成,不经用户态缓冲区。
2.6 背压(Backpressure):让快生产者等一等慢消费者
flowchart LR
subgraph 生产者
P1["消息生产者 A<br/>写入缓存队列"]
P2["消息生产者 B<br/>写入缓存队列"]
end
P1 & P2 --> Q["TCP 链路"]
Q -->|"背压"| C["消费者<br/>I/O 读取放入缓存队列"]
C -.->|"消费速率 < 生产速率时<br/>TCP 窗口缩小 → 通道阻塞<br/>→ 生产者降速"| P1
机制链条:消费者消费变慢 → TCP 接收缓冲区满 → TCP 滑动窗口变小 → 生产者发送缓冲区写不进去 → 生产者降速。书中还以 Flink 为例讲了它的应用:Flink 基于 Netty 实现了 credit-based(基于信用额度)的反压机制,下游告诉上游"我还能收多少",从根本上防止快生产慢消费把系统压垮。
三、分布式 RPC:把 Netty 组装成一个 Dubbo(第 3 章)
这一章是全书第一个大项目:手写一套分布式 RPC。目标是业务方用起来像 SpringMVC/Dubbo 一样——引个 jar 包就行,不用关心 Netty 底层;服务端可动态扩容,不用写死 IP 端口。
flowchart TD
subgraph 客户端
A["业务代码<br/>像调用本地方法一样调用"] --> B["动态代理<br/>拦截方法调用"]
B --> C["协议封装<br/>类名/方法名/参数 + 请求id"]
end
subgraph 网络层
C --> D["Netty 长连接<br/>自定义协议 + Kryo 序列化"]
end
subgraph 服务端
D --> E["Netty Server Handler<br/>解码出调用信息"]
E --> F["Java 反射<br/>调用 Spring 容器中的实现类"]
end
subgraph 注册中心["Zookeeper 注册中心"]
G["服务启动时注册<br/>IP/端口/权重"]
H["客户端订阅服务列表<br/>负载均衡选择节点"]
end
F --> G
C --> H
三个关键技术点:
- 协议设计:自定义协议头(魔数 + 长度 + 请求 id)+ 序列化内容,配合 LengthField 解码器解决粘包;
- Java 反射解耦:服务端 Handler 解析出"类名 + 方法名 + 参数类型 + 参数值",从 Spring 容器找到实现类反射调用——业务代码和 Netty 的 Handler 逻辑完全解耦;
- 服务注册与发现:引入 Zookeeper(对比了 Consul/ETCD/Eureka),服务端启动注册临时节点,客户端监听节点变化做动态感知和负载均衡(可按权重)。这一层替代了传统 Nginx/LVS 的硬负载方案。
四、核心组件源码剖析(第 4 章,全书的硬核部分)
4.1 NioEventLoopGroup 与 NioEventLoop:Netty 的发动机
NioEventLoopGroup 就是线程池,NioEventLoop 是里面的每条线程,每条线程一个 Selector,绑定一堆 Channel。核心在它的 run() 方法——一个永不退出的循环:
flowchart TD
A["run() 死循环开始"] --> B["select() 轮询 I/O 事件<br/>(带超时,防止空轮询 bug)"]
B --> C{"有就绪事件?"}
C -- 是 --> D["处理 SelectionKey<br/>OP_ACCEPT / OP_READ / OP_WRITE"]
C -- 否 --> E["跳过 I/O 处理"]
D --> F["执行异步任务队列<br/>(用户提交的任务 + 定时任务)"]
E --> F
F -->|"ioRatio 控制 I/O 与任务的时间配比<br/>默认各 50%"| A
B -.->|"连续空轮询超过阈值<br/>→ rebuildSelector() 重建 Selector<br/>(规避 JDK epoll 空轮询 bug)"| B
源码里的几个精妙优化,作者都点了出来:
- SelectedSelectionKeySet:用数组替换 JDK Selector 内部的 HashSet,遍历就绪事件从哈希遍历变成数组遍历,更快;
- Selector 重建:JDK epoll 有著名的"空轮询 bug"(select 立刻返回但无事件,CPU 100%),Netty 的解法是统计空轮询次数,超阈值就新建一个 Selector 把所有 Channel 迁过去;
- ioRatio:I/O 处理与异步任务执行的时间比例可调,默认 50:50。
4.2 Channel 的继承体系:每层抽象都有明确职责
flowchart TD
A["Channel 接口<br/>id/eventLoop/pipeline/config"] --> B["AbstractChannel<br/>模板方法:bind/connect/read/write<br/>持有 Unsafe 和 Pipeline"]
B --> C["AbstractNioChannel<br/>对接 NIO:SelectionKey<br/>interestOps/readInterestOp"]
C --> D["AbstractNioByteChannel<br/>字节流读写(TCP 数据通道)"]
C --> E["AbstractNioMessageChannel<br/>消息读写(接收连接用)"]
D --> F["NioSocketChannel<br/>客户端连接通道<br/>doRead/doWrite 具体实现"]
E --> G["NioServerSocketChannel<br/>服务端监听通道<br/>doReadMessages = accept 新连接"]
读源码时最值得体会的设计:I/O 操作全部收敛到内部类 Unsafe 里,把"Netty 的 Channel API"和"JDK 底层的 Socket 操作"隔离开;doXxx() 抽象方法留给子类实现,AbstractChannel 用模板方法串起流程。书中还点名了 Netty 的性能优化手段:JDK 的 Unsafe、乐观锁、对象池等。
4.3 ByteBuf:比 NIO ByteBuffer 好用得多的缓冲区
JDK 的 ByteBuffer 只有一个 position 指针,读写模式要 flip() 来回切换,极易出错。Netty 的 ByteBuf 用读写分离的双指针:
flowchart LR
subgraph ByteBuf["ByteBuf 内部结构"]
A["[0, readerIndex)<br/>已读区域(可丢弃)"] --> B["[readerIndex, writerIndex)<br/>可读区域 readableBytes"]
B --> C["[writerIndex, capacity)<br/>可写区域 writableBytes"]
end
继承体系(每层干一件事):
AbstractByteBuf:读写索引维护、边界检查;AbstractReferenceCountedByteBuf:引用计数(retain()/release()),配合ReferenceCountUpdater用 CAS 乐观锁更新计数;CompositeByteBuf:多个 Buffer 的逻辑组合(零拷贝场景 2);PooledByteBuf:池化缓冲区,用完归还对象池复用,是第 6 章内存管理的入口。
4.4 内存泄漏检测:弱引用 + 采样的巧思
堆外内存不归 GC 管,忘了 release() 就泄漏。Netty 的 ResourceLeakDetector 机制:
- 每个 ByteBuf 创建时(按级别采样,不是全量——开销可控)包装成弱引用/虚引用,记录创建时的调用栈;
- ByteBuf 被 GC 时,若引用计数没归零,说明泄漏了;
- 从引用队列中捞出泄漏记录,打印带调用栈的告警日志。
这个机制本身就是一节生动的 JVM 引用类型教学课——书中还配了弱引用的 JUnit 测试用例帮助理解。
五、读/写请求源码剖析:一次 I/O 的完整旅程(第 5 章)
5.1 ServerBootstrap 启动过程
bind() 背后是:反射创建 NioServerSocketChannel → 初始化 Pipeline(插入 ServerBootstrapAcceptor)→ 注册到 Boss EventLoop → 绑定端口 → 监听 OP_ACCEPT。
5.2 接入与读写:两个 Unsafe 的分工
sequenceDiagram
participant Client as 客户端
participant Boss as Boss线程(NioMessageUnsafe)
participant Acceptor as ServerBootstrapAcceptor
participant Worker as Worker线程(NioByteUnsafe)
participant Handler as 业务Handler链
Client->>Boss: 发起 TCP 连接
Boss->>Boss: 监听到 OP_ACCEPT<br/>accept 出 SocketChannel
Boss->>Acceptor: fireChannelRead(新连接)
Acceptor->>Worker: 设置 options/attrs<br/>注册到 Worker 的 Selector
Client->>Worker: 发送数据
Worker->>Worker: 监听到 OP_READ<br/>分配 ByteBuf 循环读
Worker->>Handler: fireChannelRead(msg)<br/>沿 Pipeline 传播
- Boss 侧用 NioMessageUnsafe(读出来的是"一条消息"= 一个新连接);Worker 侧用 NioByteUnsafe(读出来的是字节流)——这就是第 4 章两个 AbstractNio 子类分工的实际体现;
- OP_READ 是循环读:一次事件里反复读直到读空,每轮用
RecvByteBufAllocator预测该分配多大的缓冲区(第 6 章细讲); - 写操作先进 ChannelOutboundBuffer 链表(Entry 节点用完回收进对象池),
flush0()时交给NioSocketChannel.doWrite()批量写(合并成NIO_BUFFERS数组一次 writev),写不完(TCP 缓冲区满)则注册OP_WRITE等可写时再续写——这就是写半包的处理。
六、内存管理:jemalloc 思想在 Java 的落地(第 6 章,最难也最值)
为什么 Netty 要自己管内存?堆外内存的 malloc/free 系统调用很贵,频繁申请释放是性能杀手。Netty 把 jemalloc 的分配思想搬到了 Java 里,三层结构:
flowchart TD
subgraph 三层结构["内存分配的三层结构"]
A["① PoolThreadCache<br/>本地线程缓存<br/>每个 I/O 线程一个,免锁"]
B["② PoolArena<br/>分配区(arena)<br/>管理一组 PoolChunk"]
C["③ 系统内存<br/>堆外/堆内直接申请"]
end
A -->|"缓存命中就直接用<br/>不命中向下申请"| B
B -->|"没有可用 chunk 才申请"| C
D["PoolChunk(默认 16MB)<br/>= 2048 个 page(8KB)"] -.属于.- B
E["PoolSubpage<br/>把 page 再切成 tiny/small 小块"] -.切分.- D
分配策略(申请一块内存时):
- 先查当前线程的
PoolThreadCache,命中直接返回——无线程竞争,无锁; - 不命中则找
PoolArena分配:大于等于 8KB 走PoolChunk按 page 分配;小于 8KB 走PoolSubpage把一个 page 切成更小的块(tiny/small 规格); - Arena 也分不到,才向系统申请新 chunk(默认 16MB),用完放回 Arena 重复利用;
- 超大内存(超过 chunk 大小)不池化,直接申请、用完直接回收。
PoolChunk 内部用完全二叉树管理 2048 个 page:分配 ≥8KB 时在树上找深度合适的连续节点;PoolSubpage 用位图(bitmap)记录小块占用。PoolArena 里维护按使用率分档的 chunk 链表(qInit/q000/q025/q050/q075/q100),让分配尽量集中在少数 chunk 上,提高内存利用率。
最后一块拼图是 RecvByteBufAllocator(配合第 5 章的 OP_READ 循环读):AdaptiveRecvByteBufAllocator 会自适应预测——这次实际读了多少,下次就按这个量级分配缓冲区(默认从 1KB 起步,在 64B ~ 64KB 之间动态调整),避免"每次固定分配 64KB 却只读到 16B"的浪费。
学习建议(书中原话):这部分涉及大量多线程并发和位移与或非操作,把难以理解的代码贴到测试类里单独调试是最有效的办法。
七、时间轮高级应用:从心跳检测到 10 亿级任务监控(第 7 章)
7.1 HashedWheelTimer 的原理
IM 系统里有成千上万甚至百万条长连接链路,Netty 自带的心跳检测是给每条链路创建 ScheduledFutureTask 定时任务——链路多了定时任务就爆炸。时间轮(HashedWheelTimer)是另一种方案:只用一条额外的 Worker 线程,像钟表一样每隔一个 tick 走一格,只把当前格子的任务捞出来执行,没到的根本不碰。
flowchart TD
subgraph 时间轮["时间轮(wheelSize 个刻度)"]
direction LR
T0["刻度 0<br/>task 链表"] --- T1["刻度 1<br/>task 链表"] --- T2["..."] --- TN["刻度 N-1<br/>task 链表"]
end
P["指针(tick)<br/>每 tickDuration 走一格"] --> T0
W["Worker 线程<br/>tick++<br/>落格 = tick & (wheelSize-1)"] -.驱动.- P
先思考三个问题(书中的引导方式很好):
- 指针走一轮是多久? tickDuration × wheelSize;
- 用什么容器存任务? 每个刻度(bucket)一条链表;
- 任务时间晚于一轮终点放哪? 照样按落格公式放进去,任务上记录 round 数(还要等几圈),指针扫到它时 round 减 1,减到 0 才真正执行。
7.2~7.4 实战:外卖订单超时监控系统
书中设计的应用场景非常接地气:外卖平台每天上千万订单,用户下单后在规定时间内无人处理就要告警(超时要被投诉、要赔偿)。类似的还有优惠券过期提醒、医院预约提醒——凡是"与未来一段时间相关"的业务都能用。
但 10 亿级任务放内存里必然溢出,于是引出改造方案:
flowchart LR
A["业务系统<br/>(外卖订单等)"] -->|"上报监控数据"| K["Kafka 数据源<br/>(处理能力弱时数据留在 Kafka,削峰)"]
K --> B["时间轮计算服务<br/>(分布式部署,每分钟百万级吞吐)"]
B <--> R["Redis<br/>任务数据按 时间轮节点wheel + 刻度idx 存储"]
B -->|"到期且未处理(isOver=0)"| C["告警输出<br/>回调 callBackUrl / 通知相关人员"]
设计要点:
- 数据源选 Kafka:数据量大、要监控"是否被及时处理",Kafka 天然削峰;
- 任务数据外置 Redis:内存里只放时间轮的调度结构,任务内容放 Redis,用"时间轮节点名称 wheel + 刻度编号 idx"做 key 映射,还能直接从 Redis 捞数据做问题定位;
- 监控数据通用化设计:
dataId(唯一标识)、sysCode(系统来源)、detectionTime(检测时间)、isOver(是否结束,相当于任务取消)、isCycle/cycleTime(支持周期性调度监控)、callBackUrl(到期回调)——这套字段设计可以直接抄到自己的项目里。
八、问题分析与性能调优(第 8 章)
8.1 Linux 部署
把第 3 章的 RPC 服务部署到 Linux:分环境配置文件(server.properties:服务名、端口、Zookeeper 地址、权重,开发/测试/生产各一份)+ PropertyUtil 工具类加载 + shell 启动脚本。
8.2 JMeter 秒杀压测
普通 JMeter TCP 脚本只能压一台服务器,也不用为此加 Nginx/LVS——书中选择的方式是把 RPC 客户端打成 jar 包嵌入 JMeter(实现 AbstractJavaSamplerClient),直接模拟长连接高并发调用分布式服务,观察 TPS、并发用户数、平均响应耗时。
8.3 常见生产问题排查(实战中真能救命的套路)
| 症状 | 排查方法 |
|---|---|
| 内存溢出一定会出现(跑一段时间就炸) | jmap -histo 排序观察,看哪些对象增长快且不释放 |
| 内存溢出偶尔出现(很久才发生) | 启动参数加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/xxx.hprof,溢出时自动快照,用 Eclipse MAT 分析 |
| 无溢出但响应慢、TPS 极低 | top -H -p pid 找到 CPU 最高的线程 id → 转 16 进制小写 → jstack -l pid | grep '线程id' 定位运行栈 |
8.4 性能调优三大方向
- Linux 系统参数:压测常见的"文件句柄数过多"——全局改
/etc/sysctl.conf加fs.file-max=1000000(支持百万连接),sysctl -p生效;局部改/etc/security/limits.conf加* hard/soft nofile 1000000; - TCP 参数:缓冲区大小、 backlog 队列等内核参数调整;
- Netty 应用层:理解第 6 章后大胆用内存池减少堆外分配次数;用第 7 章的时间轮替换默认心跳检测,减轻 I/O 线程负担。
书中一个坦诚的结论值得记住:无复杂业务逻辑时,Netty 不调优性能也已经很好——调优的价值在于真实业务(数据库操作、远程调用混入 Service 层)出现瓶颈时,你知道从哪里下手。
九、全书知识地图与结语
把全书浓缩成一张图:
mindmap
root((Netty))
应用层
长连接通信(第1章)
分布式RPC(第3章 协议+反射+ZK)
时间轮(第7章 心跳/10亿级任务监控)
原理层
多路复用器 epoll/select
Reactor线程模型 Boss/Worker
编解码与粘包拆包
序列化 Protobuf/Kryo
零拷贝
背压 TCP窗口/Flink
源码层
NioEventLoop run()循环
Channel继承体系/Unsafe
ByteBuf 双指针/引用计数
内存泄漏检测 弱引用+采样
读写流程 accept/read/write半包
内存层
PoolThreadCache 线程缓存
PoolArena/PoolChunk 16MB
PoolSubpage 小块位图
AdaptiveRecvByteBuf 自适应预测
运维层
Linux部署与句柄调优
JMeter长连接压测
jmap/MAT/jstack排查三板斧
最后给新人的学习路径建议(也是作者的阅读建议):先跑通第 1 章的长连接示例和第 3 章的 RPC,建立"会用"的信心;再反复啃第 4~6 章源码(作者建议源码至少读 3 遍,直到对 Channel、Handler、NioEventLoop、ByteBuf 的各个方法都了如指掌);最后用第 7、8 章的实战项目把知识串起来。Netty 涉及多线程、复杂数据结构与内存管理模型,读完它对并发编程能力的提升是肉眼可见的。