Skip to content

Netty 从零到精通验收清单

这页用来验收你是否真的把 Netty 学到了能理解原理、能写协议服务、能做商业项目、能排查线上问题、能面试回答的程度。

Netty 不是“写一个 Echo Server 就会了”。真正学懂要能解释:

  1. BIO 为什么一连接一线程扛不住大量长连接。
  2. NIO 的 Channel、Buffer、Selector、SelectionKey 怎么协作。
  3. Reactor 模型为什么能用少量线程处理大量连接。
  4. BossGroup、WorkerGroup、EventLoop、Channel 的关系。
  5. 一个连接从 accept 到注册、读事件、Pipeline 处理、写回的完整过程。
  6. Pipeline 入站和出站 Handler 为什么顺序不同。
  7. ByteBuf 为什么有读写指针、池化、堆外内存、引用计数和泄漏风险。
  8. TCP 粘包半包为什么发生,长度字段协议如何解决。
  9. 心跳、空闲检测、断线重连、连接清理怎么设计。
  10. 写缓冲、背压、水位线、慢客户端为什么会导致内存上涨。
  11. EventLoop 阻塞、direct memory 泄漏、解码错乱、连接暴涨如何排查。

总学习路线

mermaid
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 的典型模型是一连接一线程:

mermaid
flowchart TD
    A["连接1"] --> B["线程1 阻塞 read"]
    C["连接2"] --> D["线程2 阻塞 read"]
    E["连接3"] --> F["线程3 阻塞 read"]
    G["更多连接"] --> H["更多线程阻塞等待"]

如果做设备长连接、IM、实时推送,很多连接大部分时间只是在线,不一定一直发数据。BIO 模型下,空闲连接也可能占线程,连接数上来后会出现:

问题后果
线程数量过多内存占用高
上下文切换频繁CPU 被调度消耗拖垮
慢连接占线程其他连接排队
线程池满新请求无法处理
IO 和业务混在一起延迟和故障难排查

NIO 的思路是:连接不独占线程,线程监听一批连接的事件,哪个连接就绪就处理哪个连接。

mermaid
flowchart TD
    A["多个 SocketChannel"] --> B["Selector"]
    B --> C["少量 IO 线程"]
    C --> D["处理就绪的 read/write/accept 事件"]

这就是 Netty 高并发连接的基础。

阶段2:NIO 四个核心对象

对象作用零基础理解
Channel连接和数据通道像一根可以读写的管道
Buffer数据缓冲区读写网络字节的中转区
Selector多路复用器一个线程看很多连接有没有事件
SelectionKey注册关系和事件状态某个 Channel 在 Selector 上的事件卡片

NIO 服务端简化流程:

mermaid
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 难点很多:

  1. Selector 事件循环容易写错。
  2. 半包数据要自己保存和拼接。
  3. ByteBuffer 的 flipcompact 容易用错。
  4. 连接关闭、异常、空轮询要自己处理。
  5. 线程模型和业务线程池要自己设计。

Netty 的价值就是把这些底层细节做成稳定模型。

阶段3:Reactor 模型

Reactor 的核心思想:

IO 线程不阻塞等某一个连接,而是监听很多连接的事件,事件来了再分发给对应 Handler。

单 Reactor 单线程:

mermaid
flowchart TD
    A["Selector"] --> B["Reactor 线程"]
    B --> C["accept"]
    B --> D["read"]
    B --> E["decode"]
    B --> F["business"]
    B --> G["write"]

问题:所有事情都在一个线程里,业务一慢,整个服务都慢。

主从 Reactor:

mermaid
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 服务端启动全过程

mermaid
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:

java
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:一次连接接入全过程

mermaid
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"]

关键点:

  1. 一个客户端连接通常对应一个 Channel
  2. 一个 Channel 通常绑定一个固定 EventLoop
  3. 这个 EventLoop 后续负责它的读写事件。
  4. 同一个 EventLoop 会负责多个 Channel

这就是为什么 EventLoop 不能阻塞:一个慢任务不是只影响一个连接,而是影响同一个 EventLoop 上的所有连接。

阶段6:EventLoop 为什么不能阻塞

错误示例:

java
protected void channelRead0(ChannelHandlerContext ctx, Request request) {
    Order order = orderRepository.querySlowly(request.orderId());
    ctx.writeAndFlush(order);
}

如果 querySlowly 慢 2 秒,同一个 EventLoop 上其他连接的读写也会被拖慢。

正确思路:把慢业务交给有界业务线程池。

java
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 是一条责任链。

mermaid
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

顺序错误会导致:

  1. 业务 Handler 拿到未解码 ByteBuf。
  2. 鉴权在业务后执行,形成绕过风险。
  3. 编码器类型不匹配,响应写不出去。
  4. 异常没有统一处理。

阶段8:ByteBuf 内存模型

ByteBuf 有两个关键指针:

text
0 <= readerIndex <= writerIndex <= capacity
mermaid
flowchart TD
    A["ByteBuf"] --> B["readerIndex:下次从哪里读"]
    A --> C["writerIndex:下次写到哪里"]
    A --> D["readableBytes:可读字节"]
    A --> E["writableBytes:可写空间"]
    A --> F["refCnt:引用计数"]

为什么 Netty 不直接用 ByteBuffer

ByteBuffer 痛点ByteBuf 改进
读写切换要 flipreaderIndex/writerIndex 分离
扩容不方便ByteBuf 支持动态扩容
API 不够网络友好提供更多读写方法
池化和引用计数不明显Netty 做了池化内存管理

堆内和堆外:

类型特点
Heap ByteBufJVM 堆内,受 GC 管理
Direct ByteBuf堆外直接内存,减少一次拷贝,常用于网络 IO
Pooled ByteBuf池化复用,减少频繁分配和释放

引用计数:

java
ByteBuf buf = ctx.alloc().buffer();
System.out.println(buf.refCnt());
buf.release();

如果 release 漏掉,直接内存可能持续上涨。

阶段9:ByteBuf 泄漏怎么产生

常见泄漏场景:

场景原因
手动处理 ByteBuf 后忘记 release引用计数不归零
异常分支提前返回release 没执行
缓存 ByteBuf 但没 retain/release 配对所有权混乱
传递给异步线程使用原线程可能已释放或未正确 retain
使用 SimpleChannelInboundHandler 后继续持有 msg自动释放后再使用会出问题

相对安全写法:

java
public class PacketHandler extends SimpleChannelInboundHandler<Packet> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Packet packet) {
        Response response = handle(packet);
        ctx.writeAndFlush(response);
    }
}

如果必须手动处理 ByteBuf

java
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        // 读取和处理
    } finally {
        ReferenceCountUtil.release(buf);
    }
}

泄漏检测:

bash
-Dio.netty.leakDetection.level=advanced

生产长期最高级别会有性能开销,一般用于排查阶段。

阶段10:TCP 粘包半包

TCP 是字节流协议,不保留消息边界。

应用发送:

text
hello | world | netty

接收端可能读到:

text
helloworld | netty

也可能读到:

text
he | llowor | ldnetty

这不是 Netty bug,而是 TCP 的正常行为。

解决方式:

方式原理适合
固定长度每条消息固定字节数简单定长协议
分隔符\n 等分隔文本协议
长度字段包头写 body 长度二进制协议常用
HTTP/WebSocket使用成熟协议边界Web 长连接

生产最常见是长度字段协议。

阶段11:长度字段协议

协议设计:

text
magic(2) | version(1) | type(1) | length(4) | body(N)

流程:

mermaid
flowchart TD
    A["TCP 字节流"] --> B["LengthFieldBasedFrameDecoder"]
    B --> C["按 length 拆出完整帧"]
    C --> D["检查 magic 和 version"]
    D --> E["反序列化 body"]
    E --> F["业务 Packet"]

配置:

java
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 过期都可能让服务端连接长时间不释放。

mermaid
flowchart TD
    A["连接建立"] --> B["IdleStateHandler 检测读空闲"]
    B --> C{"超过读空闲时间"}
    C -- "否" --> D["连接继续保持"]
    C -- "是" --> E["发送心跳探测"]
    E --> F{"是否收到心跳响应"}
    F -- "是" --> D
    F -- "否" --> G["关闭 Channel"]
    G --> H["清理在线表和资源"]

配置:

java
ch.pipeline().addLast(new IdleStateHandler(60, 0, 0));
ch.pipeline().addLast(new HeartbeatHandler());

连接清理必须处理:

  1. 用户和 Channel 的绑定关系。
  2. 设备在线状态。
  3. 订阅关系。
  4. 未完成请求。
  5. 写缓冲和临时对象。

channelInactive 里不要做慢操作,可以快速清理内存状态,再异步落库或发消息。

阶段13:写缓冲和背压

如果业务持续写,客户端却接收慢,数据会堆在 Netty 写缓冲里。

mermaid
flowchart TD
    A["业务持续 writeAndFlush"] --> B["ChannelOutboundBuffer"]
    B --> C{"网络和对端是否写得出去"}
    C -- "能" --> D["缓冲下降"]
    C -- "不能" --> E["pending bytes 上升"]
    E --> F["Channel 变为不可写"]
    F --> G["内存上涨和延迟升高"]

设置水位:

java
bootstrap.childOption(
        ChannelOption.WRITE_BUFFER_WATER_MARK,
        new WriteBufferWaterMark(32 * 1024, 64 * 1024)
);

发送前判断:

java
if (channel.isWritable()) {
    channel.writeAndFlush(message);
} else {
    // 低优先级消息丢弃或降级,核心消息进入有限队列
}

背压设计:

  1. 区分核心消息和可丢弃消息。
  2. 写缓冲超过高水位时停止向该连接投递。
  3. 限制每个连接的待发送队列。
  4. 监控不可写时长和 pending bytes。
  5. 慢客户端不能拖垮整个服务。

阶段14:商业场景设计

设备采集长连接

mermaid
flowchart TD
    A["设备 TCP 连接"] --> B["Netty 接入层"]
    B --> C["长度字段拆包"]
    C --> D["协议解码"]
    D --> E["设备鉴权"]
    E --> F["心跳保活"]
    F --> G["采集数据投递 MQ"]
    G --> H["清洗、校验、入库"]

设计要点:

  1. 接入层不要直接慢入库。
  2. 原始报文要按 traceId 保存一份,方便排查。
  3. 协议要有 magic、version、type、length。
  4. 设备鉴权失败立即关闭连接。
  5. 心跳超时清理设备在线状态。
  6. 高峰期用 MQ 削峰。

IM / 实时推送

mermaid
flowchart TD
    A["用户连接"] --> B["Netty 长连接服务"]
    B --> C["登录鉴权"]
    C --> D["绑定 userId 和 Channel"]
    D --> E["消息投递"]
    E --> F{"用户是否在线"}
    F -- "在线" --> G["写入 Channel"]
    F -- "离线" --> H["保存离线消息"]

多实例要解决路由:

  1. Redis 保存用户在哪个实例。
  2. MQ 或内部 RPC 把消息发到对应实例。
  3. 实例下线要清理路由。
  4. 消息要有 ACK 和重试。

网关协议适配

mermaid
flowchart TD
    A["外部 TCP 协议"] --> B["Netty 解码"]
    B --> C["协议转换"]
    C --> D["内部 HTTP/RPC/MQ"]
    D --> E["业务系统"]

注意:协议网关要限制最大包、连接数、速率和非法协议,防止被恶意连接拖垮。

阶段15:生产排查

消息处理慢

mermaid
flowchart TD
    A["消息处理慢"] --> B["看 EventLoop 线程栈"]
    B --> C["是否阻塞在 DB/HTTP/锁/日志"]
    C --> D["业务线程池队列是否积压"]
    D --> E["下游服务是否慢"]
    E --> F["写缓冲是否堆积"]

direct memory 上涨

mermaid
flowchart TD
    A["direct memory 上涨"] --> B["开启 Netty 泄漏检测"]
    B --> C["检查 ByteBuf 是否 release"]
    C --> D["检查异常分支"]
    D --> E["检查异步持有 ByteBuf"]
    E --> F["检查写缓冲 pending bytes"]

解码异常

mermaid
flowchart TD
    A["解码异常"] --> B["抓原始报文"]
    B --> C["检查 magic/version"]
    C --> D["检查 length 字段偏移"]
    D --> E["检查序列化格式"]
    E --> F["检查客户端协议版本"]

连接频繁断开

mermaid
flowchart TD
    A["连接频繁断开"] --> B["看 channelInactive 原因"]
    B --> C["心跳超时是否过短"]
    C --> D["客户端是否重连风暴"]
    D --> E["网络或 NAT 是否断开"]
    E --> F["服务端是否主动关闭慢连接"]

写不出去

mermaid
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:

  1. BIO 一连接一线程为什么扛不住大量长连接?
  2. Selector、Channel、SelectionKey 是什么关系?
  3. Reactor 模型为什么要拆 Boss 和 Worker?
  4. 一个连接从 accept 到注册 Worker 的完整过程是什么?
  5. 为什么一个 EventLoop 阻塞会影响多个连接?
  6. Pipeline 入站和出站执行顺序有什么区别?
  7. ByteBuf 的 readerIndex 和 writerIndex 分别表示什么?
  8. ByteBuf 为什么会有 direct memory 泄漏?
  9. TCP 粘包半包为什么发生?
  10. 长度字段协议的 offset、length、adjustment 怎么理解?
  11. 心跳为什么不能只依赖 TCP keepalive?
  12. 写缓冲堆积说明什么?怎么做背压?
  13. 设备采集长连接如何避免入库阻塞 EventLoop?
  14. IM 多实例如何找到用户在哪个连接上?
  15. Netty 线上消息慢、内存涨、解码错、频繁断开分别怎么排查?

关联知识点跳转

本章小结

Netty 精通的关键不是背启动代码,而是理解网络字节流、NIO 多路复用、Reactor 线程模型、EventLoop 事件循环、Pipeline 责任链、ByteBuf 内存管理、协议边界、心跳、背压和排查证据。只有能解释一次连接、一次消息、一次写缓冲堆积、一次 ByteBuf 泄漏分别怎么发生,才算真正能把 Netty 用到商业项目里。