目录
Netty 是什么?
Netty 是一个基于 Java NIO 的客户端/服务器端通信框架,旨在简化网络编程的开发过程。它不仅支持高并发、高扩展性,还具备良好的可维护性,非常适合用于构建高性能的网络应用程序。Netty 的核心实现基于 Reactor 模式,封装了底层复杂的事件驱动机制,使开发者能够专注于业务逻辑而无需处理底层通信细节。
通过使用 Netty,开发者可以轻松实现 TCP、UDP、HTTP 等常见通信协议的支持,从而大大简化了底层通信的编程工作。无论是 TCP、UDP 这样的低层传输协议,还是 FTP、SMTP、HTTP 等应用层协议,Netty 都能提供统一且高效的编程模型,帮助开发者快速搭建稳定的网络服务端或客户端应用。
Netty 的目标
Netty 的设计目标是帮助开发者快速而轻松地构建各类通信应用程序。它通过抽象底层复杂的网络通信细节,提供了清晰且统一的编程接口,使得网络程序的开发更加高效和简洁。
在实现高性能通信方面,Netty 支持多种通信协议,能够处理包括 TCP、UDP 在内的传输层协议,以及 HTTP、FTP 等应用层协议。同时,Netty 基于异步非阻塞的通信机制,结合事件驱动的架构,有效提升了系统在高并发场景下的响应能力和吞吐量。
为了实现灵活的通信控制,Netty 提供了一系列核心组件,包括用于数据传输的 Channel、用于处理通信逻辑的 Handler,以及用于执行 I/O 事件循环的 EventLoopGroup 等。这些组件相互协作,构成了 Netty 强大而灵活的通信框架核心。
Netty 实战案例 DiscardServer
什么是 DiscardServer?
一个丢弃消息的服务器:接收客户端消息后不做任何处理直接丢弃,相当于 Netty 的 “HelloWorld”
服务端程序 NettyDiscardServer
下面的代码很多地方是新知识,先大体了解代码功能,后续会进行详细讲解
服务端核心步骤
1. 创建反应器线程组(EventLoopGroup)
2. 配置 ServerBootstrap
3. 启动服务器
bind().sync() 同步绑定。
closeFuture().sync() 同步关闭监听。
shutdownGracefully() 优雅关闭线程组。
// 定义一个Netty丢弃服务器类,功能是接收数据并直接丢弃
public class NettyDiscardServer {
// 服务器监听的端口号
private final int serverPort;
// Netty的服务端启动类,用于配置和启动服务器
ServerBootstrap b = new ServerBootstrap();
// 构造函数,传入服务器端口
public NettyDiscardServer(int port) {
this.serverPort = port;
}
// 运行服务器的方法
public void runServer() {
// 创建boss事件循环组,处理连接请求,参数1表示使用1个线程
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
// 创建worker事件循环组,处理IO操作,不指定参数则使用默认线程数
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
// 配置服务器引导类
b.group(bossLoopGroup, workerLoopGroup); // 设置事件循环组
b.channel(NioServerSocketChannel.class); // 指定使用NIO传输通道
b.localAddress(serverPort); // 设置服务器监听地址和端口
b.option(ChannelOption.SO_KEEPALIVE, true); // 设置TCP保持连接选项
// 配置子通道的处理器
b.childHandler(new ChannelInitializer<SocketChannel>() {
// 初始化通道时调用
protected void initChannel(SocketChannel ch) {
// 向管道添加自定义的处理器
ch.pipeline().addLast(new NettyDiscardHandler());
}
});
// 绑定服务器并同步等待绑定完成
ChannelFuture channelFuture = b.bind().sync();
// 打印服务器启动成功信息
System.out.println("服务器启动成功,监听端口: " +
channelFuture.channel().localAddress());
// 获取通道关闭的Future
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
// 同步等待服务器通道关闭
closeFuture.sync();
} catch (Exception e) {
e.printStackTrace(); // 打印异常堆栈
} finally {
// 优雅关闭事件循环组
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
// 主方法,程序入口
public static void main(String[] args) {
// 从配置类获取端口号
int port = NettyDemoConfig.SOCKET_SERVER_PORT;
// 创建服务器实例并运行
new NettyDiscardServer(port).runServer();
}
}
关键步骤:
1. 创建反应器线程组
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1); // 负责接收客户端连接
EventLoopGroup workerLoopGroup = new NioEventLoopGroup(); // 负责处理 IO 读写
bossGroup:只负责 accept 新连接
workerGroup:负责处理每个连接的读写事件(比如数据收发)
2. 配置启动器 ServerBootstrap
b.group(bossLoopGroup, workerLoopGroup);
b.channel(NioServerSocketChannel.class); // 使用 NIO 的 ServerSocketChannel
b.localAddress(serverPort); // 绑定监听端口
b.option(ChannelOption.SO_KEEPALIVE, true); // 保持长连接
channel():指定使用 NIO 网络模型
option():配置服务器通道参数,如是否保持连接
NioServerSocketChannel 是 Netty 中专门用于 NIO 模式下的 TCP 服务器端通道,对应底层的 ServerSocketChannel(Java NIO 中的 TCP 服务器套接字通道),专门处理 TCP 协议的连接请求
ChannelOption.SO_KEEPALIVE 是 TCP 协议的保活机制选项,用于设置是否开启 TCP 连接的保活功能。
当该选项设置为 true 时:
如果客户端和服务器长时间没有数据交互(默认通常是 2 小时左右,具体时间取决于系统配置),TCP 协议会自动发送探测数据包,检测对方是否还在线。
如果对方正常响应,说明连接仍然有效,继续保持连接;
如果多次探测无响应,会判定连接已失效,自动关闭连接,释放资源。
3. 设置子通道处理器
b.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyDiscardHandler());
}
});
每当有客户端连接成功,就会初始化它的通道(SocketChannel)
向 pipeline 添加处理器 NettyDiscardHandler
业务处理器 NettyDiscardHandler
NettyDiscardHandler 是服务端通道的自定义处理器,用于读取并丢弃客户端发送的数据。
// 自定义的Netty通道处理器,继承自ChannelInboundHandlerAdapter
public class NettyDiscardHandler extends ChannelInboundHandlerAdapter {
// 当通道有数据可读时调用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 将消息转换为ByteBuf类型
ByteBuf in = (ByteBuf) msg;
try {
System.out.println("收到消息,丢弃如下:");
// 循环读取ByteBuf中的可读字节
while (in.isReadable()) {
// 逐个字节读取并打印为字符
System.out.print((char) in.readByte());
}
System.out.println(); // 打印换行
} finally {
// 释放消息资源,防止内存泄漏
ReferenceCountUtil.release(msg);
}
}
}
步骤 | 说明 |
---|---|
channelRead() | 当通道收到客户端数据时触发 |
msg → ByteBuf | 将消息强制转换为 ByteBuf 类型 |
while (in.isReadable()) | 遍历 ByteBuf 中所有可读字节 |
readByte() | 逐字节读取并打印内容 |
ReferenceCountUtil.release() | 释放 ByteBuf 引用,防止内存泄漏 |
为什么需要手动释放 ByteBuf?
Netty 使用引用计数机制管理内存,ByteBuf 在读取后必须显示释放,否则会导致内存泄漏。ReferenceCountUtil.release(msg) 等价于调用 ((ByteBuf) msg).release(),但更加通用安全。
在 Netty 中,基于引用计数机制(ReferenceCounted 接口)管理的对象(如 ByteBuf),需要开发者显式调用释放方法来回收资源,这就是手动释放的含义 —— 不像 JVM 堆内存那样由垃圾回收器自动回收,而是需要代码中主动触发释放。
ReferenceCountUtil.release(msg) 的作用是:
1. 检查 msg 是否是引用计数对象(实现了 ReferenceCounted 接口,比如 ByteBuf)
2. 如果是,则调用其 release() 方法减少引用计数,当计数减为 0 时,对象会被释放(回收内存)
3. 如果不是引用计数对象,该方法会忽略操作,避免报错。
配置类 NettyDemoConfig
// Netty演示配置类
public class NettyDemoConfig {
// 定义服务器端口常量
public static final int SOCKET_SERVER_PORT = 12345;
}
如果没有客户端发送数据,只启动服务器无法验证效果。
所以可以使用之前的 NioDiscardClient 客户端,在下面链接有讲
详解 NIO Selector_java nio selectors 工作原理-CSDN博客
下面是完整的客户端代码,运行时需要先启动服务端,再启动客户端
public class NioDiscardClient {
// 静态的 Logger 实例
private static final Logger logger = LoggerFactory.getLogger(NioDiscardClient.class);
public static void startClient() throws IOException {
// 1. 配置服务器地址和端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 12345);
// 2. 获取 SocketChannel 并连接到服务器
SocketChannel socketChannel = SocketChannel.open(address);
// 3. 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 4. 等待连接建立完成
while (!socketChannel.finishConnect()) {
// 自旋等待连接完成
}
logger.info("客户端连接成功");
// 5. 准备数据缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello world".getBytes());
byteBuffer.flip();
// 6. 将数据写入通道(发送到服务端)
socketChannel.write(byteBuffer);
// 7. 关闭输出流并断开连接
socketChannel.shutdownOutput();
socketChannel.close();
}
public static void main(String[] args) throws IOException {
startClient();
}
}
如何验证消息被丢弃?
启动客户端程序,向服务器发消息。
服务端打印接收到的内容,但不会返回任何响应。
回顾 Reactor 模式中的 IO 事件处理流程
单线程 Reactor 模式-CSDN博客
多线程 Reactor 模式-CSDN博客
如果不了解 Reactor 模式,可点击链接跳转学习
一个 IO 操作从内核产生到业务代码执行,会经历 4 个阶段:
阶段 | 说明 |
---|---|
第 1 步:注册 | IO 事件通过 Channel 注册到 Selector |
第 2 步:查询 | Reactor(反应器)通过线程轮询 Selector,检查是否有事件就绪 |
第 3 步:分发 | 如果发现事件,交给绑定的 Handler 处理 |
第 4 步:处理 | 真正的 IO 读写逻辑或业务逻辑由 Handler 完成 |
第 1、2 步由 Java NIO 提供机制,第 3、4 步为 Reactor 核心。
Netty 中的 Channel
在 Netty 中,Channel 是进行所有 IO 操作的核心载体。它是对 Java NIO 中 Channel 的封装和扩展,提供了更加灵活和统一的接口,使得网络通信编程变得更为简单高效。Channel 表示一个网络连接,既可以代表客户端的连接通道,也可以代表服务端的监听通道。
针对不同的通信协议,例如 TCP、UDP 和 SCTP,Netty 提供了对应的 Channel 实现类。同时,为了支持不同的 IO 模型,Netty 为每种协议都提供了基于 NIO(非阻塞 IO)和 OIO(阻塞 IO)的两种实现版本。例如,针对 TCP 协议,Netty 提供了 NioSocketChannel 和 OioSocketChannel。
通过这种方式,Netty 不仅屏蔽了底层 IO 实现的复杂性,还为用户提供了丰富且统一的通信编程模型。无论是同步通信还是异步通信,开发者都可以通过 Channel 接口轻松完成底层数据传输的操作。
常见 Channel 类型
协议类型 | Channel 实现 |
---|---|
TCP 传输 | NioSocketChannel, OioSocketChannel |
TCP 监听 | NioServerSocketChannel, OioServerSocketChannel |
UDP 传输 | NioDatagramChannel, OioDatagramChannel |
SCTP 传输 | NioSctpChannel, OioSctpChannel |
SCTP 监听 | NioSctpServerChannel, OioSctpServerChannel |
Netty 的 Channel 封装关系
在 Netty 中,Channel 的实现并不是直接使用 Java NIO 的 Channel 接口,而是在其基础上进行了多层封装,以提供更高层次的抽象和更强的功能扩展。以常用的 NioSocketChannel 为例,它的继承结构遵循了一个清晰的层级体系:NioSocketChannel 继承自 AbstractNioByteChannel,而后者又继承自 AbstractNioChannel,再向上是 AbstractChannel,最终实现了最顶层的 Channel 接口。
这种层层封装的设计,使得 Netty 能够在不同层级中分离出通用的逻辑与具体的实现细节,从而提升代码的复用性与扩展性。同时,尽管 Netty 封装了自己的 Channel 体系,但其最底层依然是依赖于 Java NIO 中的 SelectableChannel,也就是说,Netty 的所有 IO 操作最终都会落地到 NIO 提供的底层通道上执行。因此,Netty 在提升开发体验的同时,也保留了 NIO 高性能的核心优势。
顶层接口(Channel):定义通用行为,与具体实现无关
抽象类(AbstractChannel 等):封装通用逻辑,沉淀复用代码
具体实现类(NioSocketChannel 等):专注于特定协议和 IO 模型的细节
SelectableChannel 是 Java NIO 中的一个抽象类,是所有支持多路复用(Selector)的通道(Channel)的父类。它的核心作用是允许通道被注册到 Selector 上,通过 Selector 实现对多个通道的 IO 事件(读就绪、写就绪、连接就绪等)进行统一监听和管理。
Netty 中的 Reactor
在 Netty 中,Reactor 模式的核心组件之一是 NioEventLoop,负责事件轮询与处理调度。NioEventLoop 是 Netty 对 Java NIO 中事件驱动机制的封装,其本质是一个反应器线程,用于不断轮询底层的 Selector,监听是否有 IO 事件发生,并在事件就绪后将其分发给对应的处理器(Handler)进行处理。
每一个 NioEventLoop 都拥有一个独立的线程,并且绑定了一个 Selector 实例。它们之间形成一一对应的关系,保证事件处理的线程安全性和高效性。此外,Netty 设计中允许多个 Channel 被注册到同一个 NioEventLoop 上,从而实现一对多的高效事件管理机制。这种设计不仅充分利用了线程资源,还有效支持了高并发场景下的网络通信。
NioEventLoop 的继承结构:
在 Netty 的架构中,NioEventLoop 是实现事件驱动模型的核心类之一,它的继承结构体现了 Netty 对事件处理流程的高度抽象与组织。具体来说,NioEventLoop 继承自 SingleThreadEventLoop,而 SingleThreadEventLoop 又继承自 SingleThreadEventExecutor,最终实现了顶层的 Executor 接口。
这一层层的继承关系体现了职责的逐步细化:从通用的任务执行器(Executor),到专用于单线程执行的事件循环体(SingleThreadEventExecutor),再到真正绑定了 IO 事件处理能力的 NioEventLoop。通过这样的设计,Netty 将事件处理的通用性与专业性有机结合,使得每一个 EventLoop 既具备强大的调度能力,又能专注于单线程下的高效执行。
在实现细节上,NioEventLoop 拥有两个至关重要的成员属性:其一是 Thread,即事件处理线程;其二是 Selector,用于监听和轮询注册的 IO 事件。这两个核心组件配合使用,使得 NioEventLoop 成为支撑 Netty 高性能异步 IO 的关键支点。
EventLoop 与 Channel 的关系
一个 EventLoop 可以管理成千上万个 Channel。
属于一对多的绑定结构。
Netty 中的 Handler
IO 事件类型:
类型 | 对应 SelectionKey |
---|---|
读就绪 | OP_READ |
写就绪 | OP_WRITE |
连接完成 | OP_CONNECT |
接收连接 | OP_ACCEPT |
Handler 分类:
类型 | 说明 |
---|---|
ChannelInboundHandler | 入站事件处理器(如读数据) |
ChannelOutboundHandler | 出站事件处理器(如写数据) |
在 Netty 中,Handler 是承载业务逻辑处理的核心组件。它用于响应和处理各种 IO 事件,是连接数据与业务处理之间的桥梁。根据事件的方向不同,Handler 被分为两类:入站处理器和出站处理器。
ChannelInboundHandler 是处理入站事件的处理器,负责响应例如客户端连接建立、数据读取等事件,典型方法如 channelRead()、channelActive()。而 ChannelOutboundHandler 则用于处理出站事件,主要负责将数据写出到网络或进行编码转换等操作,典型方法如 write()、flush()。
每个 Channel 在创建时,都会绑定一个 ChannelPipeline,这个 Pipeline 中可以包含多个 Handler。通过 Pipeline 的链式结构,Netty 能够灵活地控制事件在多个 Handler 之间的流转,从而实现复杂、可扩展的网络业务处理逻辑。
Netty 中的 Pipeline
在 Netty 中,ChannelPipeline 是每个 Channel 所独有的组件,它是一个双向链表结构,专门用于管理和组织多个 Handler 的执行顺序。可以理解为,Pipeline 就是一条事件处理的通道,所有与该 Channel 相关的 IO 事件都会按照这条通道中 Handler 的顺序依次传递与处理。
通过 ChannelPipeline,Netty 实现了对事件处理器(Handler)的链式管理,不仅支持事件的入站处理(Inbound),也支持事件的出站处理(Outbound)。事件在 Pipeline 中的传递方向由事件类型决定:入站事件会按照添加顺序正向传递,出站事件则会反向传递。
这种设计带来了高度的灵活性,使开发者能够按需在不同位置添加、替换或删除 Handler,从而精细控制事件处理流程,实现清晰、模块化的通信逻辑。简而言之,ChannelPipeline 是 Netty 中连接 Channel 与业务处理器之间的桥梁,也是事件流动和处理的主通道。
普通 Reactor 模式是事件 → 单个处理器(内部包含所有步骤),而 Netty 是事件 → 流水线(多个独立 Handler 按顺序协作)
入站/出站执行顺序
1. IO 事件进入 Pipeline。
2. 沿着 Handler 链处理:
入站事件按前向顺序触发 InboundHandler。
出站事件按后向顺序触发 OutboundHandler。
尚未完结