Skip to content

Netty 与网络编程概览 ​

S4-017选修高级约 90 分钟JDK 21企业开发进阶 · Netty 与网络编程

1. 本节定位 ​

《Nginx 负载均衡与缓存》讲网关;本节讲**"高性能网络编程的底座"**:

text
BIO:一个连接一个线程 → 简单,但线程数随连接数线性增长 ✗
NIO:事件驱动(Selector)→ 少数线程撑大量连接 ✓✓
Netty:把 NIO 的复杂度封装掉 → 你还写业务,剩下的交给它 ✓✓✓

★ 本节的实测会纠正一个常见误解 ✓✓:

text
BIO:100 客户端 × 20 消息 → 689 ms,服务端线程数峰值 **179** ✗✗
NIO:100 客户端 × 20 消息 → 655 ms,处理 100 个连接只用 **1 个 reactor 线程** ✓✓

★ 性能几乎一样(689 vs 655 ms)——差别在"用了多少资源" ✓✓

2. 学习目标 ​

  • 能说清 BIO 与 NIO 的本质差别(不是速度,是扩展性) ★★★;
  • 能说出 Reactor 模型的三步(注册、选择、分发) ★★★;
  • 知道 NIO 里"粘包处理"为什么更难,以及 Netty 的一行解法 ★★★;
  • 能说出 Netty 替你封装了什么(6 项以上) ★★★;
  • 知道什么场景不该用 Netty ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个容量估算 ✓:

text
BIO 服务:1 个连接 = 1 个线程
· 1 万连接 → 1 万线程 ✗✗
· 每线程栈默认 512KB~1MB → **5~10 GB 内存** ✗✗
· 更致命:这么多线程抢 CPU,**上下文切换**会吃掉大部分算力([S2-009](../../02-cs/os/S2-009))✗

★ 而本节的实测给出了 NIO 的答案 ✓✓:

同样的 100 个连接、同样的消息量:BIO 用到 179 个线程,NIO 只用到 1 个 reactor 线程 ✓✓

进程总线程数 8(基线 7,几乎没涨) ✓

5. 核心概念 ​

5.1 BIO vs NIO:不是快慢,是扩展性 ★★★ ​

实测数据 ✓✓:

模型100 客户端 × 20 消息服务端线程数
BIO(一连接一线程)689 ms179 个 ✗✗
NIO(Selector 单线程)655 ms1 个 reactor + 基线 ✓✓

★★ 两条结论必须一起看 ✓✓:

结论说明
两者在本机 100 连接下性能相当 ✓BIO 并不"慢" ——它在连接数不高时完全够用,且代码简单得多 ✓✓
但资源占用差两个数量级 ✓✓NIO 的价值是"用常数级线程撑大量连接" ✓

★ 所以选型是这样的 ✓✓:

场景选择
连接数几百以内 ✓BIO 完全够用(如内部服务、定时任务、连接数可控的客户端)✓
连接数上万(IM、网关、推送) ★★★必须 NIO(BIO 的线程数会直接压垮机器)✗✗
要自定义协议 ★★Netty(NIO 的复杂度不值得手写)✓✓

5.2 Reactor 模型:NIO 的三步 ★★★ ​

本节的 NIO 服务端骨架(约 60 行) ✓:

java
Selector selector = Selector.open();
serverChannel.configureBlocking(false);          // ① 非阻塞是前提
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();                            // ② 阻塞等"有事件发生"
    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isAcceptable()) { ... register(OP_READ); }   // ③ 分发:连接事件
        else if (key.isReadable()) { channel.read(buffer); } // ③ 分发:读事件
    }
}

★ 三步就是 Reactor 的全部 ✓✓:注册感兴趣的事件 → 等待事件 → 分发处理 ✓

★★ 但这 60 行"只是能跑" ✗ ——离生产还差很远 ✓:

还缺什么不处理的后果
写半包处理 ★★write 不保证写完,剩余数据会丢 ✗
空轮询 bug 处理 ★★JDK 的 select() 可能无事件返回,导致 CPU 100% ✗✗
多线程 Reactor 模型 ★单线程处理所有 IO,CPU 用不满 ⚠️
粘包拆包 ★★★读到的数据边界不确定 ✗✗
连接空闲检测 ★★死连接一直占着资源 ✗
优雅关闭 ★★关闭时丢消息 ✗
内存池 ★频繁分配 ByteBuffer → GC 压力 ⚠️
流量整形突发流量打垮后端 ⚠️

★★ 这 8 条就是 Netty 存在的理由 ✓✓

5.3 粘包:NIO 里必须自己做的第一件事 ★★★ ​

实测(服务端发 3 条带长度前缀的消息)✓:

text
  ① 朴素读法(一次 read 当一条消息):把 3 条消息当成了 1 条 ✗✗
  ② 按长度字段解码(正确的做法):解出 3 条:[订单-0, 订单-1, 订单-2] ✓✓

★ 这是 《TCP 与 UDP》 的结论在 NIO 里的落地 ✓✓:

TCP 是字节流,消息边界必须由协议自己定义(定长 / 分隔符 / 长度字段)✓✓

★★ 而 NIO 里这件事更棘手 ✓:

一次 read 读到哪里完全不确定 ——可能读到半条、也可能读到三条 ✗

所以需要「半包缓存 + 状态机」:读不满就存着,下次接着解 ✓

★ Netty 的答案是一行代码 ✓✓:

java
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));

★ 它内部就是"半包缓存 + 状态机",而且处理了各种边界情况 ✓ —— 你自己写这 100 行,大概率会有漏掉的分支 ✗

5.4 Netty 替你封装了什么 ★★★ ​

★ 本节的 NIO 代码 vs Netty 的写法 ✓:

本类的 NIO 写法Netty 对应
ServerSocketChannel + bindServerBootstrap.bind(port)
register(OP_ACCEPT)NioEventLoopGroup(boss 组)
while(true) + selector.select()EventLoop 内部循环(不用你写) ✓
遍历 selectedKeyspipeline 上的 handler 回调 ✓
手写长度字段解码LengthFieldBasedFrameDecoder(一行) ✓✓
手写空闲检测IdleStateHandler ✓
手写优雅关闭shutdownGracefully() ✓

★ 一份典型 Netty 服务端骨架 ✓✓:

java
EventLoopGroup boss = new NioEventLoopGroup(1);        // 接连接
EventLoopGroup worker = new NioEventLoopGroup();       // 处理 IO
ServerBootstrap bootstrap = new ServerBootstrap()
    .group(boss, worker)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        protected void initChannel(SocketChannel ch) {
            ch.pipeline()
              .addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4))   // 拆包
              .addLast(new StringDecoder())
              .addLast(new StringEncoder())
              .addLast(new MyBusinessHandler());        // ★ 你只写业务
        }
    });
bootstrap.bind(18091).sync();

★★ 关键认识 ✓✓:

Netty 的价值不是"更快",而是"把 NIO 的复杂度封装掉" ✓✓

· 它仍然用 NIO(或 epoll 原生传输),性能上限与手写 NIO 相当 ✓

· 但它把「半包、空轮询、内存泄漏、优雅关闭」这些坑都填了 ✓✓

★ 这也解释了它的地位 ✓:

Dubbo、RocketMQ、gRPC-Java、Elasticsearch 的底层都是 Netty ✓✓

5.5 什么时候不该用 Netty ★★ ​

★ 三种情况 ✓:

场景更好的选择
只是客户端发 HTTP 请求 ★★HttpClient(《HTTP 与 HTTPS》)✓
只是写个内部服务 ★★Spring Boot 内嵌 Tomcat(《Spring Boot 自动配置与项目结构》)✓
连接数不高 ✓BIO 就够了(本节实测:性能相当,代码更简单)✓✓

★ 真正需要 Netty 的信号 ✓✓:

text
① 要自己定义协议(IM、游戏、私有 RPC)✓
② 连接数极高(数万以上长连接)✓✓
③ 要精细控制网络层(拆包、心跳、流量整形)✓

★ 学习路径建议 ✓✓:

先按本节写一遍 NIO 的 echo,再去看 Netty 的 API ✓

否则看 Netty 只会记住一堆类名,不知道每个类在解决什么问题 ✗✓

6. 递进示例 ​

6.1 最小示例:BIO 的一连接一线程 ​

java
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept();
    new Thread(() -> handle(client)).start();      // ✗ 连接数 = 线程数
}

6.2 业务示例:NIO 的事件分发 ​

见 5.2 的骨架 ✓

6.3 反例:在 NIO 里做阻塞操作 ​

java
// ✗ 在 reactor 线程里查数据库(阻塞 10 ms)
// → 这 10 ms 内所有连接的事件都得不到处理 ✗✗
if (key.isReadable()) {
    User user = userDao.find(id);      // ✗✗ 严禁在 IO 线程做阻塞操作
}

★ 正确做法:业务处理丢到独立的业务线程池 ✓✓ —— 这就是 Netty 的 addLast(businessGroup, handler) 的意义 ✓

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/netty/NioLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.netty.NioLesson

实测输出(关键部分):

text
===== 1. BIO =====
  100 个客户端 × 20 条消息: 689 ms
  服务端线程数峰值:179(基线 7,即为每个连接起了 1 个线程)✗✗

===== 2. NIO =====
  100 个客户端 × 20 条消息: 655 ms
  服务端处理连接数:100,而只用 1 个 reactor 线程 ✓✓
  进程总线程数:8(基线 7,几乎没涨)✓✓

===== 3. 粘包处理 =====
  ① 朴素读法:把 3 条消息当成了 1 条 ✗✗
  ② 按长度字段解码:解出 3 条:[订单-0, 订单-1, 订单-2] ✓✓

★ 三条可落地的结论 ✓:

#结论依据
①NIO 的收益是扩展性,不是延迟性能相当,线程数 179 vs 1 ✓✓
②NIO 的复杂度必须被封装(否则坑极多)8 项缺失 ✓
③业务逻辑绝不能跑在 IO 线程上会阻塞所有连接 ✓✓

8. Debug 观察点 ​

  1. 把 BIO 的连接数从 100 提到 500:观察线程数与耗时的变化(BIO 会明显恶化)★★;
  2. 在 NIO 的 isReadable 分支里加 Thread.sleep(10):观察吞吐断崖 ✓✓;
  3. 把长度字段解码去掉:观察消息被"粘成一条" ✗;
  4. 用 jstack 看 BIO 服务端的线程栈:观察上百个同构的 worker 线程 ✓。

9. 常见错误 ​

#错误说明
1以为 NIO 一定比 BIO 快 ✗✗是扩展性差异,不是速度 ✓✓
2在 IO 线程里做阻塞操作 ✗✗会卡住所有连接 ✓✓
3手写 NIO 却不管写半包 ✗✗数据会丢 ✓
4不管空轮询 ✗✗CPU 飙到 100% ✓
5连接数不高也要上 Netty ✗过度设计 ✓
6以为"用了 Netty 就快" ✗业务代码里阻塞照样慢 ✓
7不理解粘包就上线自定义协议 ✗✗偶发数据错乱,极难排查 ✓✓

10. 动手实验 ​

目标:亲眼看到"线程数"这条曲线。

步骤:

  1. 跑 NioLesson,记录 BIO 与 NIO 的线程数 ✓;
  2. 把客户端数改成 20 / 100 / 300,各跑一次,画出"连接数-线程数"曲线 ★★;
  3. 在 NIO 的读事件里加一次 10 ms 的阻塞,观察吞吐下降 ✓✓;
  4. 给 NIO 加上"半包缓存"(读不满时存起来),用变长消息验证 ★★★;
  5. 思考:你的项目里有哪个服务可能有上万长连接?它现在用的是什么模型?✓

验收方式:能说出"NIO 与 BIO 的差别在哪、什么时候该用哪个"。

扩展任务:用 Netty 实现一个心跳检测:

  • 客户端每 5 秒发一次 ping,服务端 15 秒没收到就断开 ✓;
  • 用 IdleStateHandler 实现 ✓;
  • 说明:为什么长连接必须有心跳(提示:NAT 会静默回收连接,《网络分层模型与数据传输过程》)★★

11. 练习题 ​

  1. 判断:NIO 一定比 BIO 性能更高。(对 / 错)
  2. 填空:Reactor 模型的三步是 、、______。
  3. 简答:为什么不能在 NIO 的 IO 线程里查数据库?
  4. (代码阅读)
java
if (key.isReadable()) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    channel.read(buffer);
    String message = new String(buffer.array());
    handle(message);
}

这段代码有什么问题? 5. (设计)给"IM 消息推送"选型:10 万长连接,消息要实时推。用什么?为什么?

参考答案(要点)
  1. 错。NIO 的收益是"用更少线程撑更多连接"(扩展性) ✓ —— 本机 100 连接下两者性能相当(689 vs 655 ms) ✓★
  2. 注册感兴趣的事件;等待事件(select());分发处理 ✓
  3. 因为 IO 线程只有一个(或几个),阻塞它会卡住所有连接的事件处理 ✗✗ —— 业务应丢到独立线程池 ✓★
  4. ① 没处理"读了多少"(read 返回值被忽略)✗;② 没处理粘包/半包 ✗✗; ③ buffer.array() 可能含上次残留 ✗(应按 flip() 后的 limit 取值)✓
  5. 要点:必须用 NIO(Netty) ✓ ——10 万连接用 BIO 需要 10 万线程,内存与切换开销不可承受 ✗✗ —— Netty 的 IdleStateHandler 做心跳、LengthField 拆包、业务线程池隔离 ✓✓

12. 掌握标准 ​

  • [ ] 能说清 BIO 与 NIO 的差别是扩展性而非速度 ★★★;
  • [ ] 能说出 Reactor 模型三步 ★★★;
  • [ ] 知道 NIO 版本离生产还差哪些东西 ★★★;
  • [ ] 知道粘包在 NIO 里更难处理,以及 Netty 的一行解法 ★★★;
  • [ ] 知道 Netty 封装了什么,以及它的真正价值 ★★★;
  • [ ] 知道"业务逻辑不能跑在 IO 线程上" ★★★;
  • [ ] 知道什么场景不该用 Netty ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Dubbo 与 RPC 基础》:把方法调用变成网络请求,以及它贵在哪。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引