Skip to content

Netty 从零到生产级掌握

Netty 不能只学成“高性能 NIO 框架”。真正到商业项目里,你要能解释:为什么 BIO 一连接一线程扛不住高并发,NIO 的 Selector 怎么减少线程,Reactor 为什么拆 Boss 和 Worker,EventLoop 为什么不能阻塞,Pipeline 为什么是责任链,ByteBuf 为什么要引用计数,TCP 为什么会粘包半包,心跳和连接管理怎么做,线上连接数高、内存泄漏、消息积压、写不出去时怎么排查。

一句话建立主线:

Netty 是基于 Java NIO 的异步事件驱动网络框架,用 Reactor 线程模型、EventLoop、Channel、Pipeline、ByteBuf 和编解码器,把复杂网络通信封装成可扩展的事件处理链。

如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 Netty 从零到精通验收清单 逐项验收。

学习目标

学完这一页,你要能做到:

  1. 解释 BIO、NIO、AIO 的差异,以及 Netty 为什么选择 NIO 事件驱动。
  2. 解释 Reactor 模型、BossGroup、WorkerGroup 的职责。
  3. 解释 Channel、EventLoop、Selector、SelectionKey 的关系。
  4. 解释一个连接从 accept 到 read、decode、handle、encode、write 的全过程。
  5. 解释 Pipeline 入站、出站 Handler 的执行顺序。
  6. 解释 ByteBuf 的读写指针、堆内/堆外、池化、引用计数和泄漏。
  7. 解释 TCP 粘包半包为什么发生,以及长度字段协议怎么解决。
  8. 解释心跳、空闲检测、断线重连、连接清理、背压和写缓冲水位。
  9. 能写一个最小可运行 Netty 服务端和长度字段协议 Demo。
  10. 能按 IM、设备长连接、采集网关、RPC、协议适配等商业场景设计 Netty 链路。
  11. 能排查 EventLoop 阻塞、直接内存泄漏、连接暴涨、写不出去、解码异常。

学习路线

mermaid
flowchart TD
    A["网络基础<br/>TCP、连接、字节流"] --> B["Java IO/NIO<br/>BIO、Channel、Buffer、Selector"]
    B --> C["Reactor 模型<br/>事件分发"]
    C --> D["Netty 线程模型<br/>BossGroup、WorkerGroup"]
    D --> E["Channel 和 EventLoop"]
    E --> F["Pipeline 和 Handler"]
    F --> G["ByteBuf 内存管理"]
    G --> H["编解码和协议边界"]
    H --> I["心跳、连接管理、背压"]
    I --> J["生产排查和调优"]

第一步:为什么 BIO 不适合大量长连接

传统 BIO 常见模型是一连接一线程:

mermaid
flowchart TD
    A["客户端连接 1"] --> B["线程 1 阻塞读写"]
    C["客户端连接 2"] --> D["线程 2 阻塞读写"]
    E["客户端连接 3"] --> F["线程 3 阻塞读写"]
    G["更多连接"] --> H["更多线程"]

如果有 10 万个长连接,大部分连接可能只是保持在线,很少发消息。但 BIO 仍然可能为连接准备大量线程或阻塞等待:

问题后果
一连接一线程线程数量巨大
大量线程阻塞内存和上下文切换成本高
慢连接占线程其他请求等待
业务和 IO 混在一起排查困难

NIO 的思路是:连接不再独占线程,线程通过 Selector 监听多个 Channel 的就绪事件,哪个连接有读写事件就处理哪个。

mermaid
flowchart TD
    A["多个 Channel"] --> B["Selector"]
    B --> C["少量 IO 线程"]
    C --> D["处理就绪事件"]

这就是 Netty 能支撑大量连接的基础。

第二步:NIO 的核心对象

Java NIO 有几个核心对象:

对象作用
Channel数据通道,代表连接或服务端监听
Buffer字节缓冲区,读写数据
Selector监听多个 Channel 的事件
SelectionKeyChannel 注册到 Selector 后的事件句柄

简化流程:

mermaid
flowchart TD
    A["ServerSocketChannel"] --> B["注册到 Selector"]
    B --> C["监听 ACCEPT 事件"]
    C --> D["接收 SocketChannel"]
    D --> E["SocketChannel 注册 READ 事件"]
    E --> F["Selector 发现可读"]
    F --> G["读取 ByteBuffer"]

直接使用 NIO 的难点:

  1. Selector 空轮询、事件注册和取消很复杂。
  2. ByteBuffer 使用不直观。
  3. 半包粘包要自己处理。
  4. 线程模型要自己设计。
  5. 异常、关闭、重连、背压都要自己处理。

Netty 就是在这些底层能力上提供工程化封装。

第三步:Reactor 模型

Reactor 是事件驱动模型。核心思想:

线程不阻塞等待某个连接,而是等待一批连接的事件,哪个事件就绪就分发给对应处理器。

单 Reactor 单线程:

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

问题:所有事都在一个线程里,业务慢会拖慢 IO。

主从 Reactor:

mermaid
flowchart TD
    A["Boss Reactor"] --> B["接收新连接"]
    B --> C["注册给 Worker Reactor"]
    C --> D["Worker 1 处理读写"]
    C --> E["Worker 2 处理读写"]
    C --> F["Worker 3 处理读写"]

Netty 服务端常见就是 BossGroup + WorkerGroup:

组件职责
BossGroup监听端口,接收新连接
WorkerGroup处理已建立连接的读写事件

为什么要拆:

  1. 接收连接要快,不能被业务处理拖慢。
  2. 已连接的读写事件数量远大于 accept。
  3. Worker 可以多线程分摊大量连接。

第四步:Netty 服务端启动全过程

mermaid
flowchart TD
    A["创建 BossGroup 和 WorkerGroup"] --> B["创建 ServerBootstrap"]
    B --> C["配置 channel 类型"]
    C --> D["配置 childHandler"]
    D --> E["bind 端口"]
    E --> F["BossGroup 注册 ServerSocketChannel"]
    F --> G["监听 ACCEPT"]
    G --> H["客户端连接进入"]
    H --> I["创建 SocketChannel"]
    I --> J["注册到 Worker EventLoop"]
    J --> K["初始化 Pipeline"]

最小服务端:

java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyEchoServer {
    public static void main(String[] args) throws InterruptedException {
        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());
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                ctx.writeAndFlush("echo:" + msg);
                            }
                        });
                    }
                });

            ChannelFuture future = bootstrap.bind(8080).sync();
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

这个 Demo 只适合入门。真实生产一定要补协议边界、异常处理、业务线程池、心跳、连接管理和日志。

第五步:EventLoop 为什么不能阻塞

EventLoop 通常绑定一个线程,并负责多个 Channel 的 IO 事件。

mermaid
flowchart TD
    A["EventLoop 线程"] --> B["Channel A 读事件"]
    A --> C["Channel B 写事件"]
    A --> D["Channel C 关闭事件"]
    A --> E["定时任务和异步任务"]

如果你在 Handler 里做慢 SQL:

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

问题:

  1. 当前 EventLoop 被慢 SQL 阻塞。
  2. 同一个 EventLoop 上的其他连接不能及时读写。
  3. P99 延迟变高。
  4. 客户端可能超时重试。
  5. 连接越来越多,写缓冲堆积。

正确做法:慢业务投递到业务线程池。

java
private final ExecutorService businessPool = Executors.newFixedThreadPool(16);

protected void channelRead0(ChannelHandlerContext ctx, Request request) {
    businessPool.submit(() -> {
        Response response = orderService.handle(request);
        ctx.writeAndFlush(response);
    });
}

注意:业务线程池也要有界队列、拒绝策略和监控,否则只是把阻塞从 EventLoop 转移到业务线程池。

第六步:Channel、Pipeline、Handler

Channel 是连接抽象。Pipeline 是这条连接上的处理链。Handler 是链上的处理器。

mermaid
flowchart TD
    A["字节流进入 Channel"] --> B["Pipeline 入站"]
    B --> C["Frame Decoder"]
    C --> D["Message Decoder"]
    D --> E["Auth Handler"]
    E --> F["Business Handler"]
    F --> G["Pipeline 出站"]
    G --> H["Message Encoder"]
    H --> I["写回客户端"]

入站和出站方向不同:

类型方向例子
Inbound网络数据进入应用解码、鉴权、业务处理
Outbound应用数据写回网络编码、压缩、加密

Pipeline 顺序非常重要:

text
长度字段拆包器 -> 协议解码器 -> 鉴权 Handler -> 业务 Handler -> 协议编码器

如果顺序错了:

  1. 业务 Handler 可能拿到原始 ByteBuf,而不是业务对象。
  2. 鉴权可能绕过。
  3. 编码器可能处理不了响应类型。
  4. 异常传播不完整。

第七步:ByteBuf 为什么重要

ByteBuf 是 Netty 的字节缓冲区。它比 Java NIO ByteBuffer 更适合网络编程。

核心特性:

特性说明
readerIndex读指针
writerIndex写指针
capacity容量
heap buffer堆内内存
direct buffer堆外直接内存
pooled buffer池化复用,减少分配
reference count引用计数,控制释放
mermaid
flowchart TD
    A["ByteBuf"] --> B["readerIndex"]
    A --> C["writerIndex"]
    A --> D["readableBytes"]
    A --> E["writableBytes"]
    A --> F["引用计数 refCnt"]

为什么会泄漏?

  1. Netty 使用池化和直接内存。
  2. ByteBuf 可能不由 JVM 堆 GC 立即管理。
  3. 引用计数没有释放,内存回不到池。
  4. 异常分支忘记 release。

相对安全的写法:

java
public class MyHandler extends SimpleChannelInboundHandler<MyMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) {
        ctx.writeAndFlush(handle(msg));
    }
}

SimpleChannelInboundHandler 默认会在处理后释放入站消息。如果你手动持有或转发 ByteBuf,要理解 retain/release。

泄漏排查:

bash
-Dio.netty.leakDetection.level=advanced

生产不建议长期最高级别,因为有性能开销。

第八步:TCP 粘包半包为什么发生

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

发送端认为发了三条消息:

text
msg1 | msg2 | msg3

接收端可能读到:

text
msg1msg2 | msg3

也可能读到:

text
ms | g1msg | 2msg3

这不是 Netty 的问题,而是 TCP 字节流特性。

解决方案:

方案原理适合
固定长度每条消息固定 N 字节简单协议,浪费空间
分隔符\n 等分隔文本协议
长度字段消息头保存 body 长度最常用二进制协议
HTTP/WebSocket使用成熟协议Web 长连接

第九步:长度字段协议 Demo

协议格式:

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

流程:

mermaid
flowchart TD
    A["收到 TCP 字节流"] --> B["LengthFieldBasedFrameDecoder"]
    B --> C["按 length 拆出完整帧"]
    C --> D["自定义 MessageDecoder"]
    D --> E["业务消息对象"]
    E --> F["业务 Handler"]

Netty 拆包器配置示例:

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。

第十步:心跳和连接管理

长连接必须有心跳。否则服务端不知道客户端是真的空闲,还是网络断了但连接还没释放。

mermaid
flowchart TD
    A["连接建立"] --> B["IdleStateHandler 检测空闲"]
    B --> C{"读空闲超时"}
    C -- "否" --> D["继续保持连接"]
    C -- "是" --> E["发送心跳或关闭连接"]
    E --> F{"心跳响应正常"}
    F -- "是" --> D
    F -- "否" --> G["关闭连接并清理资源"]

示例:

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

心跳设计注意:

  1. 心跳间隔不要太短,否则大量空包浪费带宽。
  2. 心跳超时不要太激进,避免网络抖动误踢。
  3. channelInactive 要清理在线用户表、设备会话、订阅关系。
  4. 客户端要支持断线重连和退避。

第十一步:写缓冲和背压

Netty 写数据不是永远立即写到网卡。如果对端接收慢,或者网络拥塞,写缓冲会增长。

mermaid
flowchart TD
    A["业务持续 write"] --> B["ChannelOutboundBuffer"]
    B --> C{"对端接收是否足够快"}
    C -- "是" --> D["数据写出"]
    C -- "否" --> E["缓冲堆积"]
    E --> F["内存上涨"]
    F --> G["触发不可写或 OOM 风险"]

可以通过水位控制:

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

业务发送前判断:

java
if (ctx.channel().isWritable()) {
    ctx.writeAndFlush(response);
} else {
    // 降级、丢弃低优先级消息、记录告警
}

背压思路:

  1. 对端慢时不要无限写。
  2. 区分核心消息和非核心消息。
  3. 写缓冲高水位时降级或暂停发送。
  4. 监控 pending bytes、写失败、连接数。

第十二步:生产架构怎么设计

设备采集长连接

mermaid
flowchart TD
    A["设备 TCP 连接"] --> B["Netty 网关"]
    B --> C["协议解码"]
    C --> D["设备鉴权"]
    D --> E["心跳保活"]
    E --> F["采集数据入队"]
    F --> G["MQ 或业务线程池"]
    G --> H["清洗和入库"]

设计要点:

  1. 连接接入和业务处理隔离。
  2. 协议要有长度字段和版本号。
  3. 设备鉴权后再处理业务消息。
  4. 心跳超时清理连接。
  5. 入库不要阻塞 EventLoop。
  6. 采集高峰用 MQ 削峰。
  7. 保留原始报文方便排查。

IM 或实时推送

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

设计要点:

  1. 用户和 Channel 绑定。
  2. 多实例要通过 Redis/MQ 路由消息。
  3. 写不出去要处理背压。
  4. ACK 和离线消息要有可靠性设计。

第十三步:线上排查总流程

mermaid
flowchart TD
    A["Netty 线上问题"] --> B{"表现是什么"}
    B -- "连接很多但消息慢" --> C["看 EventLoop 是否阻塞"]
    B -- "内存上涨" --> D["看 ByteBuf 泄漏和写缓冲"]
    B -- "解码失败" --> E["查协议边界和长度字段"]
    B -- "连接频繁断开" --> F["查心跳、网络、客户端重连"]
    B -- "CPU 高" --> G["查编解码、日志、业务线程池"]
    C --> H["线程栈、任务耗时、业务是否阻塞"]
    D --> I["leak detector、direct memory、pending bytes"]
    E --> J["抓包、原始报文、协议版本"]

排查证据:

证据看什么
线程栈EventLoop 是否卡在业务代码、锁、日志、DB
连接数是否异常暴涨、是否有空闲连接未清理
direct memoryByteBuf 是否泄漏
pending write bytes对端慢导致写缓冲堆积
原始报文协议长度、magic、版本、类型是否正确
心跳日志是否误踢、是否客户端断连
GC 日志堆内对象、业务线程池是否堆积
业务队列慢任务是否积压

常见坑

后果正确做法
EventLoop 做慢 SQL同线程多个连接一起慢投递业务线程池
没有协议边界粘包半包导致解析错长度字段或成熟协议
忘记释放 ByteBuf直接内存泄漏使用 SimpleChannelInboundHandler 或正确 release
Handler 顺序错解码、鉴权、编码异常按入站/出站顺序设计
写缓冲不控对端慢导致内存涨水位、isWritable、降级
心跳太短网络抖动导致误踢合理超时和重连退避
日志打印原始大包CPU/IO 飙升采样、截断、异步日志

面试标准回答

Netty 是什么

text
Netty 是基于 Java NIO 的异步事件驱动网络框架。它封装了 Selector、Channel、Buffer、线程模型、编解码、Pipeline、内存管理和连接管理,适合长连接、RPC、网关、IM、设备采集协议等高并发网络场景。

Netty 为什么高性能

text
Netty 高性能不是单点原因,而是 NIO 非阻塞 IO、Reactor 线程模型、Boss/Worker 分工、EventLoop 绑定 Channel、Pipeline 责任链、ByteBuf 池化和堆外内存、零拷贝以及成熟编解码机制共同作用。它用少量线程处理大量连接,并避免一连接一线程的阻塞成本。

EventLoop 为什么不能阻塞

text
一个 EventLoop 线程通常负责多个 Channel 的 IO 事件。如果在 Handler 里执行慢 SQL、远程调用、大计算或同步日志,会阻塞同一个 EventLoop 上其他连接的读写,导致延迟抖动和连接积压。慢业务应投递到有界业务线程池,并做好超时、拒绝和监控。

粘包半包怎么解决

text
TCP 是字节流协议,不保留应用层消息边界,所以多个消息可能粘在一起,一个消息也可能被拆开。解决方式是在应用层定义协议边界,例如固定长度、分隔符或长度字段。生产中常用 LengthFieldBasedFrameDecoder 按长度字段拆出完整帧,再交给业务解码器。

关联知识点

知识点继续学习
从零到精通验收Netty 从零到精通验收清单
商业场景训练营Netty 商业场景训练营
Netty 基础基础入门
ReactorReactor模型
EventLoopEventLoop
PipelineChannelPipeline
ByteBufByteBuf
编解码编解码
粘包拆包粘包拆包
实战入门实战入门
面试题Netty面试题
Java NIOJava IO/NIO全过程

本章小结

Netty 从零到生产级掌握,关键是把网络字节流、NIO、Reactor、EventLoop、Pipeline、ByteBuf、协议编解码、心跳、连接管理、背压和生产排查串起来。只会写一个 echo server 还不够,必须知道为什么 EventLoop 不能阻塞、为什么 TCP 要拆包、为什么 ByteBuf 会泄漏、为什么慢客户端会导致写缓冲堆积,以及这些问题在线上怎么定位。