Netty
开篇:为什么不直接用 Java NIO?
Java 从 1.4 就引入了 NIO,提供了 Channel、Buffer、Selector 这套非阻塞 IO 的 API。但如果你真正用原生 NIO 写过网络程序,一定会有以下痛苦体验:
- API 难用:ByteBuffer 读写共用一个 position,每次读写都要 flip,忘了就出 bug。
- Epoll 空轮询 bug:JDK 的 Selector 在某些 Linux 内核上会出现 CPU 100% 的空转问题。
- TCP 粘包/拆包:需要自己处理消息边界,手写半包解码器。
- 线程模型复杂:要自己管理线程池、事件分发、异常处理。
Netty 就是为了解决这些问题而生的。它是一个异步事件驱动的网络应用框架,站在 NIO 之上提供了更高层次的抽象,让开发者可以专注于业务逻辑,而不用操心底层的网络细节。Dubbo、RocketMQ、Elasticsearch、gRPC 等框架的网络层都选择了 Netty。
一、Netty 核心组件
1.1 EventLoop 与线程模型
Netty 的线程模型基于 Reactor 模式。在理解 Netty 之前,先快速过一下三种 Reactor 模型:
| 模型 | 说明 | 缺点 |
|---|---|---|
| 单 Reactor 单线程 | 一个线程同时处理连接和读写 | 一个 handler 阻塞就全卡 |
| 单 Reactor 多线程 | 一个线程处理连接,worker 线程池处理读写 | Reactor 同时管连接和 IO 事件,高并发下成瓶颈 |
| 主从 Reactor 多线程 | Boss 线程处理连接,Worker 线程处理读写 | Netty 默认采用的模型 |
Netty 默认使用主从 Reactor 多线程模型:
┌─────────────────────────────────────────────┐
│ BossGroup (1个EventLoop) │
│ 只负责 accept 新连接 │
│ 把新 Channel 注册到 WorkerGroup │
└─────────────┬───────────────────────────────┘
│ 注册
┌─────────────▼───────────────────────────────┐
│ WorkerGroup (N个EventLoop) │
│ 每个 EventLoop 绑定一个线程 │
│ 负责该 Channel 上的所有 IO 事件 │
└─────────────────────────────────────────────┘EventLoop 是 Netty 的核心处理引擎。每个 EventLoop 绑定一个线程,负责处理注册到它上面的所有 Channel 的 IO 事件。一个 Channel 在整个生命周期内只会绑定一个 EventLoop,因此不存在多线程并发访问同一个 Channel 的情况,天然线程安全。
1.2 Channel 与 Pipeline
Channel 是 Netty 对网络连接的抽象,提供了 register、bind、connect、read、write、flush 等操作。
ChannelPipeline 是 Netty 的核心编排组件,可以类比工厂的流水线。每个 Channel 都有自己的 Pipeline,Pipeline 内部是一个由 ChannelHandler 组成的双向链表:
入站(Inbound)→→→→→→→→→→→→→→→→→→→→→→→→→→
Decoder → BusinessHandler → ...
出站(Outbound)←←←←←←←←←←←←←←←←←←←←←←←←
... → Encoder- 入站事件(数据从网络到应用):从 Pipeline 头部向尾部传播。
- 出站事件(数据从应用到网络):从 Pipeline 尾部向头部传播。
ChannelHandler 是业务逻辑的载体,分为 ChannelInboundHandler(处理入站)和 ChannelOutboundHandler(处理出站)。开发者只需要编写 Handler,然后像积木一样拼装到 Pipeline 中。
1.3 ByteBuf vs ByteBuffer
Java 原生的 ByteBuffer 有两大痛点:
- 读写共用 position,必须手动 flip 切换读写模式。
- 容量固定,写满了不能自动扩容。
Netty 的 ByteBuf 完美解决了这些问题:
| 特性 | ByteBuffer | ByteBuf |
|---|---|---|
| 读写指针 | 共用 position,需要 flip | 独立的 readerIndex / writerIndex |
| 扩容 | 不支持 | 自动扩容到 maxCapacity |
| 内存池 | 不支持 | 支持池化,减少 GC |
| 零拷贝 | 有限支持 | CompositeByteBuf / slice 等丰富支持 |
ByteBuf 的结构一目了然:
┌──────────────┬─────────────────┬───────────────┐
│ 已读(废弃) │ 可读字节 │ 可写字节 │
0 readerIndex writerIndex capacity读完的部分可以通过 discardReadBytes() 回收空间,把可读字节移到数组头部,腾出更多可写空间。
ByteBuf 按内存位置和是否池化有四种组合:
| Pooled(池化) | Unpooled(非池化) | |
|---|---|---|
| Heap(堆内存) | 业务处理 + 高并发 | 业务处理 + 普通场景 |
| Direct(堆外内存) | Socket IO + 高并发 | Socket IO + 普通场景 |
二、Netty 的高性能设计
2.1 零拷贝
Netty 的零拷贝体现在两个层面:
操作系统层面:通过 FileRegion 封装了 FileChannel.transferTo(),利用 Linux 的 sendfile 系统调用,数据从文件直接发送到 Socket 缓冲区,不经过用户态。
应用层面(更多):
- 堆外内存:直接使用 DirectByteBuf,避免堆内到堆外的拷贝。Java NIO 在发送堆内数据时会先拷贝到堆外。
- CompositeByteBuf:将多个 ByteBuf 组合成一个逻辑整体,不做实际的内存拷贝。
- Unpooled.wrappedBuffer:把 byte[] 包装成 ByteBuf,不产生拷贝。
- ByteBuf.slice:把一个 ByteBuf 切成多个,共享底层数组,不拷贝。
2.2 内存池化
频繁创建和销毁 ByteBuf 会给 GC 带来压力。Netty 借鉴了 jemalloc 的思想,实现了内存池:
- Arena:每个线程绑定一个 Arena,减少锁竞争。
- Chunk:以 Page 为单位管理内存,用伙伴算法分配。
- Slab:对小内存场景做优化,将 Page 划分为等长的 region。
- tcache:每个线程私有的缓存,分配内存时优先从 tcache 获取,避免锁竞争。
2.3 高效序列化
Netty 支持多种序列化协议:JSON、Protobuf、Thrift 等。其中 Protobuf 是最常用的高性能选择,它是 Google 开源的二进制序列化协议,序列化后的数据量远小于 JSON,解析速度也快得多。
2.4 解决 TCP 粘包/拆包
TCP 是面向流的协议,没有消息边界。Netty 提供了多种开箱即用的解码器来处理:
| 解码器 | 策略 |
|---|---|
FixedLengthFrameDecoder | 固定长度 |
LineBasedFrameDecoder | 按行分隔 |
DelimiterBasedFrameDecoder | 自定义分隔符 |
LengthFieldBasedFrameDecoder | 长度字段 + 内容(最常用) |
使用方式非常简单,只需要在 Pipeline 中添加对应的 Handler:
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
1024, 0, 4, 0, 4));
ch.pipeline().addLast(new BusinessHandler());2.5 Epoll 空轮询 bug 的规避
JDK 的 NIO 在某些 Linux 内核上存在 Epoll 空轮询 bug,Selector.select() 在没有事件时也会返回,导致 CPU 100%。Netty 的解决方案很巧妙:
- 记录 select 返回空结果的次数。
- 如果连续空轮询次数超过阈值(默认 512),就重建 Selector 并将所有 Channel 重新注册。
if (/* 空轮询次数 > 512 */) {
selector = selectRebuildSelector(selectCnt);
}2.6 无锁化设计
Netty 通过以下方式避免锁竞争:
- 每个 Channel 绑定唯一的 EventLoop,所有 IO 操作都在同一个线程执行,无需加锁。
- 对象池(Recycler):重用 ByteBuf 等频繁创建的对象,减少对象创建和 GC。
- FastThreadLocal:使用数组代替 HashMap,O(1) 定位,比 JDK 的 ThreadLocal 更快。
三、Netty 中的高性能数据结构
3.1 FastThreadLocal
JDK 的 ThreadLocal 底层用 ThreadLocalMap,它是一个哈希表,用线性探测法解决冲突,在数据量大时查找效率下降。
Netty 的 FastThreadLocal 改为直接用 Object 数组,每个 FastThreadLocal 对象持有一个唯一的 index,通过 array[index] 直接获取值,时间复杂度 O(1)。并且在线程池场景中,Netty 封装了 FastThreadLocalRunnable,任务执行完后自动清理所有 FastThreadLocal,防止内存泄漏。
3.2 HashedWheelTimer(时间轮)
Netty 的时间轮用于高效管理大量定时任务。相比 JDK 的 Timer(小根堆,O(logn))和 ScheduledThreadPoolExecutor,时间轮的 Schedule 和 Run 操作都接近 O(1)。
原理是将一个环形数组想象成钟表盘面,每个槽(slot)挂一个任务链表。指针每 tick 一次,执行当前槽中到期的任务。任务根据到期时间取模落入对应的槽,如果超出一圈就用 round 计数标识需要等几圈。
Kafka 在此基础上做了两个优化:用 DelayQueue 推进时间轮解决空推进问题,用层级时间轮处理跨度很大的定时任务(类似钟表的时针、分针、秒针)。
四、Netty 在框架中的应用
| 框架 | 用途 |
|---|---|
| Dubbo | RPC 通信层,服务提供者和消费者之间的网络传输 |
| RocketMQ | Broker 与 Producer/Consumer 之间的消息收发 |
| Elasticsearch | 节点间通信 |
| gRPC | Java 版本的底层网络传输 |
| Spring WebFlux | 非阻塞 HTTP 服务器(可选 Netty 作为底层) |
这些框架选择 Netty 的原因都是一样的:高性能、高可靠、API 友好。
五、常见面试题精选
Q1:Netty 性能好的原因是什么?
六个方面:1)IO 多路复用,一个线程处理多个连接;2)主从 Reactor 线程模型,连接和 IO 分离;3)零拷贝(堆外内存 + CompositeByteBuf + FileRegion);4)内存池化,减少 GC;5)无锁串行化设计(Channel 绑定唯一 EventLoop);6)高性能序列化协议支持(Protobuf 等)。
Q2:Netty 如何解决 TCP 粘包/拆包?
通过在 Pipeline 中添加编解码器 Handler。常用的有 FixedLengthFrameDecoder(固定长度)、LineBasedFrameDecoder(行分隔)、DelimiterBasedFrameDecoder(自定义分隔符)、LengthFieldBasedFrameDecoder(长度字段 + 内容,最常用)。核心思想是约定消息边界,在解码阶段还原完整的消息帧。
Q3:Netty 的线程模型是怎样的?
基于主从 Reactor 多线程模型。BossGroup 包含少量 EventLoop,只负责 accept 新连接并将新 Channel 注册到 WorkerGroup。WorkerGroup 包含多个 EventLoop,每个 EventLoop 绑定一个线程,负责处理注册到它上面的 Channel 的所有 IO 事件。通过配置不同的 EventLoopGroup 参数,也可以实现单线程或多线程模型。
Q4:Netty 的零拷贝是怎么实现的?
操作系统层面:FileRegion 封装了 FileChannel.transferTo(),利用 sendfile 系统调用实现文件到 Socket 的零拷贝。应用层面:使用堆外内存避免堆内到堆外的拷贝;CompositeByteBuf 将多个 ByteBuf 逻辑组合不做实际拷贝;ByteBuf.slice 共享底层数组;Unpooled.wrappedBuffer 包装 byte 数组不拷贝。
小结
| 核心概念 | 说明 |
|---|---|
| EventLoop | 一个线程 + 一个 Selector,处理绑定 Channel 的所有 IO 事件 |
| Pipeline | Handler 组成的双向链表,入站从头到尾,出站从尾到头 |
| ByteBuf | 读写双指针、自动扩容、池化、零拷贝 |
| Reactor | Boss 管连接,Worker 管 IO,串行化无锁 |
一句话总结 Netty 的设计哲学:用 Reactor 管线程,用 Pipeline 管逻辑,用 ByteBuf 管内存,用零拷贝管性能。理解了这四件事,Netty 的核心脉络就清楚了。
附录:Netty 进阶知识
A.1 Netty 的三层架构
从宏观上看,Netty 的整体结构分为三层:
Core 核心层:提供底层网络通信的通用抽象和实现,包括事件模型、通用 API、支持零拷贝的 ByteBuf。这是 Netty 最精华的部分。
Protocol Support 协议支持层:覆盖了主流协议的编解码实现,包括 HTTP、HTTP/2、WebSocket、Protobuf、二进制等。Netty 还支持自定义应用层协议,降低了开发成本。
Transport Service 传输服务层:提供了网络传输能力的定义和实现,支持 Socket、HTTP 隧道、虚拟机管道等传输方式。对 TCP、UDP 做了抽象和封装。
A.2 自定义协议设计
Netty 中自定义协议通常包含以下字段:
┌──────────────────────────────────────────────────┐
│ 魔数(2B) │ 版本(1B) │ 序列化算法(1B) │ 报文类型(1B) │
├──────────────────────────────────────────────────┤
│ 状态(1B) │ 保留字段(4B) │ 数据长度(4B) │
├──────────────────────────────────────────────────┤
│ 数据内容(变长) │
└──────────────────────────────────────────────────┘- 魔数:用于校验是否为合法报文,防止非法连接。
- 版本号:支持协议升级。
- 序列化算法:标识数据内容使用的序列化方式(JSON、Protobuf 等)。
- 数据长度:解决粘包拆包的关键。解码器通过读取长度字段确定一个完整报文的边界。
A.3 writeAndFlush 的工作流程
writeAndFlush 是一个出站操作,从 Pipeline 的 Tail 节点开始向 Head 传播:
- write 阶段:数据并没有写入 Socket,而是写入了
ChannelOutboundBuffer(一个单向链表)。 - flush 阶段:将 ChannelOutboundBuffer 中的数据真正写入 Socket 缓冲区,由操作系统发送。
如果只调用 write() 不调用 flush(),数据会一直堆积在 ChannelOutboundBuffer 中,不会发出去。
A.4 五种 IO 模型对比
| 模型 | 阻塞方式 | 特点 |
|---|---|---|
| BIO | 同步阻塞 | 一个连接一个线程,简单但不适合高并发 |
| NIO(同步非阻塞) | 轮询 | 不阻塞但大量系统调用,效率不高 |
| IO 多路复用 | select/poll/epoll | 一个线程监控多个 fd,高效 |
| 信号驱动 IO | 信号通知 | 数据就绪时内核发信号,应用处理 |
| AIO(异步 IO) | 完全异步 | 内核完成 IO 后通知应用,Linux 支持有限 |
Netty 基于 IO 多路复用模型,底层使用 epoll(Linux)或 kqueue(macOS),通过 Selector 实现一个线程管理多个连接。
A.5 内存泄漏检测
Netty 使用引用计数管理 ByteBuf 的生命周期。当 ByteBuf 的引用计数为 0 时,它会被释放或放入对象池。如果 ByteBuf 对象被 GC 回收但引用计数不为 0,就说明发生了内存泄漏。
Netty 会对分配的 ByteBuf 进行抽样分析,检测到泄漏后输出 LEAK 关键字的日志。可以通过以下方式调整检测级别:
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);四个级别:DISABLED(关闭)、SIMPLE(默认,抽样 1%)、ADVANCED(抽样 1%,记录详细信息)、PARANOID(100% 检测,适合测试环境)。
A.6 Netty 中的设计模式
| 设计模式 | 在 Netty 中的体现 |
|---|---|
| 单例 | DefaultSelectStrategy.INSTANCE、ReadTimeoutException.INSTANCE |
| 工厂 | DefaultSelectStrategyFactory、各种 ChannelFactory |
| 装饰者 | WrappedByteBuf 装饰 ByteBuf |
| 责任链 | ChannelPipeline 驱动 Handler 链 |
| 观察者 | ChannelFuture 监听器 |
| 策略 | EventExecutorChooser 根据线程池大小选择取模策略 |
A.7 Netty 的对象池(Recycler)
Netty 内置了对象池机制,用于重用频繁创建的对象(如 ByteBuf),避免频繁的 GC。核心类是 Recycler,每个线程有自己的回收栈,归还对象时放入栈中,获取时从栈中弹出。
// 从对象池获取
ByteBuf buf = allocator.buffer(256);
// 使用完毕后释放(归还到对象池)
buf.release();对象池的好处:减少 GC 压力、减少内存分配开销、避免锁竞争(每线程独立栈)。
A.8 select、poll、epoll 对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | bitmap | 数组 | 红黑树 + 链表 |
| fd 上限 | 1024 | 无限制 | 无限制 |
| 遍历方式 | 线性扫描 | 线性扫描 | 事件回调 |
| 触发方式 | 水平触发 | 水平触发 | 水平 + 边缘触发 |
| 性能 | O(n) | O(n) | O(1) |
epoll 的核心优势在于:使用事件回调机制(epoll_ctl 注册,epoll_wait 获取就绪事件),不需要遍历所有 fd,适合大量连接但活跃连接少的场景。Netty 在 Linux 上默认使用 epoll。
A.9 Netty 服务端启动流程
一个最简单的 Netty 服务端的代码如下:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
1024, 0, 4, 0, 4));
ch.pipeline().addLast(new BusinessHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();启动流程分为几个阶段:
- 创建 EventLoopGroup:bossGroup 负责接收连接(通常 1 个线程足够),workerGroup 负责处理 IO(默认 CPU 核数 x 2 个线程)。
- 配置 ServerBootstrap:设置 Channel 类型(NIO/Epoll)、配置 Pipeline 中的 Handler、设置 TCP 参数。
- bind 端口:创建 ServerSocketChannel,注册到 bossGroup 的某个 EventLoop 的 Selector 上,开始监听连接事件。
- 接收连接:Boss EventLoop 接收到连接请求后,创建 SocketChannel,按轮询策略注册到 WorkerGroup 的某个 EventLoop 上。
- 处理 IO:Worker EventLoop 在其 Selector 上轮询读写事件,触发 Pipeline 中的 Handler 链。
A.10 Netty 中的 ChannelFuture
Netty 中所有 IO 操作都是异步的,返回值是 ChannelFuture。你可以通过以下方式处理结果:
// 方式一:sync() 同步等待
ChannelFuture future = channel.writeAndFlush(msg);
future.sync(); // 阻塞直到操作完成
// 方式二:addListener 异步回调
channel.writeAndFlush(msg).addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
System.out.println("发送成功");
} else {
System.err.println("发送失败: " + f.cause());
}
});推荐使用 addListener 的异步方式,避免阻塞 EventLoop 线程。
A.11 Netty 的优雅关闭
线程池的关闭不能直接 shutdownNow(),否则正在处理的请求会被中断。Netty 提供了优雅关闭的机制:
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();shutdownGracefully() 会先停止接收新的连接和任务,等待当前正在处理的任务完成(有超时时间),然后释放所有资源。默认的静默期是 2 秒,超时时间是 15 秒。
A.12 Netty 中的 CompositeByteBuf 详解
CompositeByteBuf 是 Netty 零拷贝的核心实现之一。当 TCP 传输中一个数据包被拆成多个字节流时,传统做法是创建一个新的大 ByteBuf,把多个小 ByteBuf 的数据拷贝过去。
CompositeByteBuf 的做法不同:它通过指针将多个 ByteBuf 组合成一个逻辑上的整体,不做实际的内存拷贝。对外提供统一的读写接口,内部维护一个 Component 列表来管理各个子 ByteBuf 的偏移关系。
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponents(true, buf1, buf2, buf3);
// 现在可以像操作一个 ByteBuf 一样操作 composite
// 底层 buf1、buf2、buf3 的数据没有被拷贝A.13 Netty 的心跳与空闲检测
长连接场景下需要心跳机制来检测连接是否存活。Netty 提供了 IdleStateHandler:
ch.pipeline().addLast(new IdleStateHandler(
30, 0, 0, TimeUnit.SECONDS)); // 30秒没有读事件就触发
ch.pipeline().addLast(new HeartbeatHandler());IdleStateHandler 会在指定时间内没有读/写事件时触发 IdleStateEvent,你在自定义的 Handler 中捕获这个事件,发送心跳包或关闭连接。
三个参数分别是:读空闲时间、写空闲时间、读写空闲时间。设为 0 表示不检测。