网络通信模型
在了解Netty之前,我们可以简单的先了解一下我们的网络通信方式,正所谓知其然,知其所以然。只有了解了网络通信模型,我们才能更好的去理解Netty的一些核心的原理。
如下图是一个简单的请求发送的时候的一个大概的HTTP请求处理流程。
可以发现我们的一个请求过程其实就是数据的封装的过程。
在这其中还会涉及到很多的算法来帮助我们增加这些处理过程的速度,比如滑动窗口、拥塞算法等。
那么在一个请求发送完毕之后,我们就可以考虑如何去处理这样子的一个请求,比如我们可以开启一个线程,让一个线程去处理一个请求,直到这个请求处理完毕,那么如果这个请求迟迟没有处理完成,那么也许我们就需要进行阻塞等待了。而如果这样子似乎性能有点差?毕竟虽然我们可以开很多线程,但是频繁的线程切换是非常消耗性能的。
我们似乎可以考虑,能不能用一个线程来处理多个请求连接?只有真的有数据传送过来的时候我们才让这个线程去处理这个数据,而如果没有的话,我们的线程就等待即可。
嗯,这样子听起来似乎能让一个线程处理很多的请求连接了,但是我怎么知道什么时候请求有数据呢?第一种方法我想到的是使用循环,第二种方法就是类似于通知了。
是的,这两种方法在目前的OS上都能实现。前者可以使用select/poll,后者我们貌似可以用到多路复用?epoll(事件驱动)?
是的,聊了这么多,其实我再说的就是常用的IO通信模型,BIO、NIO以及多路复用机制,其实还有AIO。
对于这些不同的IO通信模型,他们有不同的优缺点。
- BIO (Blocking I/O):
- 在这种模式下,我们为每个请求分配一个线程来处理。线程会一直处理这个请求,直到它完成。
- 但这种方法存在一定的局限性。如果请求处理时间较长,线程会被阻塞,无法处理其他任务。同时,频繁的线程切换也会消耗大量的系统资源。
- NIO (Non-blocking I/O):
- 考虑到 BIO 的限制,我们可以尝试让一个线程处理多个请求。在这种模式下,线程只在有数据传输时才处理请求,没有数据时则处于等待状态。
- 但这里产生了一个新的问题:如何知道何时有数据需要处理?一个方法是使用循环不断检查(轮询),这是传统的 select/poll 方法。另一种方法是系统在有数据时通知线程,这就是多路复用机制,例如 epoll。
- 多路复用机制:
- 在多路复用模型中,一个线程可以监控多个请求。当某个请求有数据时,系统会通知该线程,线程随后开始处理这个请求。
- 这种模式大大提高了效率,因为它减少了不必要的线程切换和资源消耗。
- AIO (Asynchronous I/O):
- AIO 是一种异步非阻塞的 I/O 模型。在这个模型中,应用程序可以直接发起一个异步的 I/O 操作,并在操作完成后得到通知。
- AIO 适合处理大量并发的、长时间运行的 I/O 操作,能进一步提升程序性能。
为什么我要在说Netty之前先聊这些?
因为Netty 是一个基于 NIO 的网络编程框架,它深入使用了 NIO 的多路复用和非阻塞特性。了解不同的 I/O 模型,尤其是 NIO 和多路复用机制,有助于深入理解 Netty 的工作原理和设计思想。
并且我们知道,并不是所有场合使用Netty这类框架都是好的。
不同的 I/O 模型有着不同的性能特点。例如,BIO 适合简单应用,但在高并发场景下性能较差;而 NIO 和多路复用(特别是 epoll)在处理大量并发连接时表现更优。理解这些差异有助于在开发过程中做出合理的架构选择和性能优化。而不是无脑的使用一些看起来高大上的框架来增加自己业务的复杂度和可维护性。
回归正题,我们来聊一聊我们today的主角—Netty
为什么Netty会出现?
在深入了解之前,我们先来想一想为什么会出现Netty。
- 初期的阻塞 I/O (BIO):
- 在早期的网络编程中,主要使用了阻塞 I/O(BIO)模型。在这个模型中,每个连接都需要一个线程来处理,当连接数增加时,线程数量也会相应增加,导致资源消耗大、扩展性差。
- 非阻塞 I/O (NIO) 的引入:
- 为了解决 BIO 的问题,引入了非阻塞 I/O(NIO)。NIO 允许单个线程处理多个连接的 I/O 操作,减少了线程数量,提高了系统资源利用率。
Netty 的出现
- 解决 NIO 使用复杂性:
- 虽然 NIO 解决了 BIO 的一些问题,但 NIO 编程相对复杂,处理多路复用器(Selector)、缓冲区(Buffer)等需要大量的样板代码和精细的控制。
- Netty 应运而生,它是一个基于 NIO 的高性能网络编程框架,提供了更易用的 API 和更强大的功能,简化了 NIO 的编程复杂性。也就是说,Netty是Java中对于NIO模型的一种封装,方便我们更加简单的使用NIO模型去编程。
- 高性能和高可扩展性:
- Netty 为了提高性能和可扩展性,实现了自己的线程模型、缓冲区管理、事件处理等机制。
- 它支持多种传输类型,如 NIO、OIO(BIO)、本地和嵌入式传输,提供灵活的网络编程能力。
- 丰富的功能和组件:
- Netty 提供了各种协议的编解码器(HTTP、WebSocket、Google Protocol Buffers 等),使得开发特定协议的网络应用更加方便。
- 它还内置了对 SSL/TLS、流量整形、身份验证等的支持。
- 社区和生态支持:
- Netty 拥有一个活跃的社区,提供了大量的文档、示例和最佳实践。
- 许多大型项目和公司(如 Apache、Twitter、阿里巴巴)都在使用 Netty,证明了其在实际应用中的强大和可靠性。
因此,其实Netty的出现类似于:一群程序员去造福另一群程序员的感觉。
Netty快速入门
ok,那么我们简单的了解了Netty是什么东西之后,我们就得准备去使用它了。
Netty学习起来其实并没有那么容易,因为Netty虽然对底层进行了封装,但是会用到Netty进行开发的基本都是面向网络通信这一块的程序员,大部分程序员可能都是直接依赖于架构师已经搭建好的架构了,很少会有去直接使用Netty编程的,但是如果会使用Netty,也意味着更强的竞争力(哈哈哈我学长说会Netty老加分了)。同时,也意味着你能开发更多类型的东西。
既然是网络通信框架,那么就说明使用Netty一定能非常容易的去接收网络上的各种请求吧。
是的,只需要不到50行代码,我就可以接受网络上的请求并且进行处理。
接下来我们先从引入依赖开始,然后我会慢慢的为你介绍Netty中的各种重要的概念。
首先是引入Netty的依赖。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.52.Final</version>
</dependency>
之后,我们创建一个类,用来准备main方法。
package blossom.project.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @author: ZhangBlossom
* @date: 2023/12/13 21:29
* @contact: QQ:4602197553
* @contact: WX:qczjhczs0114
* @blog: https://blog.csdn.net/Zhangsama1
* @github: https://github.com/ZhangBlossom
* NettyTest类
*/
// NettyTest类定义
public class NettyTest {
public static void main(String[] args) {
// 创建 boss 线程组用于处理服务器端接收客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 创建 worker 线程组用于进行 SocketChannel 的网络读写
EventLoopGroup workGroup = new NioEventLoopGroup(8);
// 创建 ServerBootstrap 对象,用于配置服务器的启动参数
ServerBootstrap bootstrap = new ServerBootstrap();
// 设置使用的 EventLoopGroup,设置用于创建服务器的 NioServerSocketChannel,
// 并且设置 childHandler 来处理每一个连接的数据读写
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 初始化每个连接的管道,添加自定义的处理器
channel.pipeline()
.addLast(new NettyHttpServerHandler())
.addLast(new NettyServerConnectManagerHandler());
}
});
// 初始化 ChannelFuture,用于异步操作的通知回调
ChannelFuture channelFuture = null;
try {
// 绑定服务器端口并启动服务器
channelFuture = bootstrap.bind(8080).sync();
System.out.println("Netty start successfully!");
// 同步等待直到服务器端口关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
// 异常处理
throw new RuntimeException(e);
} finally {
// 优雅关闭 worker 和 boss 线程组
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
/**
* @author: ZhangBlossom
* @date: 2023/10/23 19:57
* @contact: QQ:4602197553
* @contact: WX:qczjhczs0114
* @blog: https://blog.csdn.net/Zhangsama1
* @github: https://github.com/ZhangBlossom
* NettyHttpServerHandler 用于处理通过 Netty 传入的 HTTP 请求。
* 它继承自 ChannelInboundHandlerAdapter,这样可以覆盖回调方法来处理入站事件。
*/
public class NettyHttpServerHandler extends ChannelInboundHandlerAdapter {
// 成员变量nettyProcessor,用于处理具体的业务逻辑
/**
* 当从客户端接收到数据时,该方法会被调用。
* 这里将入站的数据(HTTP请求)包装后,传递给业务逻辑处理器。
*
* @param ctx ChannelHandlerContext,提供了操作网络通道的方法。
* @param msg 接收到的消息,预期是一个 FullHttpRequest 对象。
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
byte[] data = new byte[in.readableBytes()];
in.readBytes(data);
System.out.println("收到消息为:"+new String(data));
ByteBuf out = Unpooled.copiedBuffer("返回数据给客户端".getBytes());
ctx.write(out);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
super.channelReadComplete(ctx);
}
/**
* 处理在处理入站事件时发生的异常。
*
* @param ctx ChannelHandlerContext,提供了操作网络通道的方法。
* @param cause 异常对象。
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
// 打印自定义消息,实际使用时应该记录日志或进行更复杂的异常处理
System.out.println("----");
}
}
package blossom.project.netty;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
/**
* @author: ZhangBlossom
* @date: 2023/10/23 19:57
* @contact: QQ:4602197553
* @contact: WX:qczjhczs0114
* @blog: https://blog.csdn.net/Zhangsama1
* @github: https://github.com/ZhangBlossom
* 连接管理器,管理连接对生命周期
* 当前类提供出站和入站事件的处理能力,能够管理网络链接的整个生命周期
* 服务器连接管理器,用于监控和管理网络连接的生命周期事件。
*/
@Slf4j
public class NettyServerConnectManagerHandler extends ChannelDuplexHandler {
/**
* 当通道被注册到它的EventLoop时调用,即它可以开始处理I/O事件。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
// 获取远程客户端的地址
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
// 记录调试信息
log.debug("NETTY SERVER PIPLINE: channelRegistered {}", remoteAddr);
// 调用父类方法继续处理注册事件
super.channelRegistered(ctx);
}
/**
* 当通道从它的EventLoop注销时调用,不再处理任何I/O事件。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
*/
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
log.debug("NETTY SERVER PIPLINE: channelUnregistered {}", remoteAddr);
super.channelUnregistered(ctx);
}
/**
* 当通道变为活跃状态,即连接到远程节点时被调用。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
log.debug("NETTY SERVER PIPLINE: channelActive {}", remoteAddr);
super.channelActive(ctx);
}
/**
* 当通道变为不活跃状态,即不再连接远程节点时被调用。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
log.debug("NETTY SERVER PIPLINE: channelInactive {}", remoteAddr);
super.channelInactive(ctx);
}
/**
* 当用户自定义事件被触发时调用,例如,可以用来处理空闲状态检测事件。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
* @param evt 触发的用户事件。
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 检查事件是否为IdleStateEvent(空闲状态事件)
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
// 如果是所有类型的空闲事件,则关闭通道
if (event.state().equals(IdleState.ALL_IDLE)) {
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
log.warn("NETTY SERVER PIPLINE: userEventTriggered: IDLE {}", remoteAddr);
ctx.channel().close();
}
}
// 传递事件给下一个ChannelHandler
ctx.fireUserEventTriggered(evt);
}
/**
* 当处理过程中发生异常时调用,通常是网络层面的异常。
*
* @param ctx 提供了操作网络通道的方法的上下文对象。
* @param cause 异常对象。
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
final String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
// 记录警告信息和异常堆栈
log.warn("NETTY SERVER PIPLINE: remoteAddr: {}, exceptionCaught {}", remoteAddr, cause);
// 发生异常时关闭通道
ctx.channel().close();
}
}
将代码复制之后其实就可以直接run一下开始运行了。
为了验证效果,你可以打开cmd,然后使用telnet命令去进行测试,我来演示一下:
按下回车之后,你就可以发送一条消息,由于没有做其他多的处理,所以你只可以发送一个字符,发送完毕这个字符之后,你就可以在控制台上看到你发送的字符了。
好的,至此,Netty入门成功,完结撒花(开个玩笑)。
这个demo代码只是为了演示想要使用Netty进行网络通信其实遵循一套固定的代码就可以很快的完成了,但是为了让我们以后能更好的使用Netty开发一些有趣的东西。
比如网关(比如SpringCloud Gateway),RPC框架(比如Dubbo就是基于Netty的)。
那么我们就必须得了解Netty中的一些核心的概念。这里我会按照我代码的流程下来为你介绍这些核心的概念。
NIO三大核心概念
为了照顾没有NIO基础的同学,这里我将先讲解一下NIO中的三大概念:selector、channel、buffer。
Channel
- 概念:
- 在 Netty 和 Java NIO 中,Channel 类似于传统 I/O 中的流(Stream)。不同的是,Channel 是双向的,可以用于读、写或同时进行读写操作。
- Channel 是一个连接到能够进行 I/O 操作(如读、写、连接、绑定)的实体的开放连接。
- 类型:
- 在 Netty 中,有多种类型的 Channel,例如 SocketChannel 用于 TCP 网络连接,ServerSocketChannel 用于 TCP 服务器端监听新的连接。
- 作用:
- Channel 提供了执行网络 I/O 操作的接口。所有的数据读写都通过 Channel 进行。
Selector
- 概念:
- Selector 是 Java NIO 中的一个组件,用于检查一个或多个 NIO Channel(是否有数据可读或可写)的状态,从而可以实现单线程管理多个 channels。
- 工作机制:
- Selector 允许单个线程同时监控多个数据通道(Channel)。线程可以通过 Selector 查询哪个通道准备好了读数据、写数据等。
- 在 Netty 中的应用:
- 在 Netty 中,EventLoop 通常封装了一个 Selector。这允许 EventLoop 高效地管理多个 Channel 上的 I/O 事件。
Buffer
- 概念:
- Buffer 在 Java NIO 和 Netty 中是一个重要的概念。它是一块可以写入数据,然后可以从中读取数据的内存区域。
- 特点:
- NIO Buffer 是一种容器对象,Netty 对此进行了封装和扩展,提供了更高级的功能,比如引用计数和池化。
- 用途:
- Buffer 用于和 NIO Channel 交互时的数据存储。当读取数据时,数据被读到 Buffer 中;当写入数据时,数据从 Buffer 写入到 Channel。
总结一下:
- Channel 表示连接,用于执行实际的 I/O 操作。
- Selector 用于监控多个 Channel 的 I/O 状态,是实现非阻塞 I/O 的关键。
- Buffer 是数据的容器,用于在读写操作中存储数据。
其中,Selector 在 Java NIO 中的工作机制是基于事件监听的方式,而不是传统的轮询。这种机制允许单个线程高效地管理多个 Channel 的 I/O 状态。下面对 Selector 的工作原理进行简要概述:
- 注册 Channel:
- 首先,将 Channel 注册到 Selector 上,并指定感兴趣的事件。这些事件通常包括:可读(SelectionKey.OP_READ)、可写(SelectionKey.OP_WRITE)、连接(SelectionKey.OP_CONNECT)和接受(SelectionKey.OP_ACCEPT)。
- 事件监听:
- Selector 在调用其 select() 方法时开始监听注册的事件。这个调用是阻塞的,它会一直阻塞直到至少有一个注册的事件发生。
- 检测事件:
- 当一个或多个 Channel 上的事件发生时,select() 方法返回。返回值表示有多少 Channel 有事件发生。
- 查询 SelectionKey:
- 事件发生后,可以通过 Selector 的 selectedKeys() 方法获取所有发生了事件的 Channel 对应的 SelectionKey 集合。
- 每个 SelectionKey 包含了事件信息,以及对应的 Channel 引用。
- 处理事件:
- 应用程序遍历这个 SelectionKey 集合,根据每个键的事件类型来执行相应的操作,比如接受新连接、读取数据等。
事件驱动 vs. 轮询
- 事件驱动:
- Selector 的工作模式是事件驱动的。它仅在有感兴趣的事件发生时才激活,这比传统的轮询机制(不断检查所有连接状态)更为高效。
- 优势:
- 这种方法的优势在于它大大减少了不必要的检查,减少了 CPU 的使用,并允许单个线程有效地管理多个网络连接。
总结一下
Selector 的事件驱动机制是 Java NIO 高效处理多个并发连接的关键。它使得应用程序能够以一种非阻塞的方式处理多个网络通道,既高效又可扩展。
好的,再了解了NIO的基础概念之后,我这回是真的要开始讲解代码了。
EventLoopGroup
可以看到,我们代码的第一行就是EventLoopGroup这个东西。
意思好像是:事件循环组。
那么在了解组之前我们是不是可以先了解一下EventLoop的含义。
EventLoop 是 Netty 中的一个核心概念,它是 Netty 事件处理的基础。理解 EventLoop 对于掌握 Netty 的工作原理至关重要。
- 基本概念:
- 一个 EventLoop 可以被看作是一个不断运行的循环,负责处理事件,例如连接接受、数据读取、写入等。
- 在 Netty 中,每个 EventLoop 都与一个线程绑定。一旦某个 EventLoop 被绑定到一个线程,它就会一直运行在这个线程上。
- 事件处理:
- EventLoop 处理由 Channel 发出的各种 I/O 事件和任务。例如,当新数据到达时,EventLoop 将调用适当的回调函数,如 ChannelHandler 中定义的方法。
EventLoop 的作用
- 单线程模型:
- EventLoop 通常以单线程模型运行,意味着所有由 EventLoop 处理的事件和任务都在同一个线程中执行。这样可以避免多线程编程中常见的并发问题和同步开销。
- 高效的事件循环:
- EventLoop 有效地组织和处理事件和任务,使得网络应用能够快速响应网络事件。例如,它可以高效地管理非阻塞 I/O 操作,使得单个线程可以处理多个网络连接。
- 任务调度:
- 除了处理 I/O 事件,EventLoop 还可以调度和执行用户定义的任务。例如,可以将一个任务提交到 EventLoop 的任务队列中,该任务将在 EventLoop 的未来某个时间点执行。
知道了EventLoop的大概作用,我们也很容易能推出EventLoopGroup的作用了。
- EventLoopGroup 是一个 EventLoop 的集合。它负责为每个新创建的 Channel 提供一个 EventLoop。在 Netty 中,NioEventLoopGroup 是一个实现了 EventLoopGroup 接口的类,用于 NIO 传输。
- EventLoop 是执行实际任务的单线程执行器,处理与特定 Channel 相关的事件和任务。
- EventLoopGroup 是一个更高层次的实体,管理着一组 EventLoop,并负责将 Channel 分配给这些 EventLoop。
因此,有了这些了解之后,我们就可以开始去了解一开始的两行代码:
// 创建 boss 线程组用于处理服务器端接收客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 创建 worker 线程组用于进行 SocketChannel 的网络读写
EventLoopGroup workGroup = new NioEventLoopGroup(8);
也就是这里的bossGroup和workGroup。为什么我要创建两个组?
还记得我们的NIO模型吗?selector是核心的负责管理连接的,而channel才是真正的和连接打交道的。这就类似于我们的bossgroup和workergroup的作用了。
bossGroup
- 作用:
- bossGroup 负责处理传入的连接请求。其主要职责是接受新的客户端连接。
- 工作流程:
- 当一个新的连接建立时,bossGroup 会接受这个连接,并注册到 workerGroup 中的一个 EventLoop。
- 线程数量:
- 通常情况下,bossGroup 不需要太多线程。在许多情况下,一个线程就足够处理所有的连接请求,因为接受新连接本身不涉及复杂的操作。
从上面的描述可以看出,bossGroup所工作在的层次,应该是我们的网络通信层次,因为它具体负责和我们的请求连接打交道,帮助我们建立请求连接并且分配到workerGroup去处理。
workerGroup
- 作用:
- workerGroup 负责处理已经建立的连接的 I/O 操作,包括读取数据、写入数据以及执行相关的业务逻辑。
- 工作流程:
- 一旦 bossGroup 接受了连接并将其注册到 workerGroup,workerGroup 中的 EventLoop 就会处理该连接的所有后续 I/O 操作。
- 线程数量:
- workerGroup 通常需要更多的线程来处理多个并发的 I/O 操作。在高并发的场景中,拥有足够的线程来处理客户端请求是提高性能的关键。 默认两倍CPU核数 ,触发时机:服务端启动绑定端口,新连接接入
我们的workerGroup则就类似于事件的调度器(你也可以理解为EventLoop线程池),他负责对不同类型的事件进行不同的调度处理。
最后,EventLoopGroup其实就可以理解为一个线程池一样的东西,只不过他管理的是EventLoop,每当有一个连接创建,也就会创建一个Channel,同时就会从EventLoopGrouo中找到一个EventLoop,将他和Channel进行绑定。
可以看到EventLoopGroup的上层包含了ExecutorService。
此时,我们会通过EventLoop来处理网络连接生命周期中的所有IO事件。
也就是最后会交给我们的服务处理层处理,这里的服务处理层就是我们的:ChannelInboundHandlerAdapter的实现类,一会我会讲解。
因此他们的区别如下
- 主要职责:
- bossGroup 专注于接受新的客户端连接。
- workerGroup 专注于处理已建立连接的数据读写和业务逻辑。
- 线程使用:
- bossGroup 通常只需要少量线程,甚至一个线程就足够。
- workerGroup 可能需要更多线程来处理复杂的任务和高并发。
ServerBootstrap
在 Netty 中,ServerBootstrap 类是用于设置和启动服务器的一个重要组件。
也就是说,ServerBootstrap 是一个辅助类,用于帮助我们构建一个服务器。它允许我们设置服务器相关的详细参数,例如用于接受传入连接的端口、用于处理 I/O 操作的 EventLoopGroup 、使用OIO还是NIO的方式来处理网络IO连接。
所以我们可以看到,我们通过 ServerBootstrap指定 bossGroup 和 workerGroup。使用bossGroup用于处理接入的连接和使用workerGroup 用于处理已建立连接的 I/O 操作。
以及通过 ServerBootstrap 设置服务器 Channel 的类型,如 NioServerSocketChannel,这决定了服务器将使用 NIO 的方式来接受新连接。
NioServerSocketChannel 与 NioSocketChannel 关系与区别
- NioServerSocketChannel_ 负责监听传入的连接请求。_
- 当有新的连接建立时,NioServerSocketChannel的ChannelPipeline中的特定ChannelHandler会接收到新连接事件。
- 这个特定的ChannelHandler通过调用Chooser策略,选择合适的EventLoop(通常是Worker中的一个NioEventLoop)来处理这个新连接。
- 被选定的EventLoop负责后续的连接处理,包括创建和管理对应的NioSocketChannel。
- 新的NioSocketChannel将被绑定到选定的EventLoop上,并由这个EventLoop来管理其后续的事件处理和生命周期。
我们还可以配置 ChannelPipeline。ServerBootstrap 让我们设置一个 ChannelInitializer,在这个初始化器中,我们可以配置 ChannelPipeline 并添加自定义的 ChannelHandler。这些处理器用于处理网络请求的业务逻辑。
Netty 检测新连接流程:
Netty 使用了 ServerBootstrap 来配置服务器,并指定了 bossGroup 用于接受连接。当服务器启动后,NioEventLoop 实例会在一个死循环中等待新连接的到来。它会不断地轮询操作系统底层的 Selector,以检测是否有新的连接请求到达。
当有新的连接请求到达时,NioEventLoop 会通过 Selector 的事件通知机制获取到连接的事件,然后将连接封装为 Channel 对象,在 bossGroup 的线程中完成分发,但实际的连接处理会交给 workerGroup 来处理,从而实现了事件的分发和处理的分离。
Boss第一个线程轮询出accept事件, Netty服务端启动绑定Boss线程(NioEventLoop), select方法检测
通过底层channel方法创建这条连接, NioSocketChannel
而聊到了这一块,我们就可以聊一下我们Channel的一个生命周期。
当我们的Channel建立连接之后,会有一个生命周期。
可以从上面的代码知道我的:NettyHttpServerHandler这个类继承了一个ChannelInboundHandlerAdapter类。
因此,我们可以基于这个抽象类提供的方法,来在不同的Channel的生命周期的时候执行不同的事情。Channel一共有如下几种生命周期:
- channelRegistered(ChannelHandlerContext ctx):
- 当 Channel 注册到 EventLoop 时调用。
- channelUnregistered(ChannelHandlerContext ctx):
- 当 Channel 从 EventLoop 注销时调用。
- channelActive(ChannelHandlerContext ctx):
- 当 Channel 处于活跃状态(即已连接到远程主机)时调用。
- channelInactive(ChannelHandlerContext ctx):
- 当 Channel 不再活跃(即从远程主机断开)时调用。
- channelRead(ChannelHandlerContext ctx, Object msg):
- 当从 Channel 读取数据时调用。
- channelReadComplete(ChannelHandlerContext ctx):
- 当一个读操作完成时调用。
- userEventTriggered(ChannelHandlerContext ctx, Object evt):
- 当有用户事件被触发时调用。
- channelWritabilityChanged(ChannelHandlerContext ctx):
- 当 Channel 的可写状态发生改变时调用。
- exceptionCaught(ChannelHandlerContext ctx, Throwable cause):
- 处理过程中发生异常时调用。
当然,我们还有ChannelDuplexHandler 、 ChannelInboundHandler 和 ChannelOutboundHandler 接口。
ChannelDuplexHandler 同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口,这意味着它可以处理既是入站又是出站的事件。
ChannelPipeline
我们知道当真的触发一个IO事件的时候,具体处理这个IO事件的地方,就是在我们ChannelPipeline中添加的一个又一个的handler。
对应到我们的代码中,就是如下的代码段:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 初始化每个连接的管道,添加自定义的处理器
channel.pipeline()
.addLast(new NettyHttpServerHandler())
.addLast(new NettyServerConnectManagerHandler());
}
});
//更加正规的应该是如下的代码,因为一般我们都需要先进行数据的编码解码
.childHandler(new ChannelInitializer<Channel>() { // 定义处理新连接的管道初始化逻辑
@Override
protected void initChannel(Channel ch) throws Exception {
// 配置管道中的处理器,如编解码器和自定义处理器
ch.pipeline().addLast(
new HttpServerCodec(), // 处理HTTP请求的编解码器
new HttpObjectAggregator(config.getMaxContentLength()), // 聚合HTTP请求
new HttpServerExpectContinueHandler(), // 处理HTTP 100 Continue请求
new NettyHttpServerHandler(nettyProcessor), // 自定义的处理器
new NettyServerConnectManagerHandler() // 连接管理处理器
);
}
});
ChannelPipeline 是怎么添加到 bootStrap 里的?
- Bootstrap 设置 childHandler() 方法: 对于服务器端,调用 childHandler() 方法时,传递一个 ChannelInitializer 实例。这个初始化器负责为每个新接受的连接初始化新创建的 Channel。
- 反射创建 NioServerSocketChannel: 在 Netty 内部,调用 bind() 方法时,Netty 使用反射创建一个 NioServerSocketChannel 的实例。这个过程中会触发 NioServerSocketChannel 的构造函数。
- NioServerSocketChannel 构造函数调用: 在 NioServerSocketChannel 的构造函数中,会调用其父类 AbstractNioChannel 的构造函数。在这个构造函数中,会初始化一些基本的属性和配置,比如底层的 Java NIO 通道等。
- 初始化 ChannelPipeline: 在 AbstractNioChannel 的构造函数中,会设置该 Channel 的 ChannelPipeline。在这个阶段,可能会添加一些默认的 ChannelHandler 或者配置默认的参数,可以使 Netty 判断 ChannelHandler 类型
- 调用 ChannelInitializer: 在 NioServerSocketChannel 的构造函数中会调用 ChannelInitializer 的 initChannel() 方法。在这个方法中,可以进一步配置该 Channel 的 ChannelPipeline,添加自定义的 ChannelHandler,以处理新连接接入后的操作。
我们接收到一个请求之后,通过我们的 Tomcat(一个基于 Java Servlet 和 JSP 规范的 Web 容器),Tomcat 会处理这个 HTTP 请求,并将请求数据封装为 HttpServletRequest 对象。然后,这个对象会被传递给部署在 Tomcat 上的 Web 应用程序,例如一个基于 Spring MVC 的应用。在 Spring MVC 应用中,请求会进一步被处理,最终到达我们的 Controller。
需要注意的是,Tomcat 主要处理 Web 应用层的事务,如 HTTP 请求的接收和响应。Tomcat 在其早期版本中使用 Java BIO,而在 Tomcat 7 及以后的版本中,引入了 NIO 来提高性能,但这与 Netty 是两个不同的技术(因为有部分人以为这里用的就是Netty,但是不是)。
而 Netty 是一个更为底层的网络编程框架,用于构建高性能的网络服务器和客户端。Netty 提供了对低层次网络协议如 TCP/IP 和 UDP 的处理能力,但它不负责像 Tomcat 那样处理高层次的 HTTP 请求或将它们转换成 HttpServletRequest 对象。
了解了这一点之后,我想你就不会将Tomcat和Netty的关系混为一谈了。
接下来我们继续,ChannelPipeline。
如下是官方在ChannelPipeline接口中提供的注释。比较生动的为我们展现了一个IO请求的处理流程。
可以很明显的发现请求的处理流程就是一个链式的处理流程,这和我们上面编写的代码也是一致的。在ChannelPipeline中使用到了双向链表。
I/O Request
via Channel or
ChannelHandlerContext
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| [ Socket.read() ] [ Socket.write() ] |
| |
| Netty Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+
还记得上面我们说到的,一个连接请求对应一个Channel,而一个Channel对应一个ChannelPipleine,这保证了线程安全。
同时,为了保证我们的链式调用的过程中下一个处理器能获得上一个处理器的处理结果,Netty使用了一个ChannelHandlerContext的上下文对象来保存处理后的数据。
源码分析传播顺序:
- InBound 事件传播
ChannelRead事件的传播:可以从head传播也可以从当前节点传播
channelRead->fireChannelRead->invokeChannelRead->channelRead->findContextInbound->…->Tail的onUnhandledInboundMessage
- OutBound 事件传播
write()事件的传播 :从Tail节点传播或者从当前节点传播
write->findContextOutbound()->next.invokeWrite->invokeWrite0->write
- 异常的传播
invokeChannelRead->notifyHandlerException->invokeExceptrionCaught->
_ exceptionCaught->fireExceptionCaught->invokeExceptionCaught->next.invokeExceptrionCaught->__exceptionCaght _最后打印堆栈
补充: writeAndFlush()抽象步骤
- 从尾节点(tail)开始向前传播:Netty 中的 ChannelPipeline 从尾部的 Outbound 处理器(tail)开始,沿着链路向前传播事件。
- 逐个调用 ChannelHandler 的 write 方法:对于每个 Outbound 处理器,调用其 write() 方法,将数据传递给下一个处理器。这样,数据就会在每个处理器中经过相应的转换或处理。
- 逐个调用 ChannelHandler 的 flush 方法:对于每个 Outbound 处理器,调用其 flush() 方法,确保数据立即被发送。这样可以确保所有处理器中的数据都被刷新并发送到底层的传输。
所以到此,我们的几个比较重要的概念都已经介绍完毕了。
流程架构图
好的,我们已经完全的了解了如何使用Netty编写一个服务端的代码:
我们会在ServerBootstrap处开启一个Server端,用于接收我们的连接请求。而这个Server可以选择基于JDK的NIO封装的通道,他帮助我们屏蔽了使用Socket直接进行网络通信的一个复杂性(NioServerSocketChannel)。
之后,我们就可以基于ServerBootStrap来管理我们的bossGroup和wokerGroup。bossGroup用于接收和处理我们的连接请求,并且将我们的连接请求通过我们的channel管道分配到我们的workerGroup去具体的执行(注册channel到我们的某个EventLoop中去)。而workerGroup中包含的多个EventLoop都有自己的单个线程,而这个线程就会去轮询当前EventLoop所管理的相关的channel注册的就绪的事件(在 Netty 中,EventLoop 和 Channel 的关系是一对多的关系。一个 EventLoop 可以处理多个 Channel 的 I/O 操作,但每个 Channel 在任何时候只关联到一个 **EventLoop,**从这里我们也可以知道,EventLoop中肯定有一个Selector选择器来选择Channel)。如果某个EventLoop中的IO事件就绪,那么就会调用我们的ChannelPipeline中的方法进行处理。
也就是说,对于EventLoop的管理是分开的。但是处理的流程是统一的,都是依靠ChannelPipeline中我们提供的Handler来处理。
通过对上面知识的学习,我们大概能知道,一个请求由Netty进行处理,从连接创建到请求处理完毕的全流程大概如下:
客户端请求
|
v [ EventLoop ]
| / \
| / \
| / \
| [ Channel ] [ Channel ]
| (连接1) (连接2)
|
[接入新连接]
|
v
[ bossGroup ] -------------------------|
(EventLoopGroup, 接受新连接) |
| |
[NioSocketChannel] |
| |
|--- 分配连接到 ---| (BossGroup和WorkerGroup)
| | 由ServerBootstrap管理
v v |
[ workerGroup ] [其他EventLoop]-------|
(EventLoopGroup, (处理其他连接)
处理连接的 I/O) |
| |
| |
| |
| [其他Channel]
| (其他连接)
| |
|_________________|
|
v
[ Channel ]
(一个连接的实体,
由 EventLoop 管理)
|
v
[ ChannelPipeline ]
(管理 ChannelHandler 链,
处理入站和出站事件)
|
v
[ ChannelInboundHandlerAdapter ] ------|
(处理入站事件的 Handler, |
执行具体业务逻辑) 这里都是链式哦
| |
v |
[ ChannelOutboundHandlerAdapter ]------|
(处理出站事件的 Handler,
执行具体业务逻辑)
(返回响应)
|
v
[ 客户端 ]
Netty的Reactor模型
Netty 支持不同的 Reactor 模型,包括单线程单 Reactor、多线程单 Reactor 以及主从多线程 Reactor。这些模型提供了不同的方式来处理网络连接和 I/O 操作,以适应不同的应用场景和性能需求。
单线程单 Reactor
- 描述:
- 在这个模型中,所有的 I/O 操作(连接、读、写)和事件处理都在同一个线程(即同一个 EventLoop)上执行。
- 这种模型适用于负载较轻的场景,因为它避免了线程切换的开销。
- Netty 实现:
- 在 Netty 中,您可以通过创建一个单独的 EventLoopGroup 并将所有的 Channel 注册到它来实现单线程单 Reactor 模型。
代码比较好理解,我们只需要修改ServerBootstrap部分的代码即可。
// 创建 boss 线程组用于处理服务器端接收客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建 ServerBootstrap 对象,用于配置服务器的启动参数
ServerBootstrap bootstrap = new ServerBootstrap();
//两个事件组都使用bossGroup即可
bootstrap.group(bossGroup, bossGroup)
多线程单 Reactor
- 描述:
- 在这个模型中,一个 Reactor(EventLoopGroup)处理所有的连接请求,但每个连接(Channel)的 I/O 操作可以在不同的线程(不同的 EventLoop)上执行。
- 它适用于需要高并发处理能力的场景。
- Netty 实现:
- Netty 默认使用这种模型。一个 NioEventLoopGroup 可以拥有多个 EventLoop,每个 Channel 的 I/O 操作都由其中一个 EventLoop 处理。
// 创建 boss 线程组用于处理服务器端接收客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
// 创建 ServerBootstrap 对象,用于配置服务器的启动参数
ServerBootstrap bootstrap = new ServerBootstrap();
// boss事件组中线程数量不为1,且两个事件组都使用当前bossGroup
bootstrap.group(bossGroup, bossGruop)
主从多线程 Reactor
- 描述:
- 这个模型有两级 Reactor:主 Reactor 负责接受新连接,从 Reactor 负责处理连接的 I/O 操作。
- 这种模型适用于高负载、高并发的场景。
- Netty 实现:
- 在 Netty 中,可以创建两个 EventLoopGroup:一个作为 bossGroup(主 Reactor),负责处理新的连接请求;另一个作为 workerGroup(从 Reactor),负责处理每个连接的 I/O 操作。也就是我们一开始写的代码的样子。
我们在编程的过程中,无脑的使用这种方式即可。
深入了解EventLoop
在上文中我们只是简单的讲解了一下EventLoopGroup,但是作为Netty中最重要的一个概念,肯定是要继续深入了解一下它的具体的处理的流程的。
在上面我们讲到,bossGroup会将注册的连接事件,通过Channel的方式绑定到我们的EventLoop上。
当接收到一个新的连接请求时,bossGroup 负责接受连接并创建一个新的 Channel。这个 Channel 随后被注册到 workerGroup 的一个 EventLoop 上。每个 EventLoop 包含一个 Selector,负责轮询所有注册的 Channel 以检测 I/O 事件。当 Selector 发现某个 Channel 有就绪的 I/O 事件时,对应的 EventLoop 会根据事件的类型来处理,比如读取数据。读取的数据随后会被传递给配置在 ChannelPipeline 中的 ChannelHandler 进行相应的业务逻辑处理。
也就是说:
- bossGroup 负责接收进来的连接。一旦一个新的连接被接受,bossGroup 将创建一个新的 Channel 用于这个连接。
- 这个新创建的 Channel 随后被注册到 workerGroup 中的一个 EventLoop。注意!!!并不是 bossGroup 直接将连接事件绑定到 EventLoop,而是创建 Channel 后由 workerGroup 的 EventLoop 负责处理。
EventLoop~~ 内部包含一个,用于检测和选择就绪的 I/O 事件。Selector轮询注册在其上的所有Selector的 I/O 事件。~~Channel- EventLoop 内部包含一个 Selector,用于检测和选择就绪的 I/O 事件。Selector 轮询注册在其上的所有 Channel 的 I/O 事件。
- 对于读事件,EventLoop 会从就绪的 Channel 中读取数据,并将其传递给相应的 ChannelHandler 进行处理。
所以,按照上面的流程,我们可以简单的绘制一个流程图:
[ 客户端请求 ]
|
v
[ bossGroup 接收新连接 ]
|
|--- 创建新的 Channel
|
v
[ Channel 注册到 workerGroup 的 EventLoop ]
|
v
[ EventLoop 中的 Selector 轮询 Channel 的 I/O 事件 ]
|
|--- 检测到就绪事件
|
v
[ EventLoop 根据事件类型处理(如读取、写入) ]
|
v
[ 数据传递给 ChannelPipeline 和 ChannelHandler ]
|
v
[ 处理业务逻辑,如数据解析、响应生成 ]
|
v
[ 将响应发送回客户端 ]
比如我们以NioEventLoop为例。
public final class NioEventLoop extends SingleThreadEventLoop
可以发现,NioEventLoop默认使用的是单线程进行事件处理,但是并不需要过多的去担心性能。因为当多个 Channel 同时有事件触发时,EventLoop 将按照 Selector 检测到的顺序来处理这些事件。由于 I/O 操作是非阻塞的,每个事件的处理通常不会花费太多时间,从而允许 EventLoop 在短时间内处理多个事件。
而由于内部结构类似于线程池,因此我们甚至可以直接用submit的方式来提交我们要执行的任务。
使用next就可以获取到下一个要执行任务的线程。
而上面我们也聊到了EventLoop可以接收要注册的Channel。
而具体的EventLoop的执行流程,可以自行查看源码。
而EventLoopGroup之所以说他类似线程池。
可以进入到他们的顶级父类去看看:
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args)
深入了解ChannelPipeline
ChannelPipeline比较好理解,因为我们编程的时候其实就是主要编程这个地方,他就是对我们的请求进行具体处理的一个地方。
我们在上面也提到了,每一个Channel都有他自己的ChannelPipeline,从而保证了Channel的线程安全。
但是我们要如何理解ChannelPipeline?
其实ChannelPipeline 可以被看作是一个贯穿整个 Channel 的事件处理管道。它主要负责管理和执行 ChannelHandler,这些处理器用于处理入站(接收的数据)和出站(发送的数据)事件。
ChannelPipeline 管理着数据在 Channel 中的流动方向,处理入站和出站数据。例如,对于入站数据,它可能涉及解码、验证等操作;对于出站数据,则可能涉及编码、加密等。
ChannelPipeline 允许 ChannelHandler 形成一个链。每个处理器可以独立处理事件,然后决定是否将事件传递给链中的下一个处理器。
也就是说,ChannelPipeline 内部维护着一个由 ChannelHandler 实例组成的链表。这些处理器按顺序排列,用于处理特定的事件。
同时,每个 ChannelHandler 都关联一个 ChannelHandlerContext 对象,它提供了与 Channel 和 ChannelPipeline 交互的操作,使处理器能够访问网络层的状态和控制信息。
当一个事件到达 ChannelPipeline 时,它会从链的一端开始传播,通过每个 ChannelHandler。处理器可以选择继续传播事件或者停止传播。同时,处理器通过ChannelHandlerContext 来传递他们处理的信息到下一个ChannelHandler。
大概的流程如下:
[ 网络事件(入站/出站) ]
|
v
[ ChannelPipeline 开始处理 ]
|
v
[ 第一个 ChannelHandler ]
|
v
[ ChannelHandlerContext ]
|--- 提供对 Channel 和 Pipeline 的操作
|
v
[ 第二个 ChannelHandler ]
|
v
[ ChannelHandlerContext ]
|--- 提供对 Channel 和 Pipeline 的操作
|
v
[ ... 更多 ChannelHandlers ... ]
|
v
[ 最后一个 ChannelHandler ]
|
v
[ 处理完成,根据事件类型响应(如数据写回) ]
到此为止,已经基本讲解完了Netty中一些比较重要的概念了,相信你使用这些基础的概念就能进行简单的编程了。
但是,如果想要使用Netty去进一步开发网络通信的项目,那么还是需要学习很多额外的知识的。
因为Netty只是一个方便的网络通信的底层开发框架,但是想要打通一个完整的请求,你可能还需要考虑到:协议。
比如当初在字节的时候,我就知道我们公司有很多自研的RPC通信框架。
那么难点就在于如何制定公司内部的通信协议。
所以,请明确,Netty的出现只是说为了方便你完成对网络通信这一层的开发,但是你想要使用它去实现你自己的功能,比如你想用Netty实现上位机,那么协议这一块才是你的重头戏。
如果有机会,我会简单的用Netty实现一个自研的RPC框架。
深入理解 ByteBuf
ByteBuf 在 Netty 中代表了一种内存抽象,映射了不同规格和类别的内存分配策略。
内存的类别:
- Pooled 和 Unpooled:
- Pooled:使用了内存池来管理内存,能够有效地重复利用分配过的内存。PooledByteBuf 是一个从内存池中分配的 ByteBuf 实例,使用它可以提高性能。
- Unpooled:非池化的内存分配方式,每次都会创建新的内存。UnpooledByteBuf 是通过普通的 Java new 操作分配的 ByteBuf 实例,不在内存池中。
- Unsafe 和 非Unsafe:
- Unsafe:底层操作直接使用内存地址和偏移量,提供了更高效的操作方式。但是需要注意,Unsafe 操作要求开发者对内存管理有更深入的了解,并且可能不够安全,易导致潜在的内存溢出或错误。
- 非Unsafe:在操作内存时,通过数组下标进行访问,更为安全。不直接使用底层的内存地址和偏移量,更易于使用,但性能可能略逊于 Unsafe。
- Heap 和 Direct:
- Heap:在堆中分配内存。Heap 内存由 JVM 的垃圾回收器管理,创建和销毁的开销较大。HeapByteBuf 通常是通过普通的 Java 对象分配的。
- Direct:在堆外直接分配内存。Direct 内存可以减少垃圾回收器的压力,但分配和释放的成本较高。DirectByteBuf 是由 Netty 在堆外分配的内存,通常用于网络传输。
内存分配器ByteBufAllocator 介绍:
内存分配的最终执行者
- 线程本地缓存(Thread-Local Caching):
- Netty 的 ByteBufAllocator 使用了线程本地缓存来减少多线程间的竞争。每个线程都拥有自己的本地缓存,这些缓存包含了特定内存区域(arena)、chunk、chunkList、page 和 subpage。
- 每个线程在分配内存时,会优先从自己的线程本地缓存中分配,减少了对共享资源的竞争。这样可以降低锁的争用,提高并发性能。
- 内存区域划分(Arena):
- Netty 使用内存池,并将内存划分为不同的区域(Arena)。每个 Arena 由多个 Chunk 组成,每个 Chunk 又包含多个 Page 和 Subpage。
- 每个线程在分配内存时会选择一个特定的 Arena,这样不同线程的内存分配操作就不会在同一个 Arena 上发生竞争。
- 内存分配的粒度管理:
- Netty 的内存分配器会根据不同大小的内存需求,选择合适的粒度进行分配。较小的内存请求可能会被分配到 Subpage,较大的请求则可能会被分配到 Page 或 Chunk。
内存分配策略:
- Page 级别内存分配:
- 大内存请求(一般大于 pageSize)由 Page 级别内存进行分配。
- 当有大内存需求时,Netty 会尝试在已有的 Chunk 上进行分配。它会检查现有的 Chunk 是否有足够的空闲内存来满足请求。
- 如果没有足够的空闲内存,Netty 会创建一个新的 Chunk 并在其上分配所需内存。
- Subpage 级别内存分配:
- 对于较小的内存请求(例如小于 pageSize),Netty 使用 Subpage 来处理。
- 在 Subpage 内存分配过程中,Netty 会定位一个合适大小的 Subpage 对象,并初始化 Subpage 的句柄(handle)。
- 这个句柄包括了 Subpage 的状态(通过 bitmapIdx 标识)、内存映射的索引(memoryMapIdx)以及在 Chunk 中的位置(对应哪一块内存)等信息。