从卷一的 linkd 说起
卷一《Java 网络编程基础》的最后,linkd 已经能同时处理多条连接:一个 Selector 循环负责 accept 和读写,Connection 持有写队列,Handler 处理协议,HTTP/1.1 的 keep-alive、chunked、路由都能跑,WebSocket 也完成过握手、帧解析和广播。那一卷刻意没有引入任何网络框架,目的是让「连接、缓冲区、事件、协议边界」这些词各自对应到一段你自己写过的代码。
这套代码在本机实验里是成立的。把它放到真实服务里,问题会从另一个方向冒出来:
- 一个 handler 里调用了会阻塞的远程接口,整个事件循环跟着停,所有连接的心跳一起超时。
- 写队列没有上限,客户端读得慢,内存一路涨,最后只能靠重启恢复。
- 半包、粘包、协议升级、连接复用分散在多个 handler 里,新增一种协议就要改一遍事件分发。
- 缓冲区借出和归还靠人记,某条异常路径少还一次,堆外内存慢慢泄漏,进程看起来还活着。
- 出问题以后只能打印日志,不知道是线程卡住、缓冲区积压,还是对端根本没读。
这些不是「会不会用 Selector」的问题,而是「一条连接从建立到关闭,它的状态、内存和线程归谁管」的问题。Netty 主要解决的正是这一类问题。
手写结构与 Netty 组件的对应
先给一张对照表。它不是一一映射,但能帮你把卷一的知识迁移过去,而不是从零背一套新名词。
| 卷一里的东西 | Netty 里的对应物 | 迁移时要重新理解的点 |
|---|---|---|
Selector 循环与任务队列 | EventLoop、EventLoopGroup | 一个 Channel 终身绑定一个 EventLoop,线程归属从「约定」变成「结构」 |
Connection | Channel | 读写、关闭、元数据都挂在通道上,状态不再由应用自己拼 |
Handler 与事件分发 | ChannelPipeline、ChannelHandler | 入站从 head 到 tail,出站从 tail 到 head,顺序由管线决定 |
ByteBuffer 与 BufferPool | ByteBuf、ByteBufAllocator | 读写索引分离,引用计数决定何时真正回收 |
HttpParser、ChunkedCodec | HttpServerCodec、HttpObjectAggregator | 编解码器可以组合,消息边界由 codec 负责 |
outbound 队列与 OP_WRITE | 写缓冲、WriteBufferWaterMark、Channel.isWritable() | 背压有了可观察的信号,不再只看队列长度 |
close() 与 close hook | ChannelFuture、ChannelFutureListener | 关闭是异步的,结果通过 future 通知 |
这张表最重要的不是类名,而是职责边界。卷一里,Connection 同时管通道、缓冲池、写队列和关闭回调;Netty 把这些拆成 Channel、ByteBufAllocator、写缓冲和 ChannelFuture。拆开以后,每一层都能单独替换、单独观测,也更容易在错误的地方暴露错误。
Netty 快在哪里,以及它的适用边界
「Netty 高性能」这句话经常被当成结论使用。更准确的说法是:Netty 把一些在大量连接下容易做错的工程动作做成了默认路径,但每一条默认路径都有适用条件。
| 设计 | 正常情况下带来的收益 | 失效或变慢的条件 |
|---|---|---|
EventLoop 单线程事件循环 | 避免一连接一线程,减少上下文切换 | handler 里做阻塞 IO、长事务、大文件读写 |
池化 ByteBuf | 降低频繁分配与回收的成本 | 分配尺寸极端、持有时间过长、引用计数出错 |
ChannelPipeline | 协议逻辑可组合、可复用 | 管线过长、handler 有状态却被标记 @Sharable |
| 原生传输 | Linux 上用 epoll、macOS 上用 kqueue,减少 JDK 选择器的开销 | 平台不匹配、构件缺失,仍要回退 NIO |
| 写缓冲与水位线 | 把「对端读得慢」变成可观测的背压信号 | 只设水位不处理 channelWritabilityChanged,积压依旧 |
所以本书不会写「Netty 一定比 JDK NIO 快」。它只回答:在哪个场景、用哪条路径、观察哪个指标、和什么基线比。第 14 章会给可复现的压测命令和指标口径;没有实际执行过的数字,不会写成「实测」。
为什么先手写 NIO,再学 Netty
如果一上来就学 Netty,ChannelPipeline、ByteBuf 的引用计数、EventLoop 的线程归属都很容易变成「框架规定」。背下来能用,但一旦出现下面这些现象,就不知道该查哪里:
- 日志里出现
LEAK: ByteBuf.release() was not called before it's garbage-collected。 - 调用
sync()以后线程不动了,堆栈停在等待 future 完成的调用上。 - 自定义解码器偶尔丢半包,或者把两条消息粘成一条。
- 服务压测时 QPS 上不去,但 CPU 并不高。
- 客户端断开后,连接数没有下降,堆外内存持续增长。
这些问题在卷一都有对应的「手工版本」:半包对应累积缓冲,写队列对应 outbound,关闭对应 close hook,线程归属对应「register 必须在事件循环线程」。你亲手处理过这些边界,再看 Netty 的封装,才知道它替你省掉了哪一步,以及这一步出错时是什么样子。
卷一第 6 章的单线程 Reactor、第 7 章的非阻塞读写、第 8 章的 keep-alive 与 chunked、第 9 章的 HTTP/2 帧、第 11 章的 WebSocket 帧、第 14 章的水位与优雅关闭,都是本卷的直接前置。没有这些,本卷的很多「为什么不能这样写」会缺少参照。
三卷本的位置
「Java 网络与 Netty」是一个三卷本,三卷回答的问题不同:
| 卷 | 书名 | 回答的问题 | 主要产出 |
|---|---|---|---|
| 一 | 《Java 网络编程基础》 | 不用框架,一条连接从字节到协议要经过什么 | linkd v1:JDK 原生 NIO 连接服务器 |
| 二 | 《Netty 高性能网络编程》 | 用 Netty 重写上述能力,并处理引用计数、池化、背压、原生传输与网关 | linkd-v2:可验收的 Netty 服务 |
| 三 | 《Java 服务端通信工程》 | 多服务之间怎么调用、发现、超时、重试、观测和发布 | gRPC 生态与服务治理实践 |
三卷可以按顺序读,也可以按需跳。只读卷二也能跑通代码,但遇到「为什么这里要手动释放」「为什么不能在 EventLoop 里等待」时,会缺少卷一那层手工经验。卷三不重复 Netty 的基础用法,它关注的是服务之间的契约。
卷二不是 Netty API 字典。API 速查放在附录 A;正文的每一节都围绕 linkd-v2 的一个真实类、方法或运行场景展开。你学到的不是「Netty 有 20 个 handler」,而是「这条连接在这个阶段应该挂哪些 handler,为什么」。
为什么锁定 Netty 4.2.19.Final 与 JDK 25
本书的版本口径固定如下,正文不自行漂移:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | Temurin 25.0.5+8 | javac --release 25,语言特性按 25 写 |
| Maven | 3.9.12 | 构建与依赖管理 |
| Netty | 4.2.19.Final | io.netty:netty-all 或按模块引入 |
| JUnit Jupiter | 5.14.4 | 只用于测试片段 |
| gRPC-Java | 1.76.0 | 仅第 11 章 |
| Protobuf | 4.33.2 | 仅第 11 章 |
| 日志 | System.getLogger | Netty 内部日志走 JDK 日志,不引 SLF4J 绑定 |
有两点需要先说明,否则很容易在工程里踩空。
第一,io.netty:netty-all:4.2.19.Final 是聚合 POM,jar 本身很小,真正的类分布在 netty-transport、netty-buffer、netty-codec-http 等模块里。Maven 会传递依赖,不需要手工下载一堆 jar。父 POM 里只锁一个属性:
<properties>
<netty.version>4.2.19.Final</netty.version>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
第二,原生传输是平台相关的。NioEventLoopGroup 在所有平台可用;EpollEventLoopGroup 只在 Linux 且 Epoll.isAvailable() 为 true 时可用;KQueueEventLoopGroup 只在 macOS/BSD 且 KQueue.isAvailable() 为 true 时可用。macOS 没有 epoll,Epoll.isAvailable() 返回 false 是正常现象。Apple Silicon 的 kqueue 构件 classifier 是 osx-aarch_64,Intel Mac 是 osx-x86_64;构件缺失时,代码必须回退到 NioEventLoopGroup,不能假设原生传输一定存在。
环境准备与版本核对:
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
java -version
javac -version
mvn -version
版本核对应看到(节选,具体构建号以你的安装为准):
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
javac 25.0.5
Apache Maven 3.9.12
JDK 25 带来的虚拟线程对本书有两层意义。第一,业务侧的阻塞调用可以放到虚拟线程里,不必为每个请求占一个平台线程;第二,这不等于可以把阻塞调用放回 EventLoop。EventLoop 仍然是平台线程上的事件分发器,虚拟线程不能替代它。第 3 章会把这条边界讲清楚。
全书主线:linkd-v2
linkd-v2 是一个四模块 Maven 工程,groupId 为 com.plume.linkd.v2,版本 0.2.0-SNAPSHOT,入口是 com.plume.linkd.v2.app.LinkdV2Main:
linkd-v2/
├── pom.xml # 父 POM,packaging=pom
├── linkd-core/ # 协议帧、编解码、共享常量
├── linkd-server/ # HTTP、WebSocket、HTTP/2、网关
├── linkd-client/ # 客户端、重连、连接池
└── linkd-app/ # main、配置、生命周期
模块之间不循环依赖:linkd-app 依赖另外三个模块,linkd-server 和 linkd-client 依赖 linkd-core。这个结构从第 1 章搭起来,后面每一章往里加能力,不在中途换工程。
端口约定固定如下,全书不随意改:
| 端口 | 协议/用途 | 首个完整实现 |
|---|---|---|
| 9090 | HTTP/1.1:/healthz、/books/{id}、/ws/echo、/ws/broadcast | 第 2 章起,第 10、15 章扩展 |
| 9091 | h2c(HTTP/2 明文 prior-knowledge) | 第 11 章 |
| 9092 | gRPC:LinkdService.Ping | 第 11 章 |
| 9093 | 自定义二进制协议:LinkdFrame | 第 7 章 |
| 8080 | 上游 libraryd,由网关代理 | 第 15 章 |
自定义协议是 16 字节头加负载:magic 0x4C4B4446(ASCII LKDF)、version 1、type、flags、requestId、length。帧类型为 HELLO=1、HELLO_ACK=2、PING=3、PONG=4、REQUEST=5、RESPONSE=6、ERROR=7。第 7 章会先把这个帧写对,再谈心跳和认证。
读完之后你能交付什么
卷二的目标不是「看完 50 篇」,而是拿到一个能启动、能观测、能验收的 linkd-v2。完成全书后,下面这些场景应该能按顺序跑通:
# 1. HTTP 健康检查
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9090/healthz
# 2. 代理到 libraryd;上游不可达时应返回 502
curl -s http://127.0.0.1:9090/books/7
# 3. 查看监听端口
lsof -nP -iTCP:9090 -sTCP:LISTEN
验收目标(示意,不是本机实测输出):前两条命令应分别得到 200 与 ok,第三条应看到一行带 LISTEN 的记录。版本号以启动日志和 POM 为准,不要靠猜进程。
更完整的验收清单在 15.3:
- HTTP/1.1 的
/healthz返回200与ok,/books/{id}能代理到127.0.0.1:8080,上游不可达时返回502并说明是代理失败。 - WebSocket 的
/ws/echo回显文本帧,/ws/broadcast把文本广播给所有连接。 - h2c 在 9091 上完成明文 prior-knowledge 通信;TLS/ALPN 只讲边界,不作为本卷主线。
- gRPC 在 9092 上提供
LinkdService.Ping,并说明 gRPC-Java 自带的 Netty 传输与版本边界。 - 自定义协议在 9093 上完成
HELLO/HELLO_ACK、PING/PONG与一次REQUEST/RESPONSE。 - 客户端具备重连与连接池,连接关闭、半关闭、回收的语义有测试或可复现步骤。
- 写缓冲超过高水位时停止继续读,回落后恢复;
AUTO_READ的关闭与恢复有明确的所有者。 ResourceLeakDetector在ADVANCED级别下跑一轮典型流量,不出现LEAK:;出现时能按第 13 章的方法定位。- 收到
SIGINT后停止接收新连接,在期限内关闭 server channel 与EventLoopGroup,不留下悬挂线程。
五十篇怎么排
全书 50 篇 = 前置 3 篇 + 15 章 × 3 节 + 附录 2 篇。十五章按依赖分成五组:
| 组 | 章 | 主题 | 阶段产出 |
|---|---|---|---|
| 一、地基 | 1–3 | 架构定位、Bootstrap、EventLoop | 能在 9090 启动一个 Echo 服务 |
| 二、数据通道 | 4–6 | Pipeline、ByteBuf、编解码器 | 一条连接能正确收发自定义帧 |
| 三、协议与连接 | 7–9 | 自定义协议、心跳、连接池、背压 | 9093 上有状态、有背压的长连接 |
| 四、应用协议 | 10–12 | WebSocket、HTTP/2、gRPC、原生传输 | 9090/9091/9092 三套协议并存 |
| 五、生产收口 | 13–15 | 泄漏检测、压测调优、网关验收 | linkd-v2 通过 15.3 的清单 |
这个顺序不是唯一的阅读顺序,但它是最少返工的顺序。比如第 9 章的背压依赖第 5 章对 ByteBuf 和写缓冲的理解;第 15 章的网关又依赖第 8 章的连接池和第 10 章的会话表。目录篇会给出三条按目标拆分的路径,允许跳章,但会标出每一条路径跳过了哪些前置。
每一章的首节负责建立该章的概念边界,后两节负责把它落到 linkd-v2。如果你时间有限,至少读每章的首节和该章最后一节的「常见错误」;中间的代码可以按需回看。
一个判断标准:先问所有权
本卷反复出现的问题,最后都能归到一个问句:这件事、这块内存、这个线程,归谁所有?
- 入站
ByteBuf归谁释放:ByteToMessageDecoder的父类、SimpleChannelInboundHandler,还是把它传给下一个 handler 的你? Channel归哪个EventLoop:注册之后能不能换?跨线程写要不要execute?- 写缓冲积压时,是继续读、停止读,还是断开连接?谁负责恢复?
- 连接从池里借出后,正常归还、异常归还、超时归还分别走哪条路径?
- 关闭由谁发起:对端、空闲检测、业务超时,还是进程收到
SIGINT?关闭后还有没有未完成的 future?
能回答这些问题,API 的名字就不重要了;答不上来,记住再多类名也会在异常路径上出错。后面的章节会反复用「所有权」这个词,原因就在这里。
这本书不写什么
先把边界说清楚,避免不必要的期待。
不写「Netty 一定更快」。 性能只在具体场景、具体基线、具体指标下有意义。第 14 章给方法和命令,不给脱离环境的排名。
不写完整的 TLS 与 ALPN 实现。 第 11 章只说明 HTTP/2 与 TLS 的边界;证书、握手状态机、会话复用属于卷一第 10 章和卷三的部署话题。
不在第 11 章之前使用 gRPC API。 前十章只讲 Netty 自身的传输、编解码和连接管理;gRPC 是一种上层协议,不是 Netty 的同义词。
不把 Linux 的 epoll 写成 macOS 的本机实测。 macOS 用 kqueue;Linux 章节只写部署侧配置与验证命令。
不写未执行的压测、抓包和 JFR 数字。 单次运行会标注「单次运行,未做统计」;示意输出会明确写「示意」。
不使用 Netty 非公开 API。 内部实现包、反射访问 Cleaner 或 PlatformDependent 私有字段都不在本书范围。需要白名单外的类时,会先确认它属于哪个 netty-* 模块。
小结
- 卷一让你亲手处理连接、缓冲区、事件和协议边界;卷二用 Netty 重写这些能力,并补上生产环境的状态与资源管理。
EventLoop、Channel、Pipeline、ByteBuf与卷一的手写结构有对应关系,但职责边界被拆得更细。- Netty 的收益有适用条件;handler 阻塞、引用计数错误、水位不处理,都会让「高性能」失效。
- 全书版本固定为 JDK 25(Temurin 25.0.5+8)、Maven 3.9.12、Netty 4.2.19.Final;原生传输按平台选择并允许回退。
- 主线是四模块的
linkd-v2,端口 9090/9091/9092/9093 与上游 8080 固定,最终以 15.3 的验收清单收口。 - 本书只写可复现的命令、可解释的输出和明确的边界,不写未实测的性能结论。
接下来是前言:把读者对象、机器口径、四道门禁、代码约定和验证步骤展开成一份可以照着执行的环境清单。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。