Netty系列:(一)Netty详解

2025-08-03 21:49
338
0

本篇作为Netty系列的第一篇,主要介绍下Netty的核心组件,让读者对Netty有个初步了解。后续还有两篇,一篇讲Netty的高性能的原因(Reactor线程模型、ByteBuf的内存复用、零拷贝机制等),一篇对Netty的源码进行解读。

一、Netty简介

Netty是一个Java开源框架,是一个高性能、高可扩展的异步事件驱动的网络应用程序框架,它极大的简化了TCP和UDP客户端和服务器的网络编程开发。

Netty源代码:github地址 | gitee地址 ,本文章基于Netty 4.1.45.Final版本进行介绍

Netty聊天室示例项目:gitee地址

按如上官方的图片可知,netty包含三大块:

  • 支持Socket等多种传输方式;
  • 提供多种协议的编解码实现;
  • 核心设计包含事件处理模型、API的使用、ByteBuffer的增强;

二、Netty的核心组件

  • Channel:通道,Netty中自己定义的Channel,算是增强版的NIO通道
  • EventLoopGroup:事件循环组,是EventLoop的容器,管理多个EventLoop实例。
  • EventLoop:事件循环,由线程驱动,处理Channel的所有I/O事件
  • ChannelPipeline:通道流水线,是ChannelHandler的容器,形成一个处理链,实现事件处理机制。
  • ChannelHandler:通道处理器,定义了处理Channel中事件的逻辑。
  • ByteBuf:增强的ByteBuf缓冲区(对NIO的ByteBuffer做了增强,实现了0拷贝、内存复用等)
  • Bootstrap:启动器,引导Netty应用程序启动

2.1、Channel

1、TCP 是面向连接的可靠传输协议,Netty 提供了针对不同场景的 TCP Channel 实现

  • NioServerSocketChannel:基于 Java NIO 的 TCP 服务器端 Channel,用于监听客户端连接请求,对应 Java NIO 中的 ServerSocketChannel。
  • NioSocketChannel:基于 Java NIO 的 TCP 客户端 Channel,用于与服务器建立 TCP 连接并进行数据传输,对应 Java NIO 中的 SocketChannel。
  • OioServerSocketChannel:基于 Java 传统阻塞 IO(OIO)的 TCP 服务器端 Channel,适用于需要兼容旧有阻塞 IO 场景(性能较差,不推荐用于高并发)。
  • OioSocketChannel:基于 Java OIO 的 TCP 客户端 Channel,同样用于阻塞式 TCP 通信。

2、UDP 是无连接的不可靠传输协议,适用于对实时性要求高的场景(如音视频传输)

  • NioDatagramChannel:基于 Java NIO 的 UDP Channel,支持 UDP 数据包的发送和接收,对应 Java NIO 中的 DatagramChannel。
  • OioDatagramChannel:基于 Java OIO 的 UDP Channel,用于阻塞式 UDP 通信。

3、本地传输相关的 Channel

  • LocalServerChannel:本地服务器端 Channel,用于监听同一 JVM 内的客户端连接请求。
  • LocalChannel:本地客户端 Channel,用于与同一 JVM 内的 LocalServerChannel 建立连接并通信。

4、其他特殊 Channel

  • EmbeddedChannel:嵌入式 Channel,主要用于单元测试。它允许在不启动实际网络连接的情况下,模拟 ChannelPipeline 中的事件传递和数据处理,方便测试 ChannelHandler 的逻辑。
  • EpollServerSocketChannel / EpollSocketChannel:基于 Linux 系统 epoll 机制的 TCP Channel(仅在 Linux 环境下可用),性能通常优于 NIO 实现,适合高并发场景。
  • KQueueServerSocketChannel / KQueueSocketChannel:基于 macOS/BSD 系统 kqueue 机制的 TCP Channel(仅在对应系统可用),类似 epoll,提供高效的 I/O 多路复用支持。

2.2、EventLoopGroup

在 Netty 中,EventLoopGroup 是事件循环的容器,负责管理多个 EventLoop 实例,是实现异步 I/O 操作的核心组件之一。它承担着线程管理、I/O 事件处理调度等关键职责,直接影响 Netty 应用的性能和并发能力。

1、管理 EventLoop 实例

EventLoopGroup 内部维护一个 EventLoop组(每个 EventLoop 通常绑定一个线程),负责为注册的 Channel 分配 EventLoop,并协调它们的工作。

如下图所示,MultithreadEventExecutorGroup是其它EventLoopGroup实现类的父类,用一个数组维护一组EventLoop。

2、调度能力

EventLoopGroup中有两种调度逻辑:一是对EventLoop的调度,还有一种是继承自 ScheduledExecutorService(JUC 中的定时任务接口)兼具事件循环和任务调度能力。

首先说说EventLoop的调度:

MultithreadEventExecutorGroup构造函数中会通过DefaultEventExecutorChooserFactory工厂初始化一个EventLoop选择器,是基于轮询(Round-Robin)策略。

MultithreadEventExecutorGroup中内置了两种选择器实现:

  • PowerOfTwoEventExecutorChooser是选择器的优化版本,通过逻辑与实现:executors[idx.getAndIncrement() & executors.length - 1],可以参考HashMap计算下标逻辑
  • GenericEventExecutorChooser是选择器通用版本,通过取模实现:executors[Math.abs(idx.getAndIncrement() % executors.length)],这种方式的效率没有优化版本好,并且注意:如果idx的值超过整数的最大值会导致数据溢出,此时前后获得的一部分数据是不公平的轮询进行了翻转。

如下图EventLoopGroup中会通过next方法,通过选择器选择下一个EventLoop

然后再讨论一下任务调度:

MultithreadEventExecutorGroup继承自AbstractEventExecutorGroup->EventExecutorGroup->ScheduledExecutorService(Java中的定时任务接口),然后在AbstractEventExecutorGroup抽象类中实现了调度的各个方法(下图截取了一部分)。

通过next方法获得EventLoop实例,再通过EventLoop执行任务调度去处理具体的业务逻辑。(EventLoop也继承了EventExecutor -> EventExecutorGroup -> ScheduledExecutorService

TIP: EventLoop和EventLoopGroup都实现了任务调度,但其设计目的是不一样的,EventLoopGroup是为了实现对EventLoop的负载均衡调度,EventLoop则是为执行业务层面的定时调度。

3、EventLoopGroup在服务端与客户端的角色分工

  • 在服务端,通常需要两个 EventLoopGroup:
    • bossGroup:负责监听端口、接收客户端连接,并将连接注册到 workerGroup。
    • workerGroup:负责处理已注册连接的 I/O 操作。
  • 在客户端,一般只需要一个 EventLoopGroup,负责处理与服务器的连接和 I/O 交互。

4、实现类部分介绍

  • NioEventLoopGroup:基于 Java NIO 模型是大多数场景的默认选择,使用 JDK 的 Selector 实现 I/O 多路复用,支持 TCP、UDP 等协议。
  • OioEventLoopGroup:(即将废弃)基于 Java 传统阻塞 I/O(OIO),使用阻塞式 socket,每个连接对应一个线程,不适合高并发场景。
  • EpollEventLoopGroup:是Linux 专属,基于 Linux 系统的 epoll 机制:直接调用 Linux 内核的 epoll 系统调用,性能优于 NioEventLoopGroup(尤其在高并发场景下)。
  • KQueueEventLoopGroup:是macOS/BSD 专属,基于 macOS 或 BSD 系统的 kqueue 机制:类似 Linux 的 epoll,是这些系统上的高效 I/O 多路复用实现。
  • LocalEventLoopGroup:用于本地传输,配合 LocalChannel 使用,实现同一 JVM 内不同组件间的通信,无需网络开销。适合进程内通信场景。

5、线程数量配置

  • 构造方法参数:创建 EventLoopGroup 时可指定线程数(如 new NioEventLoopGroup(4)),若不指定则使用默认值(CPU 核心数 * 2)
  • 服务端 bossGroup:通常设置为 1 个线程(因为接收连接的操作本身不耗时,单线程足够)。
  • workerGroup:线程数需根据业务场景调整,一般推荐设置为 CPU 核心数的 1~2 倍,避免线程过多导致上下文切换开销。

6、注意正确关闭释放资源

  • shutdownGracefully():优雅关闭,会等待正在执行的任务完成后再终止,推荐使用。
  • shutdown():强制关闭,立即终止所有任务,可能导致资源泄露。

2.3、EventLoop

EventLoop 是处理 I/O 事件和任务调度的核心组件,是实现异步非阻塞通信的基础。它绑定一个独立线程,通过循环执行事件处理和任务调度,确保同一 Channel 的所有操作在单线程中完成,从而避免线程安全问题并提升性能。

1、核心实现类

Netty 针对不同 I/O 模型和操作系统提供了多种 EventLoop 实现,与 EventLoopGroup 一一对应

  • NioEventLoop:基于 Java NIO 模型:配合 NioEventLoopGroup 使用,底层通过 JDK 的 Selector 实现 I/O 多路复用。
  • ThreadPerChannelEventLoop:(即将废弃) 基于 Java 传统阻塞 I/O(OIO):配合 OioEventLoopGroup 使用,采用阻塞式 socket 通信。
  • EpollEventLoop:基于 Linux 的 epoll 机制:配合 EpollEventLoopGroup 使用,直接调用 Linux 内核的 epoll 系统调用,性能优于 NioEventLoop。
  • KQueueEventLoop:基于 macOS/BSD 的 kqueue 机制:配合 KQueueEventLoopGroup 使用,是这些系统上的高效 I/O 多路复用实现,类似 Linux 的 epoll。
  • DefaultEventLoop:用于本地传输和一般任务,配合 LocalEventLoopGroup 和 LocalChannel 使用,实现同一 JVM 内组件间的通信。

如下图所示,通过MultithreadEventExecutorGroup中定义了抽象方法newChild,EvnetLoopGroup的实现类中会直接new出对应的EventLoop实现来完成一一对应。

Netty4.X的版本采用统一的SingleThreadEventLoop基类,统一EventLoop模型,所有EventLoop实现都继承自它。

SingleThreadEventLoop通过父类SingleThreadEventExecutor实现任务的调度逻辑,然后Bootstrap中会调用EventLoop的execute方法把线程传入来执行异步执行。

2.4、ChannelPipeline

ChannelPipeline 是 ChannelHandler 的容器,它采用责任链模式将多个 ChannelHandler 串联成一个处理链,负责接收、传递和处理 Channel 上的所有事件(如连接建立、数据读写、异常等),并支持灵活扩展(添加 / 移除 ChannelHandler)。

DefaultChannelPipeline 的核心特点:

  • 双向链表结构:内部通过双向链表存储 ChannelHandlerContext(每个节点对应一个 Handler),HeadContext 和 TailContext 分别为链表的头节点和尾节点,用户添加的 Handler 节点位于两者之间。
  • 线程安全:所有修改 Pipeline 结构的操作(如添加 / 移除 Handler)都通过 EventLoop 执行,保证线程安全(同一 Channel 的操作在同一线程执行)。
  • 事件传播优化:通过 AbstractChannelHandlerContext(ChannelHandlerContext 的抽象实现)的 fireXXX() 和 invokeXXX() 方法,高效调度事件到下一个 Handler。

DefaultChannelPipeline是ChannelPipeline的唯一实现类,摘取DefaultChannelPipeline中一部分源码如下图:

  • 它与Channel是一对一绑定的,每个Channle初始化的时候会创建对应的DefaultChannelPipeline,保证了线程的安全。
  • AbstractChannelHandlerContext中也会保存上一个节点与下一个节点,形成一个双向链表的结构。而链表中存储的ChannelHandlerContext 是 ChannelHandler 与 ChannelPipeline 之间的桥梁,每个 ChannelHandler 在添加到 Pipeline 时都会被包装为 ChannelHandlerContext,以便ChannelHandlerContext进行资源的关联、事件的传播、和Handler的管理等。

入站事件和出站事件的识别:

首先ChannelHandlerMask类中就标明了入站和出站各个事件的标识

// 入站事件 (Inbound Events)
static final int MASK_EXCEPTION_CAUGHT = 1;                    // 0b00000000000000001
static final int MASK_CHANNEL_REGISTERED = 1 << 1;            // 0b00000000000000010
static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;          // 0b00000000000000100
static final int MASK_CHANNEL_ACTIVE = 1 << 3;                // 0b00000000000001000
static final int MASK_CHANNEL_INACTIVE = 1 << 4;              // 0b00000000000010000
static final int MASK_CHANNEL_READ = 1 << 5;                  // 0b00000000000100000
static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;         // 0b00000000001000000
static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;          // 0b00000000010000000
static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;   // 0b00000000100000000

// 出站事件 (Outbound Events)
static final int MASK_BIND = 1 << 9;                          // 0b00000001000000000
static final int MASK_CONNECT = 1 << 10;                      // 0b00000010000000000
static final int MASK_DISCONNECT = 1 << 11;                   // 0b00000100000000000
static final int MASK_CLOSE = 1 << 12;                        // 0b00001000000000000
static final int MASK_DEREGISTER = 1 << 13;                   // 0b00010000000000000
static final int MASK_READ = 1 << 14;                         // 0b00100000000000000
static final int MASK_WRITE = 1 << 15;                        // 0b01000000000000000
static final int MASK_FLUSH = 1 << 16;                        // 0b10000000000000000

然后把这些标识传入对应的入站或出站方法,通过ChannelHandler的executionMask字段(表示该Handler支持哪些事件类型)与运算,结果为0的就说明该Handler不支持该事件类型。

从上图中也能看出来,入站事件的传播方向是从Pipeline的头部向尾部传播,出站事件的传播方向是从Pipeline的尾部向头部传播。

ChannelHandler的executionMask的值是先把所有的掩码都进行或运算一遍,没有此事件再摘除此掩码的值,展示部分逻辑如下:

2.5、ChannelHandler

ChannelHandler 是处理网络事件(如连接建立、数据读写、异常等)的核心组件。它通过 ChannelPipeline 形成责任链,对流经的事件进行加工、转发或终止,是 Netty 灵活性和可扩展性的关键。

1、入站处理器

入站处理器的基类是ChannelInboundHandler接口,核心方法如下:

  • channelRegistered:Channel 注册到 EventLoop 时会被调用
  • channelUnregistered:Channel 从 EventLoop 注销时会被调用
  • channelActive:Channel 激活(连接建立成功)时会被调用
  • channelInactive:Channel inactive(连接关闭)时
  • channelRead:接收到数据时(最常用,处理业务数据)会被调用
  • channelReadComplete:数据读取完成时(可用于批量处理)会被调用
  • userEventTriggered: 当用户事件被触发时调用
  • channelWritabilityChanged:当Channel的可写状态发生变化时被调用
  • exceptionCaught 发生异常时(如解码失败、网络错误)会被调用

事件传播方向:从 ChannelPipeline 头部向尾部传播(按 Handler 添加顺序执行)。

2、出站处理器

出站处理器的基类是ChannelInboundHandler接口,核心方法如下:

  • bind:绑定端口时触发
  • connect:连接远程服务器时触发
  • disconnect:断开连接时触发
  • close:关闭 Channel 时触发
  • deregister:从当前注册的EventLoop中注销操作被调用时触发
  • read:主动读取数据时触发
  • write:发送数据时(将数据写入缓冲区)触发
  • flush:刷新缓冲区(将数据实际发送出去)触发

事件传播方向:从 ChannelPipeline 尾部向头部传播(与添加顺序相反)。

3、适配器类

为避免实现 ChannelInboundHandler 或 ChannelOutboundHandler 的所有方法,Netty 提供了适配器,开发者只需重写需要的方法:

  • ChannelInboundHandlerAdapter:ChannelInboundHandler 的默认适配器,所有方法默认将事件传播给下一个 Handler(调用 ctx.fireXXX())。
  • ChannelOutboundHandlerAdapter:ChannelOutboundHandler 的默认适配器,所有方法默认将事件传播给上一个 Handler(调用 outboundOperation(...) 的父类实现)。
  • ChannelDuplexHandler:同时实现 ChannelInboundHandler 和 ChannelOutboundHandler,适用于需要同时处理入站和出站事件的场景(如日志记录)。

4、事件的传播和终止

  • 在自定义 ChannelInboundHandler 或 ChannelOutboundHandler 时,必须手动调用 ChannelHandlerContext 的 fireXXX() 方法(针对入站事件)或对应的出站操作方法(针对出站事件),才能将事件传递给下一个 Handler;如果不调用,事件会在当前 Handler 中终止传播。
  • Netty 提供的适配器类(如 ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter)中,所有事件处理方法的默认实现就是调用对应的传播方法,确保事件能自动向下一个 Handler 传递。


 

 

全部评论