抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(三)

r660926
发布于 2022-8-9 18:55
浏览
0收藏

3. RecvByteBufAllocator简介


Reactor在处理对应Channel上的IO数据时,都会采用一个ByteBuffer来接收Channel上的IO数据。而本小节要介绍的RecvByteBufAllocator正是用来分配ByteBuffer的一个分配器。

 

还记得这个RecvByteBufAllocator在哪里被创建的吗??

 

在?《聊聊Netty那些事儿之Reactor在Netty中的实现(创建篇)》一文中,在介绍NioServerSocketChannel的创建过程中提到,对应Channel的配置类NioServerSocketChannelConfig也会随着NioServerSocketChannel的创建而创建。

 

 public NioServerSocketChannel(ServerSocketChannel channel) {
        super(null, channel, SelectionKey.OP_ACCEPT);
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
    }

在创建NioServerSocketChannelConfig的过程中会创建RecvByteBufAllocator

  public DefaultChannelConfig(Channel channel) {
            this(channel, new AdaptiveRecvByteBufAllocator());
    }

这里我们看到NioServerSocketChannel中的RecvByteBufAllocator实际类型为AdaptiveRecvByteBufAllocator,顾名思义,这个类型的RecvByteBufAllocator可以根据Channel上每次到来的IO数据大小来自适应动态调整ByteBuffer的容量。

 

对于服务端NioServerSocketChannel来说,它上边的IO数据就是客户端的连接,它的长度和类型都是固定的,所以在接收客户端连接的时候并不需要这样的一个ByteBuffer来接收,我们会将接收到的客户端连接存放在List<Object> readBuf集合中

 

对于客户端NioSocketChannel来说,它上边的IO数据时客户端发送来的网络数据,长度是不定的,所以才会需要这样一个可以根据每次IO数据的大小来自适应动态调整容量的ByteBuffer来接收。

 

那么看起来这个RecvByteBufAllocator和本文的主题不是很关联,因为在接收连接的过程中并不会怎么用到它,这个类笔者还会在后面的文章中详细介绍,之所以这里把它拎出来单独介绍是因为它和本文开头提到的Bug有关系,这个Bug就是由这个类引起的。

 

3.1 RecvByteBufAllocator.Handle的获取


在本文中,我们是通过NioServerSocketChannel中的unsafe底层操作类来获取RecvByteBufAllocator.Handle的

final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
protected abstract class AbstractUnsafe implements Unsafe {
        @Override
        public RecvByteBufAllocator.Handle recvBufAllocHandle() {
            if (recvHandle == null) {
                recvHandle = config().getRecvByteBufAllocator().newHandle();
            }
            return recvHandle;
        }
}

我们看到最终会在NioServerSocketChannel的配置类NioServerSocketChannelConfig中获取到AdaptiveRecvByteBufAllocator

public class DefaultChannelConfig implements ChannelConfig {
    //用于Channel接收数据用的buffer分配器  类型为AdaptiveRecvByteBufAllocator
    private volatile RecvByteBufAllocator rcvBufAllocator;
}

AdaptiveRecvByteBufAllocator中会创建自适应动态调整容量的ByteBuffer分配器。

public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {

    @Override
    public Handle newHandle() {
        return new HandleImpl(minIndex, maxIndex, initial);
    }
    
    private final class HandleImpl extends MaxMessageHandle {
                  .................省略................
    }
}

这里的newHandle方法返回的具体类型为MaxMessageHandle,这个MaxMessageHandle里边保存了每次从Channel中读取IO数据的容量指标,方便下次读取时分配合适大小的buffer

 

每次在使用allocHandle前需要调用allocHandle.reset(config);重置里边的统计指标。

    public abstract class MaxMessageHandle implements ExtendedHandle {
        private ChannelConfig config;
        //每次事件轮询时,最多读取16次
        private int maxMessagePerRead;
        //本次事件轮询总共读取的message数,这里指的是接收连接的数量
        private int totalMessages;
        //本次事件轮询总共读取的字节数
        private int totalBytesRead;

       @Override
        public void reset(ChannelConfig config) {
            this.config = config;
            //默认每次最多读取16次
            maxMessagePerRead = maxMessagesPerRead();
            totalMessages = totalBytesRead = 0;
        }
    }

 •maxMessagePerRead:用于控制每次read loop里最大可以循环读取的次数,默认为16次,可在启动配置类ServerBootstrap中通过ChannelOption.MAX_MESSAGES_PER_READ选项设置。

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
  .channel(NioServerSocketChannel.class)
  .option(ChannelOption.MAX_MESSAGES_PER_READ, 自定义次数)

 •totalMessages:用于统计read loop中总共接收的连接个数,每次read loop循环后会调用allocHandle.incMessagesRead增加记录接收到的连接个数。

        @Override
        public final void incMessagesRead(int amt) {
            totalMessages += amt;
        }

 •totalBytesRead:用于统计在read loop中总共接收到客户端连接上的数据大小,这个字段主要用于sub reactor在接收客户端NioSocketChannel上的网络数据用的,本文我们介绍的是main reactor接收客户端连接,所以这里并不会用到这个字段。这个字段会在sub reactor每次读取完NioSocketChannel上的网络数据时增加记录。

        @Override
        public void lastBytesRead(int bytes) {
            lastBytesRead = bytes;
            if (bytes > 0) {
                totalBytesRead += bytes;
            }
        }

 

MaxMessageHandler中还有一个非常重要的方法就是在每次read loop末尾会调用allocHandle.continueReading()方法来判断读取连接次数是否已满16次,来决定main reactor线程是否退出循环。

             

  do {
                        //底层调用NioServerSocketChannel->doReadMessages 创建客户端SocketChannel
                        int localRead = doReadMessages(readBuf);
                        if (localRead == 0) {
                            break;
                        }
                        if (localRead < 0) {
                            closed = true;
                            break;
                        }
                        //统计在当前事件循环中已经读取到得Message数量(创建连接的个数)
                        allocHandle.incMessagesRead(localRead);
                    } while (allocHandle.continueReading());

 抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(三)-鸿蒙开发者社区

image.png


红框中圈出来的两个判断条件和本文主题无关,我们这里不需要关注,笔者会在后面的文章详细介绍。

 

 •totalMessages < maxMessagePerRead:在本文的接收客户端连接场景中,这个条件用于判断main reactor线程在read loop中的读取次数是否超过了16次。如果超过16次就会返回false,main reactor线程退出循环。

 

 •totalBytesRead > 0:用于判断当客户端NioSocketChannel上的OP_READ事件活跃时,sub reactor线程在read loop中是否读取到了网络数据。

 

以上内容就是RecvByteBufAllocator.Handle在接收客户端连接场景下的作用,大家这里仔细看下这个allocHandle.continueReading()方法退出循环的判断条件,再结合整个do{....}while(...)接收连接循环体,感受下是否哪里有些不对劲?Bug即将出现~~~

标签
已于2022-8-9 18:55:47修改
收藏
回复
举报
回复
    相关推荐