Netty 从零到精通验收清单
这页用来验收你是否真的把 Netty 学到了能理解原理、能写协议服务、能做商业项目、能排查线上问题、能面试回答的程度。
Netty 不是“写一个 Echo Server 就会了”。真正学懂要能解释:
- BIO 为什么一连接一线程扛不住大量长连接。
- NIO 的 Channel、Buffer、Selector、SelectionKey 怎么协作。
- Reactor 模型为什么能用少量线程处理大量连接。
- BossGroup、WorkerGroup、EventLoop、Channel 的关系。
- 一个连接从
accept到注册、读事件、Pipeline 处理、写回的完整过程。 - Pipeline 入站和出站 Handler 为什么顺序不同。
- ByteBuf 为什么有读写指针、池化、堆外内存、引用计数和泄漏风险。
- TCP 粘包半包为什么发生,长度字段协议如何解决。
- 心跳、空闲检测、断线重连、连接清理怎么设计。
- 写缓冲、背压、水位线、慢客户端为什么会导致内存上涨。
- EventLoop 阻塞、direct memory 泄漏、解码错乱、连接暴涨如何排查。
总学习路线
flowchart TD
A["阶段1:TCP 和网络字节流"] --> B["阶段2:BIO、NIO 和 Selector"]
B --> C["阶段3:Reactor 线程模型"]
C --> D["阶段4:Netty 启动和连接接入"]
D --> E["阶段5:EventLoop 和 Channel"]
E --> F["阶段6:Pipeline 和 Handler"]
F --> G["阶段7:ByteBuf 内存管理"]
G --> H["阶段8:协议编解码和粘包半包"]
H --> I["阶段9:心跳、连接管理和背压"]
I --> J["阶段10:商业场景和生产排查"]这条路线的关键是先理解“网络传输的是字节流”,再理解“少量线程如何监听大量连接事件”,最后才是 Netty 的 API。
阶段1:为什么 BIO 不适合大量长连接
BIO 的典型模型是一连接一线程:
flowchart TD
A["连接1"] --> B["线程1 阻塞 read"]
C["连接2"] --> D["线程2 阻塞 read"]
E["连接3"] --> F["线程3 阻塞 read"]
G["更多连接"] --> H["更多线程阻塞等待"]如果做设备长连接、IM、实时推送,很多连接大部分时间只是在线,不一定一直发数据。BIO 模型下,空闲连接也可能占线程,连接数上来后会出现:
| 问题 | 后果 |
|---|---|
| 线程数量过多 | 内存占用高 |
| 上下文切换频繁 | CPU 被调度消耗拖垮 |
| 慢连接占线程 | 其他连接排队 |
| 线程池满 | 新请求无法处理 |
| IO 和业务混在一起 | 延迟和故障难排查 |
NIO 的思路是:连接不独占线程,线程监听一批连接的事件,哪个连接就绪就处理哪个连接。
flowchart TD
A["多个 SocketChannel"] --> B["Selector"]
B --> C["少量 IO 线程"]
C --> D["处理就绪的 read/write/accept 事件"]这就是 Netty 高并发连接的基础。
阶段2:NIO 四个核心对象
| 对象 | 作用 | 零基础理解 |
|---|---|---|
Channel | 连接和数据通道 | 像一根可以读写的管道 |
Buffer | 数据缓冲区 | 读写网络字节的中转区 |
Selector | 多路复用器 | 一个线程看很多连接有没有事件 |
SelectionKey | 注册关系和事件状态 | 某个 Channel 在 Selector 上的事件卡片 |
NIO 服务端简化流程:
flowchart TD
A["ServerSocketChannel 打开端口"] --> B["注册到 Selector"]
B --> C["监听 OP_ACCEPT"]
C --> D["客户端连接到来"]
D --> E["accept 得到 SocketChannel"]
E --> F["SocketChannel 注册 OP_READ"]
F --> G["Selector 发现可读"]
G --> H["读取 ByteBuffer"]直接写 JDK NIO 难点很多:
- Selector 事件循环容易写错。
- 半包数据要自己保存和拼接。
- ByteBuffer 的
flip、compact容易用错。 - 连接关闭、异常、空轮询要自己处理。
- 线程模型和业务线程池要自己设计。
Netty 的价值就是把这些底层细节做成稳定模型。
阶段3:Reactor 模型
Reactor 的核心思想:
IO 线程不阻塞等某一个连接,而是监听很多连接的事件,事件来了再分发给对应 Handler。
单 Reactor 单线程:
flowchart TD
A["Selector"] --> B["Reactor 线程"]
B --> C["accept"]
B --> D["read"]
B --> E["decode"]
B --> F["business"]
B --> G["write"]问题:所有事情都在一个线程里,业务一慢,整个服务都慢。
主从 Reactor:
flowchart TD
A["Boss Reactor"] --> B["只负责 accept 新连接"]
B --> C["把连接注册给 Worker"]
C --> D["Worker Reactor 1 处理读写"]
C --> E["Worker Reactor 2 处理读写"]
C --> F["Worker Reactor 3 处理读写"]Netty 服务端常用 BossGroup + WorkerGroup:
| 组件 | 职责 | 不该做什么 |
|---|---|---|
| BossGroup | 监听端口、接收新连接 | 不处理慢业务 |
| WorkerGroup | 处理已建立连接的读写事件 | 不执行慢 SQL、同步 HTTP、大计算 |
如果把业务阻塞放到 Worker EventLoop,多个连接会被同一个慢任务拖住。
阶段4:Netty 服务端启动全过程
flowchart TD
A["创建 bossGroup"] --> B["创建 workerGroup"]
B --> C["配置 ServerBootstrap"]
C --> D["指定 NioServerSocketChannel"]
D --> E["配置 childHandler"]
E --> F["bind 端口"]
F --> G["Boss 注册 ServerSocketChannel"]
G --> H["监听 accept 事件"]
H --> I["客户端连接进入"]
I --> J["创建 SocketChannel"]
J --> K["注册到某个 Worker EventLoop"]
K --> L["初始化 ChannelPipeline"]最小服务端 Demo:
public class EchoServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
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 StringDecoder())
.addLast(new StringEncoder())
.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
ctx.writeAndFlush("echo:" + msg);
}
});
}
});
ChannelFuture future = bootstrap.bind(9000).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}这个 Demo 只能证明 Netty 能跑。生产必须补协议边界、异常处理、心跳、鉴权、业务线程池、背压和监控。
阶段5:一次连接接入全过程
flowchart TD
A["客户端发起 TCP 连接"] --> B["Boss EventLoop 收到 OP_ACCEPT"]
B --> C["ServerSocketChannel accept"]
C --> D["创建 NioSocketChannel"]
D --> E["分配一个 Worker EventLoop"]
E --> F["注册 SocketChannel 到 Selector"]
F --> G["执行 ChannelInitializer"]
G --> H["添加 Pipeline Handler"]
H --> I["触发 channelActive"]关键点:
- 一个客户端连接通常对应一个
Channel。 - 一个
Channel通常绑定一个固定EventLoop。 - 这个
EventLoop后续负责它的读写事件。 - 同一个
EventLoop会负责多个Channel。
这就是为什么 EventLoop 不能阻塞:一个慢任务不是只影响一个连接,而是影响同一个 EventLoop 上的所有连接。
阶段6:EventLoop 为什么不能阻塞
错误示例:
protected void channelRead0(ChannelHandlerContext ctx, Request request) {
Order order = orderRepository.querySlowly(request.orderId());
ctx.writeAndFlush(order);
}如果 querySlowly 慢 2 秒,同一个 EventLoop 上其他连接的读写也会被拖慢。
正确思路:把慢业务交给有界业务线程池。
public class BusinessHandler extends SimpleChannelInboundHandler<Request> {
private final ExecutorService businessPool =
new ThreadPoolExecutor(
16,
32,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Override
protected void channelRead0(ChannelHandlerContext ctx, Request request) {
businessPool.execute(() -> {
Response response = orderService.handle(request);
ctx.writeAndFlush(response);
});
}
}注意:业务线程池也不能无界。否则高峰时请求会在业务队列里无限堆积,最后还是 OOM 或超时。
阶段7:Pipeline 入站和出站
Pipeline 是一条责任链。
flowchart TD
A["网络字节进入"] --> B["入站:FrameDecoder"]
B --> C["入站:MessageDecoder"]
C --> D["入站:AuthHandler"]
D --> E["入站:BusinessHandler"]
E --> F["出站:MessageEncoder"]
F --> G["写回网络"]入站处理网络到业务:
| Handler | 作用 |
|---|---|
| 拆包器 | 从 TCP 字节流中拆出完整帧 |
| 解码器 | 把 ByteBuf 转成业务对象 |
| 鉴权器 | 判断连接是否合法 |
| 业务处理器 | 执行业务逻辑 |
出站处理业务到网络:
| Handler | 作用 |
|---|---|
| 编码器 | 把业务对象转成 ByteBuf |
| 压缩/加密 | 可选增强 |
| 写出 | 写到 Channel |
顺序错误会导致:
- 业务 Handler 拿到未解码 ByteBuf。
- 鉴权在业务后执行,形成绕过风险。
- 编码器类型不匹配,响应写不出去。
- 异常没有统一处理。
阶段8:ByteBuf 内存模型
ByteBuf 有两个关键指针:
0 <= readerIndex <= writerIndex <= capacityflowchart TD
A["ByteBuf"] --> B["readerIndex:下次从哪里读"]
A --> C["writerIndex:下次写到哪里"]
A --> D["readableBytes:可读字节"]
A --> E["writableBytes:可写空间"]
A --> F["refCnt:引用计数"]为什么 Netty 不直接用 ByteBuffer?
| ByteBuffer 痛点 | ByteBuf 改进 |
|---|---|
| 读写切换要 flip | readerIndex/writerIndex 分离 |
| 扩容不方便 | ByteBuf 支持动态扩容 |
| API 不够网络友好 | 提供更多读写方法 |
| 池化和引用计数不明显 | Netty 做了池化内存管理 |
堆内和堆外:
| 类型 | 特点 |
|---|---|
| Heap ByteBuf | JVM 堆内,受 GC 管理 |
| Direct ByteBuf | 堆外直接内存,减少一次拷贝,常用于网络 IO |
| Pooled ByteBuf | 池化复用,减少频繁分配和释放 |
引用计数:
ByteBuf buf = ctx.alloc().buffer();
System.out.println(buf.refCnt());
buf.release();如果 release 漏掉,直接内存可能持续上涨。
阶段9:ByteBuf 泄漏怎么产生
常见泄漏场景:
| 场景 | 原因 |
|---|---|
| 手动处理 ByteBuf 后忘记 release | 引用计数不归零 |
| 异常分支提前返回 | release 没执行 |
| 缓存 ByteBuf 但没 retain/release 配对 | 所有权混乱 |
| 传递给异步线程使用 | 原线程可能已释放或未正确 retain |
| 使用 SimpleChannelInboundHandler 后继续持有 msg | 自动释放后再使用会出问题 |
相对安全写法:
public class PacketHandler extends SimpleChannelInboundHandler<Packet> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Packet packet) {
Response response = handle(packet);
ctx.writeAndFlush(response);
}
}如果必须手动处理 ByteBuf:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
// 读取和处理
} finally {
ReferenceCountUtil.release(buf);
}
}泄漏检测:
-Dio.netty.leakDetection.level=advanced生产长期最高级别会有性能开销,一般用于排查阶段。
阶段10:TCP 粘包半包
TCP 是字节流协议,不保留消息边界。
应用发送:
hello | world | netty接收端可能读到:
helloworld | netty也可能读到:
he | llowor | ldnetty这不是 Netty bug,而是 TCP 的正常行为。
解决方式:
| 方式 | 原理 | 适合 |
|---|---|---|
| 固定长度 | 每条消息固定字节数 | 简单定长协议 |
| 分隔符 | 用 \n 等分隔 | 文本协议 |
| 长度字段 | 包头写 body 长度 | 二进制协议常用 |
| HTTP/WebSocket | 使用成熟协议边界 | Web 长连接 |
生产最常见是长度字段协议。
阶段11:长度字段协议
协议设计:
magic(2) | version(1) | type(1) | length(4) | body(N)流程:
flowchart TD
A["TCP 字节流"] --> B["LengthFieldBasedFrameDecoder"]
B --> C["按 length 拆出完整帧"]
C --> D["检查 magic 和 version"]
D --> E["反序列化 body"]
E --> F["业务 Packet"]配置:
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
1024 * 1024,
4,
4,
0,
0
));参数解释:
| 参数 | 含义 |
|---|---|
maxFrameLength | 最大帧长度,防止恶意大包 |
lengthFieldOffset | 长度字段从第几个字节开始 |
lengthFieldLength | 长度字段占几个字节 |
lengthAdjustment | 长度值是否需要修正 |
initialBytesToStrip | 解码后丢弃前几个字节 |
如果协议头是 magic(2) + version(1) + type(1) + length(4),长度字段从第 4 字节开始,所以 offset 是 4。
阶段12:心跳和连接清理
长连接不能只靠 TCP 自己判断断开。网络断开、客户端崩溃、NAT 过期都可能让服务端连接长时间不释放。
flowchart TD
A["连接建立"] --> B["IdleStateHandler 检测读空闲"]
B --> C{"超过读空闲时间"}
C -- "否" --> D["连接继续保持"]
C -- "是" --> E["发送心跳探测"]
E --> F{"是否收到心跳响应"}
F -- "是" --> D
F -- "否" --> G["关闭 Channel"]
G --> H["清理在线表和资源"]配置:
ch.pipeline().addLast(new IdleStateHandler(60, 0, 0));
ch.pipeline().addLast(new HeartbeatHandler());连接清理必须处理:
- 用户和 Channel 的绑定关系。
- 设备在线状态。
- 订阅关系。
- 未完成请求。
- 写缓冲和临时对象。
channelInactive 里不要做慢操作,可以快速清理内存状态,再异步落库或发消息。
阶段13:写缓冲和背压
如果业务持续写,客户端却接收慢,数据会堆在 Netty 写缓冲里。
flowchart TD
A["业务持续 writeAndFlush"] --> B["ChannelOutboundBuffer"]
B --> C{"网络和对端是否写得出去"}
C -- "能" --> D["缓冲下降"]
C -- "不能" --> E["pending bytes 上升"]
E --> F["Channel 变为不可写"]
F --> G["内存上涨和延迟升高"]设置水位:
bootstrap.childOption(
ChannelOption.WRITE_BUFFER_WATER_MARK,
new WriteBufferWaterMark(32 * 1024, 64 * 1024)
);发送前判断:
if (channel.isWritable()) {
channel.writeAndFlush(message);
} else {
// 低优先级消息丢弃或降级,核心消息进入有限队列
}背压设计:
- 区分核心消息和可丢弃消息。
- 写缓冲超过高水位时停止向该连接投递。
- 限制每个连接的待发送队列。
- 监控不可写时长和 pending bytes。
- 慢客户端不能拖垮整个服务。
阶段14:商业场景设计
设备采集长连接
flowchart TD
A["设备 TCP 连接"] --> B["Netty 接入层"]
B --> C["长度字段拆包"]
C --> D["协议解码"]
D --> E["设备鉴权"]
E --> F["心跳保活"]
F --> G["采集数据投递 MQ"]
G --> H["清洗、校验、入库"]设计要点:
- 接入层不要直接慢入库。
- 原始报文要按 traceId 保存一份,方便排查。
- 协议要有 magic、version、type、length。
- 设备鉴权失败立即关闭连接。
- 心跳超时清理设备在线状态。
- 高峰期用 MQ 削峰。
IM / 实时推送
flowchart TD
A["用户连接"] --> B["Netty 长连接服务"]
B --> C["登录鉴权"]
C --> D["绑定 userId 和 Channel"]
D --> E["消息投递"]
E --> F{"用户是否在线"}
F -- "在线" --> G["写入 Channel"]
F -- "离线" --> H["保存离线消息"]多实例要解决路由:
- Redis 保存用户在哪个实例。
- MQ 或内部 RPC 把消息发到对应实例。
- 实例下线要清理路由。
- 消息要有 ACK 和重试。
网关协议适配
flowchart TD
A["外部 TCP 协议"] --> B["Netty 解码"]
B --> C["协议转换"]
C --> D["内部 HTTP/RPC/MQ"]
D --> E["业务系统"]注意:协议网关要限制最大包、连接数、速率和非法协议,防止被恶意连接拖垮。
阶段15:生产排查
消息处理慢
flowchart TD
A["消息处理慢"] --> B["看 EventLoop 线程栈"]
B --> C["是否阻塞在 DB/HTTP/锁/日志"]
C --> D["业务线程池队列是否积压"]
D --> E["下游服务是否慢"]
E --> F["写缓冲是否堆积"]direct memory 上涨
flowchart TD
A["direct memory 上涨"] --> B["开启 Netty 泄漏检测"]
B --> C["检查 ByteBuf 是否 release"]
C --> D["检查异常分支"]
D --> E["检查异步持有 ByteBuf"]
E --> F["检查写缓冲 pending bytes"]解码异常
flowchart TD
A["解码异常"] --> B["抓原始报文"]
B --> C["检查 magic/version"]
C --> D["检查 length 字段偏移"]
D --> E["检查序列化格式"]
E --> F["检查客户端协议版本"]连接频繁断开
flowchart TD
A["连接频繁断开"] --> B["看 channelInactive 原因"]
B --> C["心跳超时是否过短"]
C --> D["客户端是否重连风暴"]
D --> E["网络或 NAT 是否断开"]
E --> F["服务端是否主动关闭慢连接"]写不出去
flowchart TD
A["写不出去"] --> B["channel.isWritable 是否 false"]
B --> C["pending bytes 是否过高"]
C --> D["客户端是否接收慢"]
D --> E["网络是否拥塞"]
E --> F["是否无限投递低优先级消息"]阶段16:常见坑和后果
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 在 EventLoop 查数据库 | 多个连接一起慢 | 业务线程池异步处理 |
| 业务线程池无界队列 | 堆积到 OOM | 有界队列和拒绝策略 |
| 没有长度字段 | 粘包半包解析错 | 设计协议边界 |
| ByteBuf 忘记释放 | direct memory 泄漏 | 明确所有权和 release |
| Pipeline 顺序错 | 解码、鉴权、编码异常 | 拆包 -> 解码 -> 鉴权 -> 业务 -> 编码 |
| 心跳太短 | 网络抖动误踢 | 合理空闲时间和重试 |
| 写缓冲不控 | 慢客户端拖垮服务 | 水位线、isWritable、降级 |
| 日志打印完整大包 | CPU/磁盘飙升 | 截断、采样、异步日志 |
| 单实例保存所有连接状态 | 扩容和故障转移困难 | Redis/MQ 做路由和状态同步 |
阶段17:面试标准回答
问:Netty 是什么?
标准回答:
Netty 是基于 Java NIO 的异步事件驱动网络框架。它封装了 Selector、Channel、Buffer、Reactor 线程模型、EventLoop、Pipeline、ByteBuf、编解码和连接管理,适合 IM、实时推送、RPC、网关、设备采集长连接和自定义 TCP 协议等高并发网络场景。
问:Netty 为什么高性能?
标准回答:
Netty 高性能不是一个点,而是 NIO 非阻塞 IO、Reactor 线程模型、Boss/Worker 分工、EventLoop 绑定 Channel、Pipeline 责任链、ByteBuf 池化和堆外内存、零拷贝以及成熟编解码机制共同作用。它避免了一连接一线程,让少量线程处理大量连接事件。
问:EventLoop 为什么不能阻塞?
标准回答:
一个 EventLoop 线程通常负责多个 Channel 的 IO 事件。如果 Handler 中执行慢 SQL、同步远程调用、大计算或同步日志,会阻塞同一个 EventLoop 上其他连接的读写,导致延迟升高、连接积压和超时。慢业务应该投递到有界业务线程池,并配合超时、拒绝和监控。
问:粘包半包怎么解决?
标准回答:
TCP 是字节流协议,不保留应用层消息边界,所以多个消息可能粘在一起,一个消息也可能被拆开。应用层必须定义协议边界,常见方式有固定长度、分隔符和长度字段。生产中常用
LengthFieldBasedFrameDecoder按包头长度字段拆出完整帧,再交给业务解码器。
最终验收题
如果下面问题答不清楚,说明还没有真正掌握 Netty:
- BIO 一连接一线程为什么扛不住大量长连接?
- Selector、Channel、SelectionKey 是什么关系?
- Reactor 模型为什么要拆 Boss 和 Worker?
- 一个连接从 accept 到注册 Worker 的完整过程是什么?
- 为什么一个 EventLoop 阻塞会影响多个连接?
- Pipeline 入站和出站执行顺序有什么区别?
- ByteBuf 的 readerIndex 和 writerIndex 分别表示什么?
- ByteBuf 为什么会有 direct memory 泄漏?
- TCP 粘包半包为什么发生?
- 长度字段协议的 offset、length、adjustment 怎么理解?
- 心跳为什么不能只依赖 TCP keepalive?
- 写缓冲堆积说明什么?怎么做背压?
- 设备采集长连接如何避免入库阻塞 EventLoop?
- IM 多实例如何找到用户在哪个连接上?
- Netty 线上消息慢、内存涨、解码错、频繁断开分别怎么排查?
关联知识点跳转
- Netty 总览
- Netty 从零到生产级掌握
- Netty 商业场景训练营
- Netty 基础
- Reactor 模型
- EventLoop
- ByteBuf
- ChannelPipeline
- 编解码
- 粘包拆包
- 实战入门
- Netty 面试题
- Java IO/NIO 全过程
本章小结
Netty 精通的关键不是背启动代码,而是理解网络字节流、NIO 多路复用、Reactor 线程模型、EventLoop 事件循环、Pipeline 责任链、ByteBuf 内存管理、协议边界、心跳、背压和排查证据。只有能解释一次连接、一次消息、一次写缓冲堆积、一次 ByteBuf 泄漏分别怎么发生,才算真正能把 Netty 用到商业项目里。
