三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Netty 详细讲解

Netty 详细讲解

目录

一、BIO / NIO / AIO 简单对比

Java NIO 三大组件

二、Netty 核心架构与线程模型(重中之重)

主从 Reactor 模型

NioEventLoop

三、Channel、Pipeline、ChannelHandler(核心三件套)

1. Channel

2. ChannelPipeline 流水线

3. ChannelHandler 处理器

ByteBuf(Netty 封装的缓冲区,替代 JDK ByteBuffer)

四、编解码器(解决粘包半包)

五、Netty 重要概念

1. ChannelFuture

2. ChannelOption / ChildOption

3. IdleStateHandler 空闲检测(业务心跳)

4. 零拷贝 Zero‑Copy

六、Netty 执行完整流程(服务端接收消息全过程)

七、常见坑(面试高频)

八、Netty 的 Reactor 面试高频问题

九、简单完整示例(服务端)

十、Netty 适合什么场景


Netty 是基于 Java NIO 的高性能、异步事件驱动的网络通信框架,封装了 Java 原生 NIO 的复杂 API,用于快速开发高并发的 TCP/UDP 网络服务端、客户端。

主流中间件底层网络全部用 Netty:RocketMQ、Dubbo、Elasticsearch、Zookeeper、Spring Cloud Gateway、Redis Java 客户端。

Java 网络演进:BIO → NIO(Java原生) → Netty(封装NIO)

一、BIO / NIO / AIO 简单对比

模型IO 类型线程模型缺点
BIO阻塞 IO1 连接 1 线程高并发下线程爆炸,上下文切换重
NIO(Java)非阻塞同步单线程 Selector 轮询原生 API 繁琐,BUG 多 (空轮询 bug)
AIO异步非阻塞操作系统回调Windows 好,Linux 内核支持差,实际很少用

Netty 底层在 Linux 上使用 NIO(Epoll),Windows 上会自动切换 AIO。生产环境几乎都是 NIO 模式

Java NIO 三大组件

  1. Buffer 缓冲区:数据读写容器(ByteBuffer)
  2. Channel 通道:双向读写(SocketChannel、ServerSocketChannel)
  3. Selector 选择器:多路复用器,一个线程监听大量 Channel 事件(连接、读、写),实现少量线程处理上万连接。

原生 NIO 痛点:

  • ByteBuffer API 难用,需要 flip、rewind、clear,容易内存溢出
  • Selector 空轮询 BUG (JDK bug),CPU 100%
  • 没有编解码器、断连处理、心跳、半包粘包,全部自己手写
  • 线程模型、异常处理需要自己实现

👉 Netty 就是把这些坑全部封装好。

二、Netty 核心架构与线程模型(重中之重)

Netty 核心线程模型:Reactor 反应器模式,Netty 实现了三种 Reactor:

  1. 单线程 Reactor(不生产用):一个线程干接受连接 + 处理 IO
  2. 多线程 Reactor:一个 accept 线程,N 个 IO 线程处理读写
  3. 主从 Reactor(Netty 默认)【生产常用】

主从 Reactor 模型

  • Boss Group(主 Reactor):负责接收 TCP 连接,只做 accept,不处理业务读写。一般 1 个线程足够。
  • Worker Group(从 Reactor):处理已建立连接的所有 IO 事件(read、write、编解码、触发 handler),N 个线程。

NioEventLoopGroup 就是线程组;NioEventLoop 就是单个线程。

java

// 服务端经典初始化代码 // boss线程组:接收连接;worker线程组:处理socket读写 NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 指定NIO通道实现 .option(ChannelOption.SO_BACKLOG,128) // tcp半连接队列 .childOption(ChannelOption.SO_KEEPALIVE,true) // tcp心跳 .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { // 流水线:添加处理器 ch.pipeline().addLast(new MyHandler()); } }); ChannelFuture future = bootstrap.bind(8888).sync(); future.channel().closeFuture().sync();
NioEventLoop
  1. 一个NioEventLoop对应一个 Java 线程 + 一个 Selector
  2. 一个 Worker NioEventLoop 可以绑定多个 SocketChannel,多路复用监听 IO 事件
  3. 一个 SocketChannel 永远只会绑定到同一个 NioEventLoop 线程,所以同一个 channel 的 handler 执行是单线程,不需要同步锁!(非常关键的特性)
  4. 任务队列:每个 NioEventLoop 有任务队列,支持普通任务、定时任务。

注意:不要在 Netty IO 线程执行耗时业务(数据库查询、http 调用),会阻塞整个线程,所有绑定这个线程的连接全部卡死! 耗时业务丢到自定义业务线程池执行。

三、Channel、Pipeline、ChannelHandler(核心三件套)

1. Channel

代表一条 Socket 连接,封装底层 Java NIO SocketChannel。 常用方法:

  • writeAndFlush():写消息并刷到内核缓冲区
  • close():关闭连接
  • channel().isActive()判断连接是否存活

2. ChannelPipeline 流水线

每个 Channel 内部持有一个ChannelPipeline,是Handler 的双向链表。 IO 事件会沿着 pipeline 依次流转。

inbound 入站事件:外部 → 服务端(连接建立、读数据)从 head→tail 执行 outbound 出站事件:服务端 → 外部(写数据、关闭)从 tail→head 执行

HeadContext <--> Handler1 <--> Handler2 <--> TailContext

3. ChannelHandler 处理器

分为两类

  1. ChannelInboundHandler 入站处理器:处理接收过来的数据,channelRead()读取报文
  2. ChannelOutboundHandler 出站处理器:处理向外发送的数据,write()

常用适配器简化开发:

  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • SimpleChannelInboundHandler:自动释放 ByteBuf,泛型直接拿到解码后的对象,业务 handler 最常用

ByteBuf(Netty 封装的缓冲区,替代 JDK ByteBuffer)

Netty 自己实现的缓冲区,解决原生 ByteBuffer 的痛点:

  1. 读写指针分离:readerIndexwriterIndex,不用 flip 切换
  2. 支持池化内存(PooledByteBuf,默认开启,复用直接内存,减少 GC)
  3. 支持复合缓冲区 CompositeByteBuf,组合多个缓冲区不拷贝
  4. 堆内存 (HeapByteBuf) / 直接内存 (DirectByteBuf,零拷贝,不走 JVM 堆)

⚠️内存释放:Netty 引用计数retain()/release(),DirectByteBuf 不释放会直接内存泄漏! SimpleChannelInboundHandler 会自动 release;普通 InboundHandler 拿到 ByteBuf,处理完必须手动 release。

四、编解码器(解决粘包半包)

TCP 是流协议,没有消息边界,会出现粘包、半包:多条报文粘在一起,或者一条报文被拆成多次到达。

Netty 内置开箱即用解码器:

  1. FixedLengthFrameDecoder:固定长度帧
  2. DelimiterBasedFrameDecoder:分隔符分割报文(比如换行符)
  3. LengthFieldBasedFrameDecoder【最常用】:长度域解码器,协议头部存消息长度,支持自定义偏移,RPC、MQ 大量使用。

解码器属于 InboundHandler;编码器属于 OutboundHandler。 编码:业务对象 → ByteBuf;解码:ByteBuf → 业务对象。

示例:Protobuf、JSON 都可以配合 LengthFieldBasedFrameDecoder 做私有二进制协议。

五、Netty 重要概念

1. ChannelFuture

Netty 所有 IO 操作全是异步,bind、connect、writeAndFlush 全部返回 ChannelFuture,不会阻塞。

  • .sync():阻塞等待操作完成
  • .addListener():异步回调(推荐,不阻塞线程)

java

// 异步回调写法 channel.writeAndFlush(msg).addListener(future -> { if(future.isSuccess()){ System.out.println("发送成功"); }else{ future.cause().printStackTrace(); } });

2. ChannelOption / ChildOption

  • Option:作用于服务端 ServerSocketChannel(接受连接的那个 socket)
  • ChildOption:作用于每一条客户端 SocketChannel

常用参数:

  • SO_BACKLOG:TCP 半连接队列大小
  • SO_KEEPALIVE:TCP 层面的空闲探测(不推荐业务心跳,粒度粗)
  • TCP_NODELAY:关闭 Nagle 算法,小报文立刻发送,低延迟系统开启

3. IdleStateHandler 空闲检测(业务心跳)

TCP keepalive 是内核层,业务感知差。Netty 提供空闲处理器,放在 pipeline,检测读空闲、写空闲,触发事件,我们发送心跳包,超时关闭僵死连接。

java

// 读空闲5秒,写空闲8秒,读写空闲10秒,触发IdleStateEvent ch.pipeline().addLast(new IdleStateHandler(5,8,10));

4. 零拷贝 Zero‑Copy

Netty 的零拷贝不是操作系统 mmap 那种,分几层:

  1. FileRegion:文件传输,直接把文件从磁盘发送到网卡,不拷贝到 JVM 内存
  2. CompositeByteBuf:逻辑合并多个缓冲区,不做内存复制
  3. Direct Buffer:使用操作系统直接内存,JVM 堆和 socket 之间少一次拷贝

六、Netty 执行完整流程(服务端接收消息全过程)

  1. BossGroup NioEventLoop 的 Selector 监听 OP_ACCEPT 事件,有客户端连接进来,accept 得到 SocketChannel
  2. 将 SocketChannel 注册到 WorkerGroup 其中一个 NioEventLoop 的 Selector 上,监听 OP_READ 事件
  3. 客户端发送数据,Worker 线程 Selector 触发 OP_READ
  4. 读到字节到 ByteBuf,进入 pipeline,从 head 开始执行各个 InboundHandler
  5. 首先经过解码器(LengthFieldBasedFrameDecoder)拆包,把 ByteBuf 转成业务 POJO 对象
  6. 执行业务 Handler channelRead () 方法
  7. 如果业务要返回响应,调用 writeAndFlush,事件反向走 OutboundHandler(编码器编码),最终发送到 socket 缓冲区

write () 只是写入缓冲区,不会真正发送;必须调用 flush 才会刷出;writeAndFlush 合并两个操作。

七、常见坑(面试高频)

  1. 禁止在 IO 线程执行耗时操作数据库、RPC、复杂计算会阻塞 NioEventLoop 线程,同线程所有连接卡死。解决方案:把任务提交到自定义业务线程池。

  2. DirectByteBuf 内存泄漏直接内存不在堆,GC 不会自动回收,必须 release;开启-Dio.netty.leakDetection.level=PARANOID检测内存泄漏。

  3. 粘包半包不要裸用 ByteBuf 接收业务报文,必须加解码器

  4. 不要混用同步 sync (),IO 线程内调用 sync () 造成死锁IO 线程中绝对不能调用 future.sync (),要用 addListener 异步回调。

  5. NioEventLoop 绑定 Channel 后,channel 所有 handler 都在这个线程执行,所以同一个 channel 内变量不需要加 synchronized;跨多个 channel 共享变量仍然需要锁。

  6. TCP 断开不一定触发 channelRead,一定要捕获channelInactive连接断开事件,清理会话。

八、Netty 的 Reactor 面试高频问题

Q:BossGroup 线程数为什么一般设置 1? ServerSocketChannel 只有一个,只负责 accept 接受连接,单线程足够;多线程反而会竞争 accept 锁,没有收益。

Q:WorkerGroup 默认线程数?CPU核心数*2,Netty 默认构造器就是这个值。

Q:Netty 怎么解决 JDK NIO Selector 空轮询 bug? 检测 Selector.select () 返回 0 但没有 IO 事件,达到阈值就重建新 Selector,把所有 channel 迁移过去。

Q:writeAndFlush 发送消息是立刻发送给客户端吗? 不是,写到 Netty 的发送缓冲区队列,由 NioEventLoop 在合适时机调用底层 socket write 系统调用刷入操作系统内核 socket 缓冲区;内核缓冲区再发给网络。

九、简单完整示例(服务端)

java

public class NettyServerDemo { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup boss = new NioEventLoopGroup(1); NioEventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { ChannelPipeline p = ch.pipeline(); // 简单按换行符分割报文 p.addLast(new DelimiterBasedFrameDecoder(1024, Delimiters.lineDelimiter())); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); // 业务handler p.addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println("收到客户端消息:" + msg); ctx.writeAndFlush("服务端回执:" + msg + "\r\n"); } @Override public void channelInactive(ChannelHandlerContext ctx) { System.out.println("客户端断开连接"); } }); } }); ChannelFuture f = b.bind(9000).sync(); f.channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }

十、Netty 适合什么场景

✅ 长连接服务、RPC 框架、消息中间件、网关、IM 聊天、私有二进制协议 ❌ 简单短连接 http 接口(直接用 SpringBoot web 即可,没必要手写 Netty)

如果你需要,我可以继续:

  1. 手写 Netty 客户端完整代码
  2. LengthFieldBasedFrameDecoder 详细图解(协议头长度域)
  3. 模拟半包粘包演示
  4. Netty 高频面试题整理
  5. Netty 源码核心流程梳理(NioEventLoop.run 循环)
← 返回列表