本文内容来自B站黑马课程及相关书籍学习总结
1 Netty概述
Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。
2 入门案例:Hello World
添加Netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.76.Final</version>
</dependency>
2.1 服务端
public class HelloServer {
public static void main(String[] args) {
//1.创建服务端引导类
ServerBootstrap b = new ServerBootstrap();
//2.创建反应器轮询组
NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
//3.为引导类设置反应器轮询组
b.group(bossLoopGroup, workerLoopGroup);
//4.设置通道类型
b.channel(NioServerSocketChannel.class);
//5.装配子通道流水线
b.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//添加处理器
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
}
});
}
});
//6.绑定监听端口
b.bind(8080);
}
}
2.2 客户端
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//1.创建引导类
Bootstrap b = new Bootstrap();
//2.创建反应器轮询组
NioEventLoopGroup workLoopGroup = new NioEventLoopGroup();
//3.为引导类设置反应器轮询组
b.group(workLoopGroup);
//4.设置通道类型
b.channel(NioSocketChannel.class);
//5.转配通道流水线
b.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
});
//6.连接服务端,并发送数据
b.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel()
.writeAndFlush("hello, world");
}
}
3 Netty组件
3.1 Bootstrap
Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务端的Netty组件的组装,以及Netty程序的初始化和启动执行。
Netty有两个引导类,分别用于服务器和客户端,即ServerBootStrap和BootStrap。
3.1.1 父子通道
在Netty中,将有接收关系的监听通道和传输通道叫作父子通道。其中,负责服务器连接监听和接收的监听通道(如NioServerSocketChannel)也叫父通道(Parent Channel),对应于每一个接收到的传输类通道(如NioSocketChannel)也叫子通道(Child Channel)。
3.1.2 EventLoopGroup
EventLoop事件循环对象
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 io 事件。
它的继承关系比较复杂
-
一条线是继承自 j.u.c.ScheduledExecutorService,因此包含了线程池中所有的方法
-
另一条线是继承自 netty 自己的 OrderedEventExecutor,
- 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
- 提供了 parent 方法来看看自己属于哪个 EventLoopGroup
EventLoopGroup事件循环组
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
-
继承自 netty 自己的 EventExecutorGroup
- 实现了 Iterable 接口提供遍历 EventLoop 的能力
- 另有 next 方法获取集合中下一个 EventLoop
演示NioEventLoop处理普通任务和定时任务
NioEventLoop除了可以处理io事件,还可以处理普通任务和定时任务,而DefaultEventLoop只能处理普通任务和定时任务。
public static void main(String[] args) {
//1. 创建事件轮询组
NioEventLoopGroup group = new NioEventLoopGroup(2);//io事件 普通任务 定时任务
// DefaultEventLoop group = new DefaultEventLoop();//普通任务 定时任务
//2. 获取下一个事件循环对象
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());
//3. 执行普通任务
group.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("hello!");
});
//4. 执行定时任务
group.scheduleAtFixedRate(() -> {
log.debug("ok");
}, 0, 1, TimeUnit.SECONDS);
log.debug("main");
}
演示NioEventLoop处理IO事件
创建两个NioEventLoopGroup,一个bossLoopGroup用于处理Accept事件,另一个workerLoopGroup用于处理其他IO读写事件。
public static void main(String[] args) {
//1.创建服务端引导类
ServerBootstrap b = new ServerBootstrap();
//2.创建事件循环组
NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup(2);
//3.为引导类设置事件循环组
b.group(bossLoopGroup, workerLoopGroup);
//4.设置通道类型
b.channel(NioServerSocketChannel.class);
//5.装配子通道流水线
b.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//添加处理器
ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
}
});
}
});
//6.绑定监听端口
b.bind(8080);
}
通道处理器默认使用workerLoopGroup的线程执行任务,如果当前连接数较多,且handler处理耗时较长,那么处理效率不高。此时可以创建一个DefaultEventLoopGroup,用于处理handler任务。
//创建独立的非nio事件循环组处理普通任务
DefaultEventLoopGroup group = new DefaultEventLoopGroup();
//...
b.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//添加处理器
ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
ch.pipeline().addLast(group, "handle2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
}
});
}
});
Handler如何切换EventLoop执行
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 下一个 handler 的事件循环是否与当前的事件循环是同一个线程
EventExecutor executor = next.executor();
// 是,直接调用
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
}
// 不是,将要执行的代码作为任务提交给下一个事件循环处理(换人)
else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
- 如果两个 handler 绑定的是同一个线程,那么就直接调用
- 否则,把要调用的代码封装为一个任务对象,由下一个 handler 的线程来调用
3.1.3 BootStrap启动流程
以服务端为例,大致步骤如下:
第1步:创建事件循环组,并设置到ServerBootstrap引导类实例;
第2步:设置通道的IO类型;
第3步:设置传输通道的配置选项;
//step4:设置通道的参数
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
第4步:装配子通道的Pipeline;
第5步:开始绑定服务器新连接的监听端口
第7步:自我阻塞,直到监听通道关闭
第8步:关闭EventLoopGroup
3.1.4 ChannelOption
无论是对于NioServerSocketChannel父通道类型还是对于NioSocketChannel子通道类型,都可以设置一系列的ChannelOption(通道选项):
-
SO_RCVBUF和SO_SNDBUF
这两个为TCP传输选项,每个TCP socket(套接字)在内核中都有一个发送缓冲区和一个接收缓冲区,这两个选项就是用来设置TCP连接的两个缓冲区大小的。
-
TCP_NODELAY
此为TCP传输选项,如果设置为true就表示立即发送数据。TCP_NODELAY用于开启或关闭Nagle算法。
-
SO_KEEPALIVE
此为TCP传输选项,表示是否开启TCP的心跳机制。true为连接保持心跳,默认值为false。
-
SO_BACKLOG
此为TCP传输选项,表示服务端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。多个客户端到来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,队列的大小通过SO_BACKLOG指定。
3.2 Channel
channel 的主要作用
-
close() 可以用来关闭 channel
-
closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- 而 addListener 方法是异步等待 channel 关闭
-
pipeline() 方法添加处理器
-
write() 方法将数据写入
-
writeAndFlush() 方法将数据写入并刷出
3.2.1 同步和异步回调执行发送
同步方式:
//连接服务端
ChannelFuture channelFuture = b.connect(new InetSocketAddress("localhost", 8080));
//使用sync方法同步等待连接完成
channelFuture.sync();
Channel channel = channelFuture.channel();
log.debug("{}",channel);
channel.writeAndFlush("hello,world!");//客户端发送数据
注意: connect 方法是异步的,意味着不等连接建立,方法执行就返回了。如果此时直接从channelFuture获取channel,那么channel对象可能不是正确的。
异步回调方式:
//使用addListener(回调对象)方法异步等待连接建立后执行发送
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Channel channel = future.channel();
log.debug("{}",channel);
channel.writeAndFlush("hello,world!");//客户端发送数据
}
});
3.2.2 关闭问题
使用channel.closeFuture()方法,该方法返回一个ChannelFuture对象,当channel执行关闭时,会通知该future,用于执行一些通道关闭后的操作,如释放底层资源。
同步方式执行通道关闭后的操作:
//获取closeFuture对象,当通道关闭时会通知该future
ChannelFuture closeFuture = channel.closeFuture();
log.debug("waiting close...");
closeFuture.sync();//阻塞等待通道关闭
log.debug("执行关闭之后的操作");
workLoopGroup.shutdownGracefully();//优雅关闭事件循环组
异步方式执行通道关闭后的操作:
closeFuture.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("执行关闭之后的操作");
workLoopGroup.shutdownGracefully();//优雅关闭事件循环组
}
});
💡 异步提升的是什么
- 单线程没法异步提高效率,必须配合多线程、多核 cpu 才能发挥异步的优势
- 异步并没有缩短响应时间,反而有所增加
- 合理进行任务拆分,也是利用异步的关键
3.3 Future & Promise
Netty的Future接口继承自java.util.concurrent.Future接口,并进行扩展;而Netty的Promise接口则继承了Netty的Future接口并进行了扩展。
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
| 功能/名称 | jdk Future | netty Future | Promise |
|---|---|---|---|
| cancel | 取消任务 | - | - |
| isCanceled | 任务是否取消 | - | - |
| isDone | 任务是否完成,不能区分成功失败 | - | - |
| get | 获取任务结果,阻塞等待 | - | - |
| getNow | - | 获取任务结果,非阻塞,还未产生结果时返回 null | - |
| await | - | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 | - |
| sync | - | 等待任务结束,如果任务失败,抛出异常 | - |
| isSuccess | - | 判断任务是否成功 | - |
| cause | - | 获取失败信息,非阻塞,如果没有失败,返回null | - |
| addLinstener | - | 添加回调,异步接收结果 | - |
| setSuccess | - | - | 设置成功结果 |
| setFailure | - | - | 设置失败结果 |
3.3.1 Jdk Future示例
@Slf4j
public class TestJdkFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//2. 提交任务
Future<Integer> future = service.submit(() -> {
log.debug("执行任务");
Thread.sleep(1000);
return 50;
});
log.debug("等待结果...");
log.debug("结果是:{}", future.get());
}
}
3.3.2 Netty Future示例
@Slf4j
public class TestNettyFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建时间循环组
DefaultEventLoopGroup executors = new DefaultEventLoopGroup();
//2. 提交任务
Future<Integer> future = executors.submit(() -> {
log.debug("执行任务");
Thread.sleep(1000);
return 50;
});
log.debug("等待结果...");
//异步回调接收结果
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("结果是:{}", future.getNow());
}
});
}
}
3.3.3 Netty Promise示例
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建EventLoopGroup
DefaultEventLoop executors = new DefaultEventLoop();
//2. 创建Promise
DefaultPromise<Integer> promise = new DefaultPromise<>(executors);
//3. 提交任务,计算完成后填充结果
executors.execute(() -> {
log.debug("开始计算...");
try {
Thread.sleep(1000);
promise.setSuccess(80);
} catch (Exception e) {
promise.setFailure(e);
}
});
//4. 接收结果
log.debug("等待结果...");
log.debug("结果是:{}",promise.get());
}
}
3.4 Handler & Pipeline
ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline 流水线
-
入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
-
出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
3.4.1 ChannelInboundHandler入站处理器
ChannelInboundHandlerAdapter核心方法:
-
channelRegistered()
当通道注册完成后,触发通道注册事件,在通道流水线注册过的入站处理器的channelRegistered()回调方法会被调用。
-
channelActive()
当通道激活完成后,触发通道激活事件,在通道流水线注册过的入站处理器的channelActive()回调方法会被调用。
-
channelRead()
当通道缓冲区可读时,触发通道可读事件,在通道流水线注册过的入站处理器的channelRead()回调方法会被调用,以便完成入站数据的读取和处理。
-
channelReadComplete()
当通道缓冲区读完时,触发通道缓冲区读完事件,在通道流水线注册过的入站处理器的channelReadComplete()回调方法会被调用。
-
channelInactive()
当连接被断开或者不可用时,触发连接不可用事件,在通道流水线注册过的入站处理器的channelInactive()回调方法会被调用。
-
exceptionCaught()
当通道处理过程发生异常时,触发异常捕获事件,在通道流水线注册过的入站处理器的exceptionCaught()方法会被调用。
3.4.2 ChannelOutboundHandler出站处理器
当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler出站处理器完成Netty通道到底层通道的操作,比如建立底层连接、断开底层连接、写入底层Java NIO通道等。
ChannelOutboundHandler核心方法:
-
bind()
监听地址(IP+端口)绑定:完成底层Java IO通道的IP地址绑定,如果使用TCP传输协议,这个方法用于服务端。
-
connect()
连接服务端:完成底层Java IO通道的服务端的连接操作。如果使用TCP传输协议,那么这个方法将用于客户端。
-
write()
写数据到底层:完成Netty通道向底层Java IO通道的数据写入操作。
-
flush()
将底层缓存区的数据腾空,立即写出到对端。
-
read()
从底层读数据:完成Netty通道从Java IO通道的数据读取。
-
disConnect()
断开服务器连接:断开底层Java IO通道的socket连接。如果使用TCP传输协议,此方法主要用于客户端。
-
close()
主动关闭通道:关闭底层的通道,例如服务端的新连接监听通道。
3.4.3 执行顺序
public static void main(String[] args) throws InterruptedException {
ServerBootstrap b = new ServerBootstrap();
NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
b.group(bossLoopGroup,workerLoopGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("1");
super.channelRead(ctx, msg);
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("2");
super.channelRead(ctx, msg);
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("3");
ch.write("收到!");
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4");
super.write(ctx, msg, promise);
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("5");
super.write(ctx, msg, promise);
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("6");
super.write(ctx, msg, promise);
}
});
}
});
b.bind(8080).sync();
}
从日志打印结果可以看出,ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。ChannelPipeline 的实现是一个 ChannelHandlerContext(包装了 ChannelHandler) 组成的双向链表。
-
super.channelRead(ctx, msg)方法内部调用的是ctx.fireChannelRead(msg) ,即调用下一个入站处理器
-
ctx.channel().write(msg) 会 从尾部开始触发 后续出站处理器的执行
-
出站处理器中,ctx.write(msg, promise) 的调用会 触发上一个出站处理器
-
ctx.channel().write(msg) vs ctx.write(msg)
- 都是触发出站处理器的执行
- ctx.channel().write(msg) 从尾部开始查找出站处理器
- ctx.write(msg) 是从当前节点找上一个出站处理器
3.4.4 EmbeddedChannel
EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际传输,不需要启动Netty服务器和客户端。
使用EmbeddedChannel,开发人员可以在单元测试用例中方便、快速地进行ChannelHandler业务处理器的单元测试,避免每开发一个业务处理器都进行服务器和客户端的重复启动。
主要方法:
-
writeInbound()
调用writeInbound()方法,向EmbeddedChannel写入一个入站数据(如二进制ByteBuf数据包),模拟底层的入站包,从而被入站处理器处理到
-
writeInbound()
调用writeOutbound()方法,向模拟通道写入一个出站数据(如二进制ByteBuf数据包),该包将进入处理器流水线,被待测试的出站处理器所处理
案例:
//创建处理器
ChannelInitializer<EmbeddedChannel> initializer = new ChannelInitializer<EmbeddedChannel>() {
@Override
protected void initChannel(EmbeddedChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4");
super.write(ctx, msg, promise);
}
});
}
};
EmbeddedChannel channel = new EmbeddedChannel(initializer);
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
//模拟入站操作
channel.writeInbound(buffer.writeBytes("hello".getBytes()));
//模拟出站操作
channel.writeOutbound(buffer.writeBytes("hello".getBytes()));
3.5 ButeBuf
Netty提供了ByteBuf缓冲区组件来替代Java NIO的ByteBuffer缓冲区组件,以便更加快捷和高效地操纵内存缓冲区。
3.5.1 创建ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
3.5.2 ByteBuf的分配器
Netty通过ByteBufAllocator分配器来创建缓冲区和分配内存空间。Netty提供了两种分配器实现:PoolByteBufAllocator(池化分配器)和UnpooledByteBufAllocator(非池化分配器)。
池化和非池化对比:
- 使用非池化,每次调用时创建一个新的ByteBuf实例;使用完之后,通过Java的垃圾回收机制回收或者直接释放(对于直接内存而言)。
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
Netty默认的分配器为ByteBufAllocator.DEFAULT。该默认分配器可以通过系统参数(System Property)选项io.netty.allocator.type进行配置来选择开启池化功能:
-Dio.netty.allocator.type={unpooled|pooled}
Netty4.1版本默认的分配器为PooledByteBufAllocator(池化内存分配器)。
3.5.3 ByteBuf缓冲区类型
根据内存的管理方不同,缓冲区分为堆缓冲区(Heap ByteBuf) 和直接缓冲区(Direct ByteBuf)
创建池化基于堆的 ByteBuf:
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
创建池化基于直接内存的 ByteBuf:
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
直接内存:
- 直接内存不属于Java堆内存,所分配的内存其实是调用操作系统malloc()函数来获得的,由Netty的本地Native堆进行管理
- 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
- 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
3.5.4 ByteBuf组成
ByteBuf是一个字节容器,内部是一个字节数组。从逻辑上来分,字节容器内部可以分为四个部分
第一部分是已用字节,表示已经使用完的废弃的无效字节;
第二部分是可读字节,这部分数据是ByteBuf保存的有效数据,从ByteBuf中读取的数据都来自这一部分;
第三部分是可写字节,写入ByteBuf的数据都会写到这一部分中;
第四部分是可扩容字节,表示的是该ByteBuf最多还能扩容的大小。
3.5.5 ByteBuf的方法
写入方法:
| 方法签名 | 含义 | 备注 |
|---|---|---|
| isWritable() | ByteBuf是否可写 | |
| writeTYPE(TYPE value) | 写入基本数据类型 | |
| writeBytes(ByteBuf src) | 写入 netty 的 ByteBuf | |
| writeBytes(byte[] src) | 写入 byte[] | |
| writeBytes(ByteBuffer src) | 写入 nio 的 ByteBuffer | |
| int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
还有一类方法是 set 开头的一系列方法,也可以写入数据,但不会改变写指针位置
读取方法:
读取和上述写入的方法一一对应;
如果需要重复读取废弃内容,则可以使用如下方法:
markReaderIndex()与resetReaderIndex():前一种方法表示把当前的读指针readerIndex保存在markedReaderIndex属性中;后一种方法表示把保存在markedReaderIndex属性的值恢复到读指针readerIndex中。
还有种办法是采用 get 开头的一系列方法,这些方法不会改变 read index。
3.5.6 ByteBuf扩容
扩容规则是
- 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
- 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10=1024(2^9=512 已经不够了)
- 扩容不能超过 max capacity 会报错
3.5.7 ByteBuf的引用计数
Netty的ByteBuf的内存回收工作是通过引用计数方式管理的。
ByteBuf引用计数的大致规则如下:
-
在默认情况下,当创建完一个ByteBuf时,引用计数为1;
-
每次调用retain()方法,引用计数加1;
-
每次调用release()方法,引用计数减1;
-
当引用为0,再次访问这个ByteBuf对象,将会抛出异常;此时这个ByteBuf没有被引用,它占用的内存会回收。
当ByteBuf的引用计数已经为0时,Netty会进行ByteBuf的回收,分为以下两种场景:
-
如果属于池化的ByteBuf内存,回收方法是:放入可以重新分配的ByteBuf池,等待下一次分配。
-
如果属于未池化的ByteBuf缓冲区,需要细分为两种情况:如果是堆(Heap)结构缓冲,会被JVM的垃圾回收机制回收;如果是直接(Direct)内存类型,则会调用本地方法释放外部内存(unsafe.freeMemory)
除了通过ByteBuf成员方法retain()和release()管理引用计数之外,Netty还提供了一组用于增加和减少引用计数的通用静态方法:
- ReferenceCountUtil.retain(Object):增加一次缓冲区引用计数的静态方法,从而防止该缓冲区被释放。
- ReferenceCountUtil.release(Object):减少一次缓冲区引用计数的静态方法,如果引用计数为0,缓冲区将被释放。
3.5.8 ByteBuf的自动创建与自动释放
-
起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline
-
入站 ByteBuf 处理原则
- 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
- 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
- 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
- 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
- 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
-
出站 ByteBuf 处理原则
- 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
-
异常处理原则
- 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
TailContext自动释放
Netty默认会在ChannelPipline的最后添加一个TailContext(尾部上下文,也是一个入站处理器)。它实现了默认的入站处理方法,在这些方法中会帮助完成ByteBuf内存释放的工作。
沿着channelRead()方法向下,最后通过ReferenceCountUtil.release()释放了缓冲区。
// io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
如果没有调用父类的入站处理方法将ByteBuf缓存区向后传递,则需要手动进行释放。
SimpleChannelInboundHandler自动释放
继承SimpleChannelInboundHandler基类实现处理器,此时必须将业务处理代码移动到重写的channelRead0(ctx, msg)方法中,SimpleChannelInboundHandler的channelRead方法在执行完channelRead0()方法后,会释放缓冲器:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
//释放缓冲区
ReferenceCountUtil.release(msg);
}
}
}
出站处理时的自动释放
出站缓冲区的自动释放方式是HeadContext自动释放。通过write()方法写入流水线时,调用ctx.writeAndFlush(ByteBuf msg),就会让ByteBuf缓冲区进入流水线的出站处理流程。在每一个出站Handler业务处理器中的处理完成后,数据包(或消息)会来到出站处理的最后一棒HeadContext,在完成数据输出到通道之后,ByteBuf会被释放一次,如果计数器为零,就将被彻底释放掉。
3.5.9 ByteBuf复制和零拷贝
大部分场景下,在Netty接收和发送ByteBuffer的过程中会使用直接内存进行Socket通道读写,使用JVM的堆内存进行业务处理,会涉及直接内存、堆内存之间的数据复制。内存的数据复制效率非常低,Netty提供了多种方法,以帮助应用程序减少内存的复制。
1. Slice切片浅层复制
ByteBuf的slice()方法可以获取到一个ByteBuf的切片。一个ByteBuf可以进行多次切片浅层复制;多次切片后的ByteBuf对象可以共享一个存储区域(没有发生内存复制,还是使用原ByteBuf的内存),切片后的 ByteBuf 维护独立的 read,write 指针。
Slice()方法有两个重载版本:
-
public ByteBuf slice()
-
public ByteBuf slice(int index, int length)
第一个无参数slice()方法的返回值是ByteBuf实例中可读部分的切片;带参数的slice(int index, int length)方法可以通过灵活地设置不同起始位置和长度来获取到ByteBuf不同区域的切片。
代码示例:
public static void main(String[] args) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
buf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});
log(buf);
//在切片过程中,没有发生数据复制
ByteBuf slice1 = buf.slice(0, 5);
ByteBuf slice2 = buf.slice(0, 5);
log(slice1);
log(slice2);
slice1.setByte(0,'b');//修改切片ByteBuf,会同步修改原ByteBuf
log(slice1);
log(buf);
}
总结:
- 切片不会复制源ByteBuf的底层数据,底层数组和源ByteBuf的底层数组是同一个。
- 切片不会改变源ByteBuf的引用计数。
2. duplicate整体浅层复制
duplicate()和slice()方法都是浅层复制。不同的是,slice()方法是切取一段的浅层复制,而duplicate()是整体的浅层复制
3. 浅层复制的问题
浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,会导致一个问题:在源ByteBuf调用release()方法之后,一旦引用计数为零,就变得不能访问了;
因此,在调用浅层复制实例时,可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用。在浅层复制实例用完后,需要调用release()方法,将引用计数减1,这样就不会影响源ByteBuf的内存释放了。
4. copy
ByteBuf的copy方法会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关
5. CompositeByteBuf实现零拷贝
CompositeByteBuf可以把需要合并的多个ByteBuf组合起来,对外提供统一的readIndex和writerIndex。
CompositeByteBuf只是在逻辑上是一个整体,在CompositeByteBuf内部,合并的多个ByteBuf都是单独存在的,避免了内存拷贝。
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
System.out.println(ByteBufUtil.prettyHexDump(buf1));
System.out.println(ByteBufUtil.prettyHexDump(buf2));
CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
buf3.addComponents(true, buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));
- 优点,对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制
- 缺点,复杂了很多,多次操作会带来性能的损耗
6. wrap操作实现零拷贝
Unpooled提供了一系列的wrap包装方法,可以快速地包装出CompositeByteBuf实例或者ByteBuf实例,而不用进行内存拷贝。
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
ByteBuf buf4 = Unpooled.wrappedBuffer(buf1, buf2);
除了通过Unpooled包装CompositeByteBuf之外,还可以将byte数组包装成ByteBuf,所得到的ByteBuf对象和bytes数组共用同一个存储空间。
💡 ByteBuf 优势
- 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用,使用更流畅
- 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf
4 回显服务器案例
服务端代码:
@Slf4j
public class NettyEchoServer {
public static void main(String[] args) throws InterruptedException {
//创建服务端引导类
ServerBootstrap serverBootstrap = new ServerBootstrap();
//创建事件循环组
NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
//3.为引导类设置事件循环组
serverBootstrap.group(bossLoopGroup, workerLoopGroup);
//4.设置通道类型
serverBootstrap.channel(NioServerSocketChannel.class);
//5.添加处理器
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
//服务端打印数据
log.debug("服务端接受数据:{}", buf.toString(StandardCharsets.UTF_8));
//向客户端写回数据
ByteBuf response = ctx.alloc().buffer();
response.writeBytes(buf);
ctx.writeAndFlush(response);
super.channelRead(ctx, buf);
}
});
}
});
//6.绑定监听端口号
ChannelFuture channelFuture = serverBootstrap.bind(8081);
channelFuture.addListener((future) -> {
if (future.isSuccess()) {
log.info(" ========》反应器线程 回调 服务器启动成功,监听端口: " +
channelFuture.channel().localAddress());
}
});
log.info(" 调用线程执行的,服务器启动成功,监听端口: " +
channelFuture.channel().localAddress());
// 7. 等待通道关闭的异步任务结束
// 服务监听通道会一直等待通道关闭的异步任务结束
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 8 优雅关闭EventLoopGroup,
// 释放掉所有资源包括创建的线程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
}
客户端代码:
@Slf4j
public class NettyEchoClient {
public static void main(String[] args) throws InterruptedException {
//1.创建客户端引导类
Bootstrap bootstrap = new Bootstrap();
//2.创建事件轮询组
NioEventLoopGroup workerEventGroup = new NioEventLoopGroup();
//3.引导类设置=事件轮询组
bootstrap.group(workerEventGroup);
//4.设置通道类型
bootstrap.channel(NioSocketChannel.class);
//5.添加处理器
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());//出站编码器
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug("打印服务端返回:{}", buf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
}
});
//6.连接服务端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8081));
channelFuture.sync();
//7.向服务端发送数据
Channel channel = channelFuture.channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while(true){
String line = scanner.nextLine();
if (line.equals("q")){
channel.close();
break;
}
channel.writeAndFlush(line);
}
}).start();
//8. 关闭释放资源
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
workerEventGroup.shutdownGracefully();
}
});
}
}