Netty如何高效接收网络数据?聊透ByteBuffer动态自适应扩缩 四
3.1 分配DirectByteBuffer接收网络数据
Sub Reactor在接收NioSocketChannel上的IO数据时,都会分配一个ByteBuffer用来存放接收到的IO数据。
这里大家可能觉得比较奇怪,为什么在NioSocketChannel接收数据这里会有两个ByteBuffer分配器呢?一个是ByteBufAllocator,另一个是RecvByteBufAllocator。
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
这两个ByteBuffer又有什么区别和联系呢?
在上篇文章?《抓到Netty一个Bug,顺带来透彻地聊一下Netty是如何高效接收网络连接》中,笔者为了阐述上篇文章中提到的Netty在接收网络连接时的Bug时,简单和大家介绍了下这个RecvByteBufAllocator。
在上篇文章提到的NioServerSocketChannelConfig中,这里的RecvByteBufAllocator类型为ServerChannelRecvByteBufAllocator。
image.png
还记得这个ServerChannelRecvByteBufAllocator类型在4.1.69.final版本引入是为了解决笔者在上篇文章中提到的那个Bug吗?在4.1.69.final版本之前,NioServerSocketChannelConfig中的RecvByteBufAllocator类型为AdaptiveRecvByteBufAllocator。
而在本文中NioSocketChannelConfig中的RecvByteBufAllocator类型为AdaptiveRecvByteBufAllocator。
image.png
所以这里recvBufAllocHandle()获得到的RecvByteBufAllocator为AdaptiveRecvByteBufAllocator。顾名思义,这个类型的RecvByteBufAllocator可以根据NioSocketChannel上每次到来的IO数据大小来自适应动态调整ByteBuffer的容量。
对于客户端NioSocketChannel来说,它里边包含的IO数据时客户端发送来的网络数据,长度是不定的,所以才会需要这样一个可以根据每次IO数据的大小来自适应动态调整容量的ByteBuffer来接收。
如果我们把用于接收数据用的ByteBuffer看做一个桶的话,那么小数据用大桶装或者大数据用小桶装肯定是不合适的,所以我们需要根据接收数据的大小来动态调整桶的容量。而AdaptiveRecvByteBufAllocator的作用正是用来根据每次接收数据的容量大小来动态调整ByteBuffer的容量的。
现在RecvByteBufAllocator笔者为大家解释清楚了,接下来我们继续看ByteBufAllocator。
大家这里需要注意的是AdaptiveRecvByteBufAllocator并不会真正的去分配ByteBuffer,它只是负责动态调整分配ByteBuffer的大小。
而真正具体执行内存分配动作的是这里的ByteBufAllocator类型为PooledByteBufAllocator。它会根据AdaptiveRecvByteBufAllocator动态调整出来的大小去真正的申请内存分配ByteBuffer。
PooledByteBufAllocator为Netty中的内存池,用来管理堆外内存DirectByteBuffer。
AdaptiveRecvByteBufAllocator中的allocHandle在上篇文章中我们也介绍过了,它的实际类型为MaxMessageHandle。
public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {
@Override
public Handle newHandle() {
return new HandleImpl(minIndex, maxIndex, initial);
}
private final class HandleImpl extends MaxMessageHandle {
.................省略................
}
}
在MaxMessageHandle中包含了用于动态调整ByteBuffer容量的统计指标。
public abstract class MaxMessageHandle implements ExtendedHandle {
private ChannelConfig config;
//用于控制每次read loop里最大可以循环读取的次数,默认为16次
//可在启动配置类ServerBootstrap中通过ChannelOption.MAX_MESSAGES_PER_READ选项设置。
private int maxMessagePerRead;
//用于统计read loop中总共接收的连接个数,NioSocketChannel中表示读取数据的次数
//每次read loop循环后会调用allocHandle.incMessagesRead增加记录接收到的连接个数
private int totalMessages;
//用于统计在read loop中总共接收到客户端连接上的数据大小
private int totalBytesRead;
//表示本次read loop 尝试读取多少字节,byteBuffer剩余可写的字节数
private int attemptedBytesRead;
//本次read loop读取到的字节数
private int lastBytesRead;
//预计下一次分配buffer的容量,初始:2048
private int nextReceiveBufferSize;
...........省略.............
}
在每轮read loop开始之前,都会调用allocHandle.reset(config)重置清空上一轮read loop的统计指标。
@Override
public void reset(ChannelConfig config) {
this.config = config;
//默认每次最多读取16次
maxMessagePerRead = maxMessagesPerRead();
totalMessages = totalBytesRead = 0;
}
在每次开始从NioSocketChannel中读取数据之前,需要利用PooledByteBufAllocator在内存池中为ByteBuffer分配内存,默认初始化大小为2048,这个容量由guess()方法决定。
byteBuf = allocHandle.allocate(allocator);
@Override
public ByteBuf allocate(ByteBufAllocator alloc) {
return alloc.ioBuffer(guess());
}
@Override
public int guess() {
//预计下一次分配buffer的容量,一开始为2048
return nextReceiveBufferSize;
}
在每次通过doReadBytes从NioSocketChannel中读取到数据后,都会调用allocHandle.lastBytesRead(doReadBytes(byteBuf))记录本次读取了多少字节数据,并统计本轮read loop目前总共读取了多少字节。
@Override
public void lastBytesRead(int bytes) {
lastBytesRead = bytes;
if (bytes > 0) {
totalBytesRead += bytes;
}
}
每次循环从NioSocketChannel中读取数据之后,都会调用allocHandle.incMessagesRead(1)。统计当前已经读取了多少次。如果超过了最大读取限制此时16次,就需要退出read loop。去处理其他NioSocketChannel上的IO事件。
@Override
public final void incMessagesRead(int amt) {
totalMessages += amt;
}
在每次read loop循环的末尾都需要通过调用allocHandle.continueReading()来判断是否继续read loop循环读取NioSocketChannel中的数据。
@Override
public boolean continueReading() {
return continueReading(defaultMaybeMoreSupplier);
}
private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
@Override
public boolean get() {
//判断本次读取byteBuffer是否满载而归
return attemptedBytesRead == lastBytesRead;
}
};
@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
return config.isAutoRead() &&
(!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
totalMessages < maxMessagePerRead &&
totalBytesRead > 0;
}
• attemptedBytesRead :表示当前ByteBuffer预计尝试要写入的字节数。
• lastBytesRead :表示本次read loop真实读取到了多少个字节。
defaultMaybeMoreSupplier用于判断经过本次read loop读取数据后,ByteBuffer是否满载而归。如果是满载而归的话(attemptedBytesRead == lastBytesRead),表明可能NioSocketChannel里还有数据。如果不是满载而归,表明NioSocketChannel里没有数据了已经。
是否继续进行read loop需要同时满足以下几个条件:
• totalMessages < maxMessagePerRead 当前读取次数是否已经超过16次,如果超过,就退出do(...)while()循环。进行下一轮OP_READ事件的轮询。因为每个Sub Reactor管理了多个NioSocketChannel,不能在一个NioSocketChannel上占用太多时间,要将机会均匀地分配给Sub Reactor所管理的所有NioSocketChannel。
•totalBytesRead > 0 本次OP_READ事件处理是否读取到了数据,如果已经没有数据可读了,那么就直接退出read loop。
•!respectMaybeMoreData || maybeMoreDataSupplier.get() 这个条件比较复杂,它其实就是通过respectMaybeMoreData字段来控制NioSocketChannel中可能还有数据可读的情况下该如何处理。
◆maybeMoreDataSupplier.get():true表示本次读取从NioSocketChannel中读取数据,ByteBuffer满载而归。说明可能NioSocketChannel中还有数据没读完。fasle表示ByteBuffer还没有装满,说明NioSocketChannel中已经没有数据可读了。
◆respectMaybeMoreData = true表示要对可能还有更多数据进行处理的这种情况要respect认真对待,如果本次循环读取到的数据已经装满ByteBuffer,表示后面可能还有数据,那么就要进行读取。如果ByteBuffer还没装满表示已经没有数据可读了那么就退出循环。
◆respectMaybeMoreData = false表示对可能还有更多数据的这种情况不认真对待 not respect。不管本次循环读取数据ByteBuffer是否满载而归,都要继续进行读取,直到读取不到数据在退出循环,属于无脑读取。