本篇作为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 传递。
全部评论