四卷本结束之后,还缺一层
《Java 编程入门》《Java 编程实战》《Java 高级编程》《Java 虚拟机原理与性能调优》四卷写完,library 从一个命令行程序长成了 libraryd:能连数据库,能返回 JSON,能用虚拟线程处理请求,也能在 JFR 和 GC 日志里被观察。
从「写一个能用的 Java 服务」这个目标看,这套内容已经闭环了。
但闭环里有一个被反复跳过的区间。
libraryd 启动之后,Tomcat 或 Javalin 在 8080 端口上监听。用户敲下 curl,几百毫秒后拿到 JSON。这中间发生了什么?字节从网卡进来,经过内核协议栈,被某个线程读进缓冲区,拼成 HTTP 报文,解析出方法和路径,交给路由,再序列化成响应写回去。这条链路在四卷本里只以「框架已经处理好了」的形式出现。
框架确实处理好了。可一旦出现下面这些情况,「处理好了」就不再是答案:
- 压测时 QPS 上不去,CPU 却很闲,不知道是连接数、线程数还是内核队列的问题。
- 客户端偶尔收到截断的 JSON,日志里没有任何异常。
- 服务发布时重启,少量请求报
Connection reset,但不知道该怎么排空。 - 一个接口平时 20 ms,偶尔 2 s,抓包看到大量重传,却看不懂
tcpdump的时间线。 - 想给服务加一个 WebSocket 推送,或者把 HTTP/1.1 升级到 HTTP/2,只能照着博客复制配置。
这些问题不属于语言,也不完全属于 JVM。它们属于网络编程:连接怎么建立、字节怎么成帧、缓冲区怎么流转、线程怎么等待、协议怎么协商、关闭怎么收尾。
《Java 网络编程基础》补的就是这一层。
一个说不清的请求
先看一个具体场景,它会在第 1 章被拆开。
libraryd 提供一个只读接口 GET /books/42。客户端发送:
curl -v http://127.0.0.1:8080/books/42
这条命令背后至少有八个问题,四卷本没有逐条回答:
127.0.0.1是怎么被解析成地址的?InetAddress会不会查 DNS?- 三次握手是谁发起的?服务端的 accept 队列有多长?队列满了客户端看到什么?
- 请求字节到达时,是哪个线程在执行
read?它阻塞在哪里? - 一个 1 KB 的请求和一个 2 MB 的请求,在读缓冲区里有什么区别?
GET /books/42 HTTP/1.1这行文本,什么时候变成HttpRequest对象?- 响应写完之前,连接能不能复用?
Content-Length和chunked各自解决什么问题? - 客户端中途断开,服务端怎么知道?半关闭和 RST 有什么区别?
- 进程收到
SIGINT,正在写的响应怎么办?
只要有一个问题答不上来,调优就只能靠猜。
从网卡到方法:一条请求路径
把这条路径画出来,Java 能看见的部分其实没有想象中多:
客户端
│ TCP 三次握手
▼
网卡 → 内核协议栈 → 监听 socket 的 accept 队列
│ accept() 返回一个新的已连接 fd
▼
JVM:ServerSocketChannel / SocketChannel
│ read() 把字节从内核接收缓冲区拷进 ByteBuffer
▼
应用层缓冲:半包拼接、帧解码
│
▼
HttpParser:请求行 → 头部 → 消息体
│
▼
HttpRouter:method + path → HandlerFn
│
▼
业务方法:byte[] → String → 对象
│
▼
write():用户缓冲区 → 内核发送缓冲区 → 网卡
这张图里有三个边界值得记住。
第一个边界是内核与用户态。 read 和 write 都是拷贝,不是「把数据交给对方」。内核接收缓冲区满了,窗口就会缩小;发送缓冲区满了,write 就会阻塞或返回 0。很多「服务端没收到」的问题,其实卡在这两个缓冲区里。
第二个边界是字节流与消息。 TCP 只保证字节有序到达,不保证一次 read 对应一个请求。一个请求可能分三次读到,三个请求也可能一次读完。半包与粘包不是异常,而是默认行为。第 4 章和第 7 章会用 FrameDecoder 把这件事讲透。
第三个边界是协议与业务。 HTTP/1.1 的报文边界靠 Content-Length 或 chunked;HTTP/2 靠帧头的长度字段和流 ID;WebSocket 靠帧头的 FIN 与 opcode。协议解析器的工作,就是把无结构的字节变成有边界的消息。业务代码不应该直接面对 ByteBuffer。
框架把中间层藏起来了
Spring Boot 三部曲里,Tomcat 的连接参数以配置项出现:
server:
port: 8080
tomcat:
threads:
max: 200
accept-count: 100
max-connections: 8192
connection-timeout: 20s
这些配置当然有用,但它们回答不了「为什么是 200」「accept-count 和 max-connections 有什么区别」「连接超时和读超时是不是一回事」。不知道底层模型,调参就是在一组互相影响的数字里碰运气。
Netty 也一样。ByteBuf、EventLoop、ChannelPipeline、ChannelHandler 这些名字很好用,但如果不曾自己维护过读缓冲区、注册过 SelectionKey、处理过写半包,ChannelPipeline 就只是一串需要记住的 API。
本书的安排是:先用 JDK 标准库把中间层写一遍,再在卷二用 Netty 重写同一套东西。 写过一遍之后,Netty 的类名只是对你已经理解的机制换了称呼,而不是一套新的魔法。
站内已有内容为什么不够
本站已经有几组与网络相关的内容,边界需要说清楚,避免重复,也避免读者走错门。
network 专题偏协议全景与运维视角:TCP/IP、DNS、BGP、HTTP 演进、HTTP/3、负载均衡、eBPF、DPDK、可观测性。每一篇把一个主题讲深,但不提供一条「从 ServerSocket 写到网关」的连续代码路径。它回答「这个协议是什么」,本书回答「用 Java 怎么实现它」。
《MMO 服务器开发》的网络卷以 Go 为主线,讲游戏服务端的连接模型、帧格式、命令号、跨服通信,并用 Java 21 做对照。它的重点是游戏业务架构,不是 Java 网络 API 的系统训练。本书不写游戏帧协议,也不写跨服。
Spring Boot 三部曲讲框架层:自动配置、事务、安全、可观测性。本书不写 Spring,也不假设你会用 Spring;它讲的是框架底下那一层。
Java 四卷本讲语言、工程、运行时与 JVM。本书直接承接卷三的线程、JMM、虚拟线程,以及卷四的诊断工具,但不重复它们的内容。遇到 Selector 的唤醒机制、DirectByteBuffer 的回收、JFR 的 socket 事件,本书会给出可运行的验证,而不是把 JVM 卷的结论再抄一遍。
这本书写给谁
本书假设你已经能写 Java,但不假设你写过网络服务器。典型读者有三类。
第一类:写过 Spring Boot,但说不清 Tomcat 之下发生了什么。 能配 server.tomcat.max-threads,但遇到连接堆积、响应截断、发布时连接重置,只能搜索报错。这类读者需要一条从 Socket 到 HTTP 的完整链路,把配置项背后的对象认全。
第二类:准备做中间件、网关、RPC 或游戏服务端。 已经会用 Netty 或 Spring WebFlux,但 API 是照着示例拼的。这类读者需要自己实现一遍事件循环、帧解码、背压和优雅关闭,之后再看框架源码,能对上号。
第三类:被具体问题卡住的后端开发者。 比如半包导致 JSON 解析偶发失败、长连接被中间设备静默断开、压测时大量 TIME_WAIT、服务重启丢请求。这类读者可以从第 7、14、15 章切入,再回头补前面的模型。
不适合两类读者:完全没有 Java 基础的人,以及只想找「Netty 面试题答案」的人。前者请先读《Java 编程入门》;后者会发现本书的结论都带条件和可复现命令。
三卷路线
《Java 网络编程基础》是一套三卷本的第一卷。三卷共用同一个项目 linkd,但技术栈和问题层次不同。
| 卷 | 技术栈 | 回答的问题 | 产出 |
|---|---|---|---|
| 卷一(本书) | JDK 25 标准库 | 字节怎么流动、连接怎么管理、协议怎么解析 | linkd v1:可运行的 HTTP/1.1、HTTP/2 教学实现、TLS、WebSocket 与网关 |
| 卷二 | Netty 4.2 | 同样的协议,成熟框架怎么组织 | 用 Netty 重写卷一,对照 ByteBuf、EventLoop、ChannelPipeline |
| 卷三 | gRPC、协议设计、生产化 | 协议怎么设计、服务怎么治理、怎么上线 | 协议规范、gRPC 服务、限流、灰度与生产检查清单 |
卷一刻意不引入任何第三方网络库。没有 Netty,没有 OkHttp,没有 Apache HttpClient,没有 Jetty,没有 Undertow,也没有 Spring。日志用 System.getLogger,JSON 用手写的最小解析或固定响应,测试只用 JUnit Jupiter。
这个限制不是为了复古,而是为了控制变量。网络编程的难点本来就在边界:缓冲区什么时候翻转、半包什么时候拼接、写队列什么时候排空、连接什么时候算关闭。如果这些边界被框架封装掉,读者就只能记住「框架会处理」,而不知道处理的是什么。
卷二会放开限制,用 Netty 4.2 重写同一套协议。那时你会看到:ByteBuf 对应 ByteBuffer 的读写索引,EventLoop 对应 Selector 加任务队列,ChannelPipeline 对应 Handler 链,ChannelFuture 对应写完成回调。名字变了,问题没变。
卷三再往上走一层:不再关心「怎么把字节读出来」,而是关心「协议该怎么设计、版本怎么兼容、超时和重试放在哪一层、服务怎么被治理」。三卷的边界是清楚的:卷一实现,卷二对照框架,卷三做设计。
为什么终点是网关
本书最后一章不是「再写一个协议」,而是把前面所有东西收口成一个最小网关 linkd v1。
网关是网络编程问题的汇合点:
- 它要接受连接,所以有 accept 队列、连接上限、空闲检测。
- 它要解析协议,所以有半包、帧边界、头部大小限制。
- 它要转发请求,所以有超时、重试、幂等和上游连接池。
- 它要控制流量,所以有读写水位、背压和队列上限。
- 它要发布,所以有优雅关闭、连接排空和期限。
- 它要被排障,所以有
tcpdump、netstat、lsof、JFR 和压测指标。
把这些放在一个项目里,才能看出它们互相牵制:背压会导致写队列增长,写队列增长会拖慢关闭,关闭太急会丢响应,关闭太慢会拖长发布窗口。单独看每一节都懂,合在一起才是工程。
linkd v1 的契约在全书统一:
| 项目 | 约定 |
|---|---|
| 模块 | linkd-core、linkd-http、linkd-ws、linkd-app |
| 坐标 | com.plume.linkd:0.1.0-SNAPSHOT |
| 入口 | com.plume.linkd.app.LinkdMain |
| 端口 | HTTP 9090;上游 libraryd 8080 |
| 健康检查 | GET /healthz 返回 200 OK 与 ok |
| 业务接口 | GET /books/{id},代理到 http://127.0.0.1:8080/books/{id} |
| WebSocket | /ws/echo,文本回显、二进制原样回显 |
| HTTP/2 | 仅明文 h2c(prior knowledge),教学实现 |
| 关闭 | SIGINT 停止 accept、排空写队列、关闭连接 |
业务接口在本卷允许返回固定假响应,但正文会明确标注「固定响应」,不把它说成真实上游。HTTP/2 实现是教学实现,不声称生产级。这两条是写作纪律,也是读代码时判断边界的依据。
linkd 长什么样
模块结构在全书不变:
linkd/
├── pom.xml # 父 POM,packaging=pom
├── linkd-core/ # 缓冲区、帧、连接、事件循环
├── linkd-http/ # HTTP/1.1、HTTP/2
├── linkd-ws/ # WebSocket
└── linkd-app/ # main、路由、配置
依赖方向是 app → http/ws → core,模块之间不循环依赖。核心类型在 com.plume.linkd.core:
| 类型 | 作用 |
|---|---|
BufferPool | 复用 ByteBuffer,控制堆外内存 |
FrameDecoder | 从累积字节里切出完整帧 |
Connection | 一个连接的读写队列与关闭状态 |
EventLoop | 注册通道、等待就绪、执行任务 |
Handler | onReadable / onWritable / onClosed |
HTTP 层是 HttpRequest、HttpResponse、HttpParser、HttpRouter;WebSocket 层是 WebSocketFrame 与 WebSocketSession。这些签名在 50 篇里保持一致,跨节引用时不会改名。
每节至少落到两个具体类、方法或运行场景。比如讲半包,不会只画「缓冲区里有半个请求」的示意图,而是打开 FrameDecoder.decode(ByteBuffer),看它返回空列表、一帧还是多帧;讲背压,不会只说「要限流」,而是看 Connection.enqueue 在队列超过水位后怎么让 EventLoop 取消 OP_READ。
每节怎么组织
每一节按同一个骨架写:
- 承接的问题。 从一个真实代码、抓包或故障切入,不写百科式开场。
- 核心机制。 两到四个小节,配可运行代码和真实命令;每段代码后解释,每段输出标注来源。
- 在 linkd 里落地。 给出接近完整的类或方法,说明它放在哪个模块、和谁交互。
- 常见错误与边界。 两到三个具体坑,尽量给报错原文或错误写法。
- 小结。 四到六条,每条一句。
正文从 ## 开始,页面标题由模板渲染成 h1。代码围栏都标语言:java、bash、text、xml、yaml、json、properties。反例代码会标注「反例」,并说明它为什么错。
三条进入方式
全书可以顺序读,也可以按问题跳读。
按顺序。 从第 1 章开始,每节末尾会指向下一节。这条路径适合第一次系统学网络编程的读者,因为后面的协议实现依赖前面的缓冲区与事件循环。
按问题。 如果你正被某个症状卡住,先看目录里的三条症状路径:连接建立不了、请求卡住或截断、吞吐上不去或关闭丢请求。每条路径给出从哪一节进、先看什么命令、再回到哪一节补机制。
按项目。 如果你只想看 linkd 怎么从零长起来,按「项目状态」表走:第 1 章搭骨架,第 3 章阻塞 echo,第 6 章单线程 Reactor,第 8 章 HTTP/1.1,第 11 章 WebSocket,第 15 章验收。中间跳过纯语义章节,遇到不懂的再回查。
三条路径都不要求先读完 Java 四卷本,但需要你能看懂 record、sealed、try-with-resources、CompletableFuture 和基本的 ExecutorService 用法。卷三的线程与 JMM、卷四的 JFR 与线程转储,会在相关章节被引用,但不会重复展开。
实验纪律
网络实验比语言实验更容易得出错误结论,因为变量更多:机器、内核、负载、时序、端口状态。本书统一以下纪律。
版本固定。 Temurin 25.0.5+8,Maven 3.9.12,--release 25。默认 java 可能指向旧版本,所有命令先设置 JAVA_HOME。
机器口径固定。 Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)。换机器后绝对值会变,方向和字段含义通常不变。
命令可复现。 涉及抓包给 sudo tcpdump -i lo0,涉及连接给 netstat -anv -p tcp 与 lsof -nP -iTCP:9090,涉及压测给 ab 的完整参数。wrk、hey 在本机未安装,本书不写它们的实测。
平台事实不虚构。 macOS 没有 ss,没有 epoll;Selector 在 macOS 上基于 kqueue。本书不写 ss 和 epoll 的「本机输出」,也不把 Selector 说成 epoll。Linux 上的差异会在卷二讲 Netty 原生传输时再区分。
单次运行不是结论。 性能数字必须给命令、参数和机器口径;单次运行标注「单次运行,未做统计」。没有实际执行过的输出,标为「示意输出」,不冒充实测。
不写「一定更快」。 零拷贝、直接内存、虚拟线程都有适用条件。本书给条件、给测量方法,不给脱离场景的结论。
本书不写什么
- 不写 Linux 内核协议栈的实现细节:
sk_buff、拥塞控制算法内部、eBPF 数据面属于network专题。 - 不写 Netty 源码:卷二才用 Netty,卷一只在必要处做概念对照。
- 不写 Spring Boot、Tomcat、Jetty 的用法:那是框架层的内容。
- 不写游戏服务端的业务协议:MMO 网络卷已经覆盖,本书只讲通用 TCP/UDP 与 WebSocket。
- 不写 HTTP/3 与 QUIC:本卷的协议范围到 HTTP/2、TLS 1.3、WebSocket 为止。
- 不把教学实现包装成生产组件:
linkd v1是学习项目,不是可以直接替换 Nginx 的网关。
从第 1 章开始
第 1 章会跟着一次 GET /healthz 走完整条路径:从 curl 发出请求,到 LinkdMain 里的一行日志。你会先看到路径,再看到每一层对应的 Java 类型,最后看到 linkd 的四模块骨架怎么搭起来。
读完第 1 章,你应该能回答三个问题:请求字节在哪几个缓冲区里待过、哪个线程把它读出来、响应写完之前连接处于什么状态。如果这三个问题还答不上来,后面的 Selector 和协议实现都会变成背 API。
网络编程没有捷径,但有顺序。先把字节看清楚,再把连接管住,最后才谈协议和网关。这本书按这个顺序写。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。