《Netty 高性能网络编程》前言:读法、环境与约定

交代本书的读者对象与前置知识,列出 Temurin 25.0.5+8、Maven 3.9.12、Netty 4.2.19.Final 的环境清单和 Apple M1 Pro 机器口径,说明 linkd-v2 的四道门禁、端口 9090/9091/9092/9093、代码与输出约定,以及每节内容的验证步骤和常见误解。

这本书写给谁

这本书写给三类读者。

第一类:读完卷一,想把 linkd 换成 Netty 的人。 你已经知道 Selector 怎么等就绪事件、ByteBuffer 的 flip/compact 怎么用、HTTP/1.1 的消息边界在哪里。现在你想知道:这些手工步骤在 Netty 里分别由谁负责,哪些坑框架替你挡了,哪些坑框架只是换了个形式。

第二类:正在维护一个 Netty 服务的人。 你能照着示例写出 ServerBootstrap 和几个 handler,但遇到引用计数泄漏、连接数不降、写缓冲积压、EventLoop 被阻塞时,缺少一套从现象到根因的排查路径。这本书的第 9、13、14 章就是为这类问题准备的。

第三类:要做协议网关或长连接服务的人。 你需要同时处理 HTTP/1.1、WebSocket、h2c、gRPC 和自定义二进制协议,还要考虑重连、连接池、限流、优雅关闭。第 8、10、11、15 章会把这些能力放进同一个 linkd-v2 工程,而不是各写一个互不相干的 demo。

如果你完全没有写过 Socket 或 NIO,建议先读卷一。卷二的很多「为什么不能这样写」,默认你知道阻塞、非阻塞、半包、粘包分别是什么。

前置知识与卷一映射

下表列出本卷直接依赖的卷一内容。没有读过的部分,至少把对应的概念补上,否则代码能跑,结论理解不了。

卷一内容本卷用到的地方需要带走的概念
第 3 章 Socket 与超时第 2 章 Bootstrap、第 8 章客户端连接、读、写超时的边界不同
第 4 章字节序与成帧第 6 章编解码器、第 7 章协议帧定长、分隔符、长度域三种成帧
第 5 章 ByteBuffer 状态第 5 章 ByteBuf读写索引、视图、直接缓冲区的成本
第 6 章 Reactor第 3 章 EventLoop单线程事件循环、任务队列、线程归属
第 7 章非阻塞读写第 4 章 Pipeline、第 9 章背压半包、写队列、OP_WRITE
第 8 章 HTTP/1.1第 10、15 章keep-alive、chunked、响应顺序
第 9 章 HTTP/2 帧第 11 章 h2c帧、流、多路复用、流量控制
第 11 章 WebSocket第 10 章握手、帧、分片、心跳
第 14 章水位与优雅关闭第 9、15 章高/低水位、排空、关闭期限

不需要提前会 Netty,也不需要会 gRPC。第 11 章之前不会出现 gRPC API;如果你跳过前面直接看第 11 章,至少先读第 4 章的 Pipeline 和第 6 章的编解码器。

版本与机器口径

本书的写作与验证环境固定如下。正文里出现的「本机输出」都来自这套环境;换机器、换 JDK 小版本,输出可能不同。

项版本/配置说明
机器Apple M1 Pro,10 核,32 GBApple Silicon,arm64
操作系统macOS 26.3 (25D125)原生传输用 kqueue,不是 epoll
JDKTemurin 25.0.5+8javac --release 25
Maven3.9.12mvn -version 里的 Java 必须是 25
Netty4.2.19.Finalnetty-all 聚合依赖
JUnit Jupiter5.14.4只用于测试片段
gRPC-Java1.76.0仅第 11 章
Protobuf4.33.2仅第 11 章
日志System.getLogger不引 SLF4J 绑定

机器口径统一写成:

Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
Temurin 25.0.5+8,Netty 4.2.19.Final

环境变量与版本核对:

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
Java version: 25.0.5, vendor: Eclipse Adoptium

mvn -version 里的 Java version 不是 25 时,先查 JAVA_HOME 和 PATH,不要继续往下跑。Maven 用的是它启动时看到的 JDK,不是 pom.xml 里写的 release。

linkd-v2 工程结构

全书围绕一个四模块工程推进:

linkd-v2/
├── pom.xml                     # 父 POM,packaging=pom
├── linkd-core/                 # 协议帧、编解码、共享常量
├── linkd-server/               # HTTP、WebSocket、HTTP/2、网关
├── linkd-client/               # 客户端、重连、连接池
└── linkd-app/                  # main、配置、生命周期

父 POM 只锁两个关键属性,模块 POM 不再各写一遍版本:

<properties>
  <netty.version>4.2.19.Final</netty.version>
  <maven.compiler.release>25</maven.compiler.release>
</properties>

groupId 是 com.plume.linkd.v2,版本 0.2.0-SNAPSHOT;linkd-app 依赖另外三个模块,模块之间不循环依赖。入口类为 com.plume.linkd.v2.app.LinkdV2Main。第 1.3 节会把工程搭起来,后面的章节只往里加类,不再改模块划分。

构建命令:

mvn -q -DskipTests package

第一次构建会下载 Netty 及其传递依赖。netty-all 是聚合 POM,jar 很小,真实类在 netty-transport、netty-buffer、netty-codec-http 等模块里,不需要手工下载 jar。

端口与路径

端口是跨章契约,不能在某节里临时换一个:

端口协议/路径用途
9090HTTP/1.1、WebSocket/healthz、/books/{id}、/ws/echo、/ws/broadcast
9091h2c明文 HTTP/2 prior-knowledge,不做 TLS ALPN
9092gRPCLinkdService.Ping
9093自定义二进制协议LinkdFrame,16 字节头 + 负载
8080上游 libraryd由 9090 的网关代理

启动后先确认监听:

lsof -nP -iTCP:9090 -sTCP:LISTEN
lsof -nP -iTCP:9091 -sTCP:LISTEN

macOS 没有 ss,连接状态用 netstat -anv -p tcp 查看;抓包用 tcpdump -i lo0,需要 sudo。这些命令在第 13、15 章会反复出现。

端口被占用时,不要改端口绕过,先找出占用进程。全书所有验证脚本都假设端口固定,临时改端口会让后面的检查对不上。

四道门禁

「代码能编译」不等于「这一节写对了」。本书对每一节的可运行内容设四道门禁,从便宜到贵依次执行。前三道是每节的最低要求,第四道在涉及缓冲区、连接池、背压和关闭的章节必须执行。

门禁命令通过判据失败先看
G1 编译mvn -q -DskipTests compileBUILD SUCCESS,无编译错误版本、包名、API 白名单
G2 单测mvn -q test测试全部通过,失败信息可定位断言、测试替身、端口冲突
G3 冒烟启动 LinkdV2Main 并访问端口/healthz 返回 200 与 ok,9093 能收到 PONG启动日志、端口、handler 顺序
G4 资源开 ADVANCED 泄漏检测跑一轮无 LEAK:,写缓冲回落到低水位,SIGINT 后端口关闭第 5、9、13 章

G1:编译门禁

mvn -q -DskipTests compile

-q 只输出错误和必要信息,脚本里用它可以减少噪音;本地排查时去掉 -q,看完整的模块与依赖。编译失败先确认 maven.compiler.release 是 25,再确认代码里的 Netty 类确实在依赖中。不要用 --add-opens 或反射去绕编译错误,那通常意味着用了非公开 API。

G2:单测门禁

mvn -q test

测试只覆盖纯逻辑:帧头解析、长度校验、路由匹配、重连退避计算、连接池借还状态。涉及真实端口的测试要单独标记,避免和正在运行的服务抢 9090。JUnit Jupiter 5.14.4 只用于测试片段,正文不把测试框架当成运行时依赖。

单测失败时,先看断言值和实际值,不要先改测试。很多失败是「实现改了,测试没改」或「测试假设了固定端口」。

G3:冒烟门禁

启动服务后,至少验证三条链路:

curl -fsS http://127.0.0.1:9090/healthz
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9090/books/7
# 9093:运行第 7 章的客户端,发送 PING 并确认收到 PONG

第一条应返回 ok;第二条在上游 libraryd 可用时返回 200,不可用时返回 502;第三条走自定义协议,由第 7 章的客户端完成。三个结果都要记录,不能只看「进程还活着」。

冒烟阶段还要验证关闭:向进程发送 SIGINT,确认 server channel 停止接收新连接、已建立连接按策略处理、EventLoopGroup 在期限内退出。只 kill -9 不算通过。

G4:资源门禁

java -Dio.netty.leakDetection.level=advanced \
  -cp <classpath> com.plume.linkd.v2.app.LinkdV2Main

ResourceLeakDetector 默认级别是 SIMPLE;排查时临时调到 ADVANCED,跑一轮典型流量,检查日志里是否出现 LEAK:。PARANOID 开销大,只适合短时间定位,不要留在生产配置里。

资源门禁还看两个信号:写缓冲超过高水位后,读是否被暂停并在回落后恢复;连接关闭后,连接数、堆外内存和线程数是否回到基线附近。具体命令和指标口径在第 9、13、14 章,这里只要求「有观察、有记录」。

代码约定

包名。 共享协议在 com.plume.linkd.v2.protocol,服务端在 com.plume.linkd.v2.server,客户端在 com.plume.linkd.v2.client,启动与配置在 com.plume.linkd.v2.app。正文提到类时写全包名或写清所属模块,避免跨节复制时找不到。

类型。 所有 public 类必须是顶层类,或 public static 嵌套类;一个代码块里不要放两个 public 顶层类型。共享类型的签名以 BRIEF 的契约为准,不在某一节里单独改名。

代码块语言。 Java 标 java,命令行标 bash,终端输出标 text,pom.xml 标 xml,配置标 yaml 或 properties,JSON 标 json。没有语言标记的围栏在构建检查里会被判失败。

省略。 代码块里的 ... 表示省略,不是可编译语法。完整示例会写全;片段会说明缺了哪些部分。异常栈只保留关键帧,用 ... 省略框架内部调用。

输出。 「本机输出」表示在写作环境真实执行过;「示意输出」表示为了说明结构而简化,不能当成实测结果。单次运行会标注「单次运行,未做统计」。性能数字必须附可复现命令和机器口径。

日志。 统一用 System.getLogger,不引 SLF4J 绑定。Netty 内部日志走 JDK 日志;需要看它的时候,调 java.util.logging 的级别,而不是加一个日志实现。

异步。 EventLoop 线程里不调用 sync() 或 await()。需要知道写结果时用监听器:

ChannelFuture future = channel.writeAndFlush(frame);
future.addListener((ChannelFutureListener) f -> {
    if (!f.isSuccess()) {
        LOG.log(System.Logger.Level.WARNING, "write failed", f.cause());
    }
});

这段代码的重点不是语法,而是线程:监听器在 future 完成时执行,不阻塞 EventLoop。ctx.writeAndFlush() 从当前 handler 往前找出站 handler,ctx.channel().writeAndFlush() 从 tail 开始;两者的差异在第 4 章展开。

API。 只使用公开 API。遇到不在白名单里的类,先确认它属于哪个 netty-* 模块,再决定是否引入;不靠反射访问私有字段,也不把 ChannelOption 当字符串传。

怎么验证一节的内容

每一节的代码按下面五步验证,顺序不要颠倒。

  1. 对齐版本。 java -version、javac -version、mvn -version 三者一致,pom.xml 的 release 是 25。
  2. 编译。 完整类用 mvn -q -DskipTests compile;单文件片段用 javac --release 25 -cp <netty-jars> File.java。
  3. 运行。 启动 LinkdV2Main 或该节的测试入口,确认端口监听、日志没有异常。
  4. 观察。 用 curl、Python 客户端、lsof、netstat 或 JFR 观察行为;不要只看「没有报错」。
  5. 记录。 把命令、输出、机器口径和结论写下来。没有记录的「验证过」等于没验证。

连接诊断的常用命令:

# 谁在监听 9090
lsof -nP -iTCP:9090 -sTCP:LISTEN

# 连接状态(macOS;没有 ss)
netstat -anv -p tcp | rg '\.9090 '

# 回环抓包,需要 sudo
sudo tcpdump -i lo0 -nn 'tcp port 9090'

验证记录模板:

日期:
机器:Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
版本:Temurin 25.0.5+8,Netty 4.2.19.Final
命令:
输出:
结论:通过 / 不通过(原因)

如果输出和书里不一致,先按「版本 → 端口 → 依赖 → 代码」的顺序排查,不要先怀疑结论。版本不一致时,很多「bug」只是行为差异;端口不一致时,很多「连接失败」只是连到了旧进程。

三条阅读路径(简版)

完整版在目录篇,这里先给结论:

路径适合的人主线大约投入
协议实现线要写客户端/服务端协议的人第 1–8 章,落到 9093 自定义协议6–7 周
性能排查线正在维护长连接服务的人第 2–5、9、12–14 章5–6 周
生产网关线要做接入层、网关的人第 1–2、4、8–11、15 章6–8 周

三条路径都假设你读过卷一的第 6、7 章。如果没读,至少先补「单线程 Reactor」和「非阻塞读写」两节,否则第 3、4 章会缺少参照。

常见误解

「Netty 一定比 JDK NIO 快。」 不一定。Netty 在大量连接、频繁分配、协议组合的场景下优势明显;在 handler 阻塞、缓冲区持有过久、水位不处理的场景下,它一样会慢,甚至更慢。比较要在同一负载、同一客户端、同一机器上做,指标看吞吐、p50/p99、GC 和 RSS,不能只看一个 QPS。

「netty-all 是一个大 jar,里面什么都有。」 它是聚合 POM,jar 很小,真实类分布在多个模块里,由 Maven 传递依赖。不要手写下载命令把它当成运行时依赖。

「macOS 上也能用 epoll。」 不能。macOS 用 kqueue,Epoll.isAvailable() 返回 false 是正常现象。代码要按平台选择,并在构件缺失时回退 NioEventLoopGroup。

「SimpleChannelInboundHandler 会自动释放所有消息。」 它默认释放入站消息,但如果你覆写 autoRelease 为 false、把消息传给下一个 handler,或者自己 readRetainedSlice(),释放责任就转移了。第 5 章会逐条说明。

「EventLoop 里等一小会儿没关系。」 有关系。EventLoop 是单线程执行器,一条连接上的等待会挡住同一个 EventLoop 上的所有连接。业务阻塞要移到业务线程池或虚拟线程,不能靠「只等 5 毫秒」自我安慰。

小结

  • 本书面向读完卷一、正在维护 Netty 服务、或要做协议网关的读者;前置是 Socket、成帧、Reactor、HTTP/1.1 与 WebSocket。
  • 版本口径固定:Temurin 25.0.5+8、Maven 3.9.12、Netty 4.2.19.Final;机器为 Apple M1 Pro、macOS 26.3。
  • 工程是四模块的 linkd-v2,端口 9090/9091/9092/9093,上游 libraryd 在 8080。
  • 四道门禁是编译、单测、冒烟、资源;前两道每节必过,后两道在涉及资源和关闭的章节必过。
  • 代码块标语言,输出区分「本机」与「示意」,EventLoop 里不阻塞、不 sync()。
  • 验证要留下命令、输出、机器口径和结论;没有记录就不算通过。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发