这本书写给谁
本书是《Java 网络编程基础》三卷本的第一卷,写给已经能写 Java、但缺少网络编程完整链路训练的开发者。
典型情况有三类。
第一类是做业务开发,日常用 Spring Boot 写接口,能配置连接池和线程池,但没有亲手处理过 Socket、Selector、半包和写队列。遇到连接堆积、响应截断、发布时连接重置,只能靠搜索和试参数。
第二类是要做中间件、网关、RPC 或游戏服务端,已经会用 Netty,但 API 是照着示例拼的。能写出 ChannelInboundHandlerAdapter,却说不清 ByteBuf 的读索引为什么需要 discardReadBytes,也说不清 EventLoop 为什么不能执行阻塞任务。
第三类是被具体网络问题卡住的后端开发者。比如长连接被中间设备静默断开、压测时大量连接停在 CLOSE_WAIT、服务重启丢少量请求、tcpdump 里看到重传却判断不了原因。
本书不面向完全没写过 Java 的读者,也不面向只想找「Netty 面试题标准答案」的读者。前者请先读《Java 编程入门》;后者会发现本书的每个结论都带版本、命令和适用条件。
前置知识
假定你已经掌握:
- Java 基本语法、类与接口、异常、集合、泛型、
record、Lambda。 - 多线程基础:
Thread、Runnable、ExecutorService、synchronized的粗略用法。 - Maven 的基本命令:
mvn compile、mvn test、mvn package。 - 能读懂
try-with-resources、CompletableFuture、AutoCloseable。 - 了解 TCP 与 UDP 的粗略区别;不要求记得三次握手细节,第 2 章会讲。
不要求读过 Java 四卷本,但本书会引用卷三的线程与 JMM、卷四的 JFR 与线程转储。第一次出现时会给出足够上下文,深入部分明确指向对应卷。
版本口径
全书版本固定,正文中的输出和编译命令都以这套版本为准。版本不同,行为可能不同,尤其是 TLS、虚拟线程和 JFR 事件字段。
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | Temurin 25.0.5+8 | 编译与运行,--release 25 |
| Maven | 3.9.12 | 多模块构建 |
| JUnit Jupiter | 5.14.4 | 测试片段 |
| 第三方网络库 | 无 | 卷一禁止 Netty、OkHttp、HttpClient、Jetty、Undertow、Spring |
| 日志 | System.getLogger | 不引入 SLF4J 绑定 |
| 其他简化库 | 无 | 禁止 Lombok、Guava、Jackson |
JDK 25 是 LTS。本书用到它,不是因为它最新,而是因为下面几条事实会直接影响网络代码:
- 虚拟线程正式可用,但本卷的网络服务器默认仍用平台线程加
Selector。虚拟线程只在第 12 章「业务线程池」里做对照,不用它替代事件循环。 SocketChannel默认阻塞,非阻塞必须显式configureBlocking(false)。这个默认值很容易让「非阻塞服务器」实际上仍然是阻塞的。Selector在 macOS 上基于 kqueue,在 Linux 上基于 epoll。本书不把两者混为一谈。ByteBuffer的position、limit、capacity三个值必须分清;flip()不是「反转」,而是把写模式切到读模式。DirectByteBuffer的回收依赖 Cleaner。本书不建议用sun.misc.Unsafe或反射Cleaner作为常规手段。
实验环境
书里的「本机输出」统一来自下面这套环境。换机器后绝对值会变,但命令、字段和方向通常不变。
Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
Temurin 25.0.5+8
先把 JDK 固定住。默认 java 可能指向 JDK 8 或其他版本,所有命令都先设置 JAVA_HOME:
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)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)
javac 25.0.5
Apache Maven 3.9.12 (848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1)
Maven home: /Users/bingrong.yan/apache-maven-3.9.12
Java version: 25.0.5, vendor: Eclipse Adoptium, runtime: /Users/bingrong.yan/.jdks/jdk-25.0.5+8/Contents/Home
如果 java -version 不是 25.0.5,先查 JAVA_HOME,再看 mvn -version 里的 runtime。Maven 使用 JAVA_HOME 指向的 JDK,而不是 PATH 里第一个 java。
macOS 的平台事实
本书在 macOS 上写作和验证,有几条平台事实必须提前说明,否则读者会以为命令写错了。
没有 ss。 Linux 上常用的 ss -lntp 在 macOS 上不存在。连接诊断用:
netstat -anv -p tcp
lsof -nP -iTCP:9090
没有 epoll。 macOS 的 Selector 基于 kqueue。本书不写 epoll 的实测,也不把 Selector 描述成 epoll 的封装。Linux 与 macOS 的差异会在卷二讲 Netty 原生传输时再展开。
tcpdump 需要 sudo。 抓回环流量用 lo0:
sudo tcpdump -i lo0 -nn -s 0 port 9090
没有实际执行过的抓包,不会写成「本机输出」。
压测工具只有 ab。 本机 /usr/sbin/ab 可用,wrk 和 hey 未安装。本书的性能实验用 ab,并给出完整参数;不写 wrk、hey 的实测数字。
端口固定。 linkd 用 9090,上游 libraryd 用 8080。抓包和压测都走 loopback,避免把局域网因素混进来。
项目结构
卷一新建 linkd,与 libraryd 分开。libraryd 只作为上游被代理,本书不改它的业务代码。
linkd/
├── pom.xml # com.plume.linkd:linkd-parent:0.1.0-SNAPSHOT
├── linkd-core/ # 缓冲区、帧、连接、事件循环
├── linkd-http/ # HTTP/1.1、HTTP/2
├── linkd-ws/ # WebSocket
└── linkd-app/ # main、路由、配置
依赖方向:
linkd-app ──► linkd-http ──► linkd-core
│ │
└──────► linkd-ws ──────┘
linkd-app 依赖另外三个模块;linkd-http 与 linkd-ws 都依赖 linkd-core,彼此不依赖;模块之间不循环依赖。包根是 com.plume.linkd,入口是 com.plume.linkd.app.LinkdMain,默认端口 9090。
核心类型在全书保持稳定:
| 类型 | 包 | 关键方法 |
|---|---|---|
BufferPool | com.plume.linkd.core | acquire(int)、release(ByteBuffer) |
FrameDecoder | com.plume.linkd.core | decode(ByteBuffer) |
Connection | com.plume.linkd.core | channel()、enqueue(ByteBuffer)、close() |
EventLoop | com.plume.linkd.core | register、run、execute |
Handler | com.plume.linkd.core | onReadable、onWritable、onClosed |
HttpRequest / HttpResponse | com.plume.linkd.http | record 访问器 |
HttpParser / HttpRouter | com.plume.linkd.http | parse、route |
WebSocketFrame / WebSocketSession | com.plume.linkd.ws | sendText、sendBinary、close |
如果某一节必须调整签名,会先在项目契约里改,并在正文说明迁移,不会只改一篇。读代码时如果发现类型名对不上,先检查是不是跨节版本不一致。
全书结构
15 章 45 节,分五个部分。每章三节,节与节之间是「机制 → 实现 → 工程问题」的关系,不是三个并列知识点。
| 部分 | 章 | 主题 | 落点 |
|---|---|---|---|
| 一、地图与语义 | 1 | 网络编程地图与实验环境 | 请求路径、I/O 模型、linkd 骨架 |
| 2 | TCP/UDP 语义与 Java API | 连接、字节流、数据报、地址与接口 | |
| 3 | Socket、超时与连接生命周期 | Socket、ServerSocket、半关闭、RST | |
| 二、字节与事件循环 | 4 | 字节、缓冲区与编解码基础 | 字节序、字符集、三种成帧 |
| 5 | NIO Buffer 与 Channel | 三值状态、直接内存、Scatter/Gather | |
| 6 | Selector 与单线程事件循环 | 就绪事件、单线程 Reactor、主从模型 | |
| 7 | 非阻塞服务器与半包处理 | accept/read/write、粘包、状态机 | |
| 三、协议实现 | 8 | 手写 HTTP/1.1 服务器 | 报文、keep-alive、路由 |
| 9 | HTTP/2 帧与多路复用 | 帧、HPACK、流量控制 | |
| 10 | TLS/SSLEngine 与非阻塞握手 | 握手、wrap/unwrap、ALPN | |
| 11 | WebSocket 协议与帧处理 | Upgrade、帧、心跳与广播 | |
| 四、运行时工程 | 12 | Reactor 线程模型 | 事件循环、业务池、上下文 |
| 13 | 零拷贝与 DirectByteBuffer | 系统调用边界、回收、mmap | |
| 14 | 背压、超时与优雅关闭 | 水位、重试、排空 | |
| 五、验证与收口 | 15 | 抓包、压测与故障定位 | tcpdump、ab、v1 验收 |
第 1–3 章建立「连接是什么」;第 4–7 章建立「字节怎么流动」;第 8–11 章把字节变成协议;第 12–14 章处理线程、内存和关闭;第 15 章用工具证明前面的结论。顺序读最完整,跳读路径见目录篇。
书中还有两个附录:附录 A 是 Socket 与 NIO API 速查,附录 B 是网络诊断命令清单。附录不引入新概念,只做查表。
代码与输出约定
完整类。 能独立编译的类给完整源码,包括 package、import 和 main(如果可运行)。读者可以直接复制到文件里执行。
片段。 只展示关键部分的代码用注释说明上下文,正文会写清「这段代码位于哪个方法里」。片段不保证单独编译。
反例。 标注「反例」的代码块故意保留错误,可能编译失败或运行异常。它们用来展示边界,不要直接用于生产。
真实输出。 放在 text 块里,并标注「本机输出」。为了排版删除无关行时,会写「已省略」。没有实际运行过的输出标为「示意输出」,不冒充实测。
命令。 bash 块里的命令按顺序执行。涉及端口的命令给出可替换端口;涉及临时目录的用 /tmp/linkd-*,不污染仓库。tcpdump 命令会标明需要 sudo。
代码围栏。 所有围栏标语言:java、bash、text、xml、yaml、json、properties。不标语言的围栏视为排版错误。
公开类型。 一个代码块里不放两个 public 顶层类型。嵌套类型写成 public static,并且与正文提到的包名一致。
版本标注。 涉及预览 API 的代码会写 --enable-preview;本卷正文不使用预览 API,如果测试片段用到,会单独标注。涉及平台差异的结论会直接写出「macOS」或「Linux」范围。
卷一为什么不引入第三方网络库
这条约束值得单独解释,因为读者最容易在这里产生疑问:既然 Netty 更好用,为什么不用?
原因有三个。
第一,控制变量。 网络编程的难点在边界:读缓冲区里有多少字节、read 返回 0 还是 -1、写队列什么时候排空、SelectionKey 什么时候失效。框架把这些边界封装起来,出错时你只能看到「回调没触发」。自己写一遍,边界就变成可打印、可断点、可抓包的对象。
第二,建立对照。 卷二用 Netty 重写同一套协议时,需要有一份「标准库版本」作为对照。ByteBuffer 对 ByteBuf、Selector 对 EventLoop、Handler 对 ChannelPipeline,只有两边都写过,才知道框架替你做了什么、没做什么。
第三,避免版本噪音。 Netty 4.2 的 API、Spring 的自动配置、Jackson 的序列化行为都会随版本变化。卷一把依赖压到 JDK 本身,书中的结论在三五年内更稳定,读者也更容易判断问题出在协议还是框架。
允许使用的只有 JDK 标准库和 JUnit Jupiter。日志用 System.getLogger,JSON 用固定字符串或最小手写解析,测试用 mvn test。这不是说这些库不好,而是本书要讲的机制会被它们遮住。
如何验证书中的结论
本书的结论分四层验证,每节尽量做到前两层。
第一层:编译。 所有完整类在 JDK 25 下通过 javac --release 25;多模块代码通过:
cd linkd
mvn -q -DskipTests compile
编译失败时先看 JAVA_HOME 和 --release,不要先怀疑代码。
第二层:运行。 能独立运行的程序给出 java 命令和本机输出。服务器类程序给出启动命令、curl 请求和响应;涉及端口的先确认 9090/8080 没有被占用。
java -cp linkd-app/target/classes com.plume.linkd.app.LinkdMain
curl -i http://127.0.0.1:9090/healthz
响应示例会在正文标注是「本机输出」还是「固定响应」。/books/{id} 在本卷允许返回固定假响应,正文会明确说明。
第三层:协议与系统调用。 涉及连接、半包、关闭的结论,用系统工具验证:
netstat -anv -p tcp | grep 9090
lsof -nP -iTCP:9090
sudo tcpdump -i lo0 -nn -s 0 port 9090
每条命令都会说明「看哪一列、期望看到什么、异常时长什么样」。没有实际抓包证据时,正文只讲用法,不编造时间线。
第四层:性能与资源。 压测用 ab,并给出完整参数与机器口径:
/usr/sbin/ab -n 20000 -c 200 -k http://127.0.0.1:9090/healthz
吞吐、分位、错误数要一起看。单次运行只说明「这次发生了什么」,不当作结论;至少重复三次并给分布。CPU、内存、连接数用 top、jcmd、netstat 交叉验证。
四层之外还有一条总纪律:一次只改一个变量。 端口、缓冲区大小、线程数、连接数、超时、机器负载,任何一项变了,前后数字就不能直接比较。
版本或结果不一致时怎么排查
如果你在自己的环境里得到不同结果,按下面顺序排查,不要先改代码。
- JDK 版本。
java -version与javac -version是否都是 25.0.5。--release 25只保证字节码目标,不能替代运行时 JDK。 JAVA_HOME。 Maven 和 IDE 可能使用不同的 JDK。先看mvn -version的 runtime,再看 IDE 的 Project SDK。- 端口占用。 9090 或 8080 被旧进程占用时,新进程可能启动失败,也可能连到旧进程。用
lsof -nP -iTCP:9090确认。 - 地址解析。
localhost可能先解析到::1而不是127.0.0.1。服务只绑定 IPv4 时,客户端用localhost会连不上。本书统一写127.0.0.1。 - 代理与环境变量。
HTTP_PROXY、ALL_PROXY会让curl走代理,回环请求也可能被拦截。实验前确认NO_PROXY包含127.0.0.1。 - 平台差异。 macOS 没有
ss、没有 epoll;tcpdump的接口名是lo0而不是lo。Linux 上的输出不能直接套。 - TLS 与证书。 第 10 章的证书由
keytool生成,有效期和 SAN 必须匹配访问地址。证书不匹配时,先看curl -v的握手错误,不要先改代码。 - 缓冲区与超时。 不同机器的默认缓冲区、
SO_TIMEOUT、TCP_NODELAY行为可能不同。对照正文的参数逐项确认。
与站内其他内容的边界
本书与站内几组内容有交集,边界如下。
network 专题。 那组文章讲协议全景、网络运维、Linux 数据面(eBPF、DPDK)、HTTP/3、负载均衡与可观测性。本书不重复协议百科,也不写内核实现;它讲「用 Java 标准库怎么实现」,并且以 linkd 的代码为主线。需要补协议背景时,正文会指向对应专题文章,但不复制内容。
《MMO 服务器开发》网络卷。 那套书以 Go 为主线,讲游戏服务端的连接模型、帧格式、命令号、跨服与分区分服,并用 Java 21 做对照。本书不写游戏业务协议,也不写跨服;它讲通用 TCP/UDP、HTTP、TLS、WebSocket,以及 Java NIO 的事件循环。两者在「长度域分帧」上有交集,但本书从 FrameDecoder 的实现和单元测试切入,不从游戏命令号切入。
Spring Boot 三部曲。 那套书讲框架层:自动配置、MVC、事务、安全、Actuator。本书不写 Spring,也不假设读者使用 Spring。第 8 章手写 HTTP/1.1 时,会对照 Tomcat 的连接参数,但目的是解释参数背后的对象,不是教怎么配 Spring。
Java 四卷本。 卷三讲线程、JMM、虚拟线程、JUC;卷四讲 JVM 运行时、GC、JIT、JFR 与诊断。本书承接这些内容:第 6、12 章用卷三的线程模型解释 Reactor;第 13、15 章用卷四的诊断工具观察直接内存和 socket 事件。但本书不重复 JMM 的可见性规则,也不重复 GC 算法;遇到这些内容会明确写「详见卷三/卷四」。
snippets/。 仓库里的 snippets/ 是独立代码示例,不参与 Hugo 构建,也不属于本书项目。本书的代码都在 linkd/ 下,不往 snippets/ 放。
从环境开始
在打开第 1 章之前,先确认三件事:
java -version与javac -version都是 25.0.5。mvn -version的 runtime 指向 JDK 25。- 9090 和 8080 没有被其他进程占用。
三条都满足之后,按下面的顺序做第一次构建:
cd linkd
mvn -q -DskipTests compile
如果这一步失败,先看错误里的 JDK 路径和模块名,再对照本章的「版本或结果不一致时怎么排查」。不要在没有跑通构建的情况下读第 5 章的 ByteBuffer,否则代码和正文会对不上。
卷一的代码量不大,但每节都有可运行片段。建议边读边敲,至少把 linkd-core 的 BufferPool、FrameDecoder、Connection、EventLoop 四个类型亲手写一遍。网络编程的理解来自「看到字节在哪个缓冲区里」,而不是来自记住 API 列表。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。