Netty读数据源码解析

一.接收读就绪事件

io.netty.channel.nio.NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey, io.netty.channel.nio.AbstractNioChannel)

/**
 * 处理就绪的IO事件
 * @param k 就绪的IO事件
 * @param ch 该就绪事件对应的channel(NioServerSocketChannel / NioSocketChannel)
 */
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    // 对于服务端来说,这里的unsafe就是NioMessageUnsafe
    // 对于客户端来说,这里的unsafe就是NioSocketChannelUnsafe
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    if (!k.isValid()) {
        final EventLoop eventLoop;
        try {
            eventLoop = ch.eventLoop();
        } catch (Throwable ignored) {
            // If the channel implementation throws an exception because there is no event loop, we ignore this
            // because we are only trying to determine if ch is registered to this event loop and thus has authority
            // to close ch.
            return;
        }
        // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
        // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
        // still healthy and should not be closed.
        // See https://github.com/netty/netty/issues/5125
        if (eventLoop == this) {
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
        }
        return;
    }

    try {
        // 获取就绪的IO事件
        int readyOps = k.readyOps();
        // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
        // the NIO JDK channel implementation may throw a NotYetConnectedException.
        // 条件成立:说明发生了连接就绪事件
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
            // See https://github.com/netty/netty/issues/924
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }

        // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
        // 条件成立:说明发生了写就绪事件
        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
            ch.unsafe().forceFlush();
        }

        // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead to a spin loop
        // 处理就绪的事件是读事件或者接收事件
        // 读就绪事件,调用NioSocketChannelUnsafe.read()
        // 接收就绪事件,调用NioMessageUnsafe.read()
        // 条件成立:说明发生了读就绪事件或者接收连接就绪事件
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

可以看到processSelectedKey方法就是处理select上的就绪事件的,我们关注最下面的处理读就绪事件和接收连接就绪事件,因为读就绪事件是发生在客户端channel中的,而接收连接就绪事件是发生在服务端channel中,所以调用的read方法是不一样的,我们这里看处理读就绪事件的read方法

io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

/**
     * 读取客户端发送过来的数据
     */
    @Override
    public final void read() {
        // 得到客户端NioSocketChannel的配置类NioSocketChannelConfig
        final ChannelConfig config = config();

        if (shouldBreakReadReady(config)) {
            clearReadPending();
            return;
        }
        // 得到客户端NioSocketChannel的pipeline
        final ChannelPipeline pipeline = pipeline();

        // 得到缓冲区内存分配器,它是专门用来分配内存缓冲的,如果不是android平台,默认使用PooledByteBufAllocator
        final ByteBufAllocator allocator = config.getAllocator();

        // 默认返回的实例是AdaptiveRecvByteBufAllocator,AdaptiveRecvByteBufAllocator是一个能够预测下次分配多大内存缓冲区的分配处理器,根据里面的分配算法去指定需要分配内存的大小
        final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();

        // 重置读循环最大次数,重置已读取数据的总大小为0,重置已经读循环的次数为0
        allocHandle.reset(config);

        ByteBuf byteBuf = null;
        boolean close = false;
        try {
            do {
                // 在分配处理器的辅助下,得到一个评估后大小的内存缓冲区
                byteBuf = allocHandle.allocate(allocator);
                // 在这行代码中会去从channel中读取数据到byteBuf,然后根据已读到的数据大小去对下一次需要分配的byteBuf大小进行动态尺寸伸缩
                allocHandle.lastBytesRead(doReadBytes(byteBuf));
                // 条件成立:这种情况说明本次读循坏已经从客户端channel读取不到数据了
                if (allocHandle.lastBytesRead() <= 0) {
                    // 释放缓冲区
                    byteBuf.release();
                    byteBuf = null;
                    close = allocHandle.lastBytesRead() < 0;
                    if (close) {
                        // There is nothing left to read as we received an EOF.
                        readPending = false;
                    }
                    // 跳出读循环
                    break;
                }
                // 已经读循环的次数加1
                allocHandle.incMessagesRead(1);
                readPending = false;
                // 向pipeline中传递一个channelRead事件,并且带上这次读循坏读到的数据
                pipeline.fireChannelRead(byteBuf);
                byteBuf = null;
            } while (allocHandle.continueReading());

            // 对这次已读取的总数据大小进行一个评估,以此来判断下一次读数据的时候分配多大的缓冲区(缓冲区容量尺寸伸缩)
            allocHandle.readComplete();
            // 向pipeline中传递一个channelReadComplete事件
            pipeline.fireChannelReadComplete();

            if (close) {
                closeOnRead(pipeline);
            }
        } catch (Throwable t) {
            handleReadException(pipeline, byteBuf, t, close, allocHandle);
        } finally {
            // Check if there is a readPending which was not processed yet.
            // This could be for two reasons:
            // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
            // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
            //
            // See https://github.com/netty/netty/issues/2254
            if (!readPending && !config.isAutoRead()) {
                removeReadOp();
            }
        }
    }
}

首先可以看到会拿到一个ByteBufAllocator,也就是内存分配器,顾名思义它就是用来分配得到一个内存缓冲区的,然后具体这个内存缓冲区的容量需要分配多大就通过内存分配处理器去进行计算了,所以也就是说这里的内存分配处理器就是一个关键,我们下面具体来看下它是如何工作计算出要分配多大内存的

二.接收内存分配处理器

我们先来看下接收内存分配器的继承结构图

 其中红色线代表的是内部类,所以可以看到接收内存分配器netty提供了两个实现,分别是FixedRecvByteBufAllocator和AdaptiveRecvByteBufAllocator,这里我们关键看一下AdaptiveRecvByteBufAllocator,根据名字这个接收内存分配器的主要作用其实就是能够根据读取的消息大小去推断接收这些消息需要分配的内存大小,达到一个自适应的效果

1.自适应接收内存分配器的创建

我们先看一下接收内存分配器是怎么创建的

public RecvByteBufAllocator.Handle recvBufAllocHandle() {
    if (recvHandle == null) {
        recvHandle = config().getRecvByteBufAllocator().newHandle();
    }
    return recvHandle;
}
public <T extends RecvByteBufAllocator> T getRecvByteBufAllocator() {
    return (T) rcvBufAllocator;
}

默认的,netty在读数据的时候就是使用AdaptiveRecvByteBufAllocator,当然我们也可以通过option方法去配置使用其他的内存分配器,而创建了AdaptiveRecvByteBufAllocator实例之后,还要通过newHandle方法去创建出一个HandleImpl实例,从上面的继承关系图可以看出,这个HandleImpl是AdaptiveRecvByteBufAllocator的内部类,而它又是继承于AdaptiveRecvByteBufAllocator的父类DefaultMaxMessagesRecvByteBufAllocator中的内部类MaxMessageHandle。其实这个AdaptiveRecvByteBufAllocator中的这个内部类HandleImpl就是负责实现内存分配自适应的功能,先看它的构造方法

public Handle newHandle() {
    return new HandleImpl(minIndex, maxIndex, initial);
}

分别传入了三个参数,这三个参数其实是外部类AdaptiveRecvByteBufAllocator的三个属性,并且在AdaptiveRecvByteBufAllocator实例被创建的时候会对这三个参数进行初始化,代码如下:

/**
 * 使用默认参数创建一个新的缓冲区分配器,使用默认参数,预期的缓冲区大小从1024开始,不会低于64,也不会高于65536。
 */
public AdaptiveRecvByteBufAllocator() {
    this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
}

默认值如下:

static final int DEFAULT_MINIMUM = 64;
static final int DEFAULT_INITIAL = 2048;
static final int DEFAULT_MAXIMUM = 65536;
/**
 * 使用指定的参数创建一个新的缓冲区分配器
 *
 * @param minimum  the inclusive lower bound of the expected buffer size
 * @param initial  the initial buffer size when no feed back was received
 * @param maximum  the inclusive upper bound of the expected buffer size
 */
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
    checkPositive(minimum, "minimum");
    if (initial < minimum) {
        throw new IllegalArgumentException("initial: " + initial);
    }
    if (maximum < initial) {
        throw new IllegalArgumentException("maximum: " + maximum);
    }

    // 通过二分查找法在SIZE_TABLE找出与minimum相等或者最接近的值的下标
    int minIndex = getSizeTableIndex(minimum);
    // 如果找到的值小于minimum,那么minIndex就等于这个值在SIZE_TABLE数组的下标+1
    if (SIZE_TABLE[minIndex] < minimum) {
        this.minIndex = minIndex + 1;
    }
    // 如果找到的值等于minimum,那么minIndex就直接等于这个值在SIZE_TABLE数组的下标
    else {
        this.minIndex = minIndex;
    }

    // 通过二分查找法在SIZE_TABLE找出与maximum相等或者最接近的值的下标
    int maxIndex = getSizeTableIndex(maximum);
    // 如果找到的值大于maximum,那么minIndex就等于这个值在SIZE_TABLE数组的下标-1
    if (SIZE_TABLE[maxIndex] > maximum) {
        this.maxIndex = maxIndex - 1;
    }
    // 如果找到的值等于minimum,那么maxIndex就直接等于这个值在SIZE_TABLE数组的下标
    else {
        this.maxIndex = maxIndex;
    }

    this.initial = initial;
}

可以看到上面的方法其实就是根据传入的minimum和maximum去从SIZE_TABLE数组中分别通过二分查找法找到最接近对应值的元素下标,并分别赋值了给minIndex和maxIndex,那么SIZE_TABLE这个数组又是从哪里初始化的呢?答案是在静态代码块中,代码如下:

static {
    List<Integer> sizeTable = new ArrayList<Integer>();
    // 从16开始,每一次增加16放到sizeTable中直到增加的数到496为止
    // 例如: 16,32,48,64...496
    for (int i = 16; i < 512; i += 16) {
        sizeTable.add(i);
    }

    // 从512开始,每一次都把对应的二进制左移1位,也就是说每一次都把自身*2再放到sizeTable中,直到int类型溢出变成负数
    for (int i = 512; i > 0; i <<= 1) { // lgtm[java/constant-comparison]
        sizeTable.add(i);
    }

    // 把初始化好的集合放到SIZE_TABLE数组中
    SIZE_TABLE = new int[sizeTable.size()];
    for (int i = 0; i < SIZE_TABLE.length; i ++) {
        SIZE_TABLE[i] = sizeTable.get(i);
    }
}

初始化的规则就是在512之前,最小从16开始,每一个数都比前一个数大16,而从512开始,之后的每一个数都会比前一个数增大一倍,直到int类型溢出变成负数结束。初始化完了minimum,initial,maximum和SIZE_TABLE之后,HandleImpl实例就需要使用这几个参数了

HandleImpl(int minIndex, int maxIndex, int initial) {
    this.minIndex = minIndex;
    this.maxIndex = maxIndex;

    // 通过二分查找法在SIZE_TABLE找出与initial相等或者最接近的值的下标
    index = getSizeTableIndex(initial);
    nextReceiveBufferSize = SIZE_TABLE[index];
}

通过二分查找法去给initial这个值从SIZE_TABLE中找到对应的元素下标,然后把这个元素下标对应的值拿出来并赋值给nextReceiveBufferSize,因为默认地initial就等于2048,而SIZE_TABLE中也有一个元素是2048,所以默认地nextReceiveBufferSize就等于2048

2.创建预估大小的ByteBuf

上面说了HandleImpl的创建以及参数的初始化,那么这些参数对于自适应创建ByteBuf有什么用呢,我们看到allocate方法

/**
 * 返回一个根据guess()方法指定大小的内存缓冲区
 * @param alloc
 * @return
 */
@Override
public ByteBuf allocate(ByteBufAllocator alloc) {
    return alloc.ioBuffer(guess());
}

该方法是HandleImpl的父类MaxMessageHandle实现的,作用就是根据指定的大小从内存分配器中创建出一个内存缓冲区,而这个缓冲区的大小是通过guess方法返回的,guess方法是一个接口方法,MaxMessageHandle并没有实现,所以这里需要HandleImpl实现,代码如下:

/**
 * 返回下一次分配的内存缓冲区的大小
 * @return
 */
@Override
public int guess() {
    return nextReceiveBufferSize;
}

可以看到返回的就是我们上面从SIZE_TABLE中获取到的nextReceiveBufferSize,也就是说首次通过自适应接收内存分配器创建出来的内存大小就是2048个字节

3.如何进行自适应分配内存

do {
    // 在分配处理器的辅助下,得到一个评估后大小的内存缓冲区
    byteBuf = allocHandle.allocate(allocator);
    // 在这行代码中会去从channel中读取数据到byteBuf,然后根据已读到的数据大小去对下一次需要分配的byteBuf大小进行动态尺寸伸缩
    allocHandle.lastBytesRead(doReadBytes(byteBuf));
    // 条件成立:这种情况说明本次读循坏已经从客户端channel读取不到数据了
    if (allocHandle.lastBytesRead() <= 0) {
        // 释放缓冲区
        byteBuf.release();
        byteBuf = null;
        close = allocHandle.lastBytesRead() < 0;
        if (close) {
            // There is nothing left to read as we received an EOF.
            readPending = false;
        }
        // 跳出读循环
        break;
    }
    // 已经读循环的次数加1
    allocHandle.incMessagesRead(1);
    readPending = false;
    // 向pipeline中传递一个channelRead事件,并且带上这次读循坏读到的数据
    pipeline.fireChannelRead(byteBuf);
    byteBuf = null;
} while (allocHandle.continueReading());

第一次循环通过allocate方法分配出一个ByteBuf,大小是2048,然后调用doReadBytes方法,该方法是一个抽象方法,在NioSocketChannel中的实现如下:

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    // 设置想要读取的数据大小等于ByteBuf的容量大小
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    // 读取客户端channel读缓冲区的数据放到ByteBuf中,读取长度最大为ByteBuf分配的大小,并且返回读取数据的大小
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

调用ByteBuf的writableBytes方法获取到这个ByteBuf的可写大小,然后传给了DefaultMaxMessagesRecvByteBufAllocator接收内存分配器的attemptedBytesRead方法:

@Override
public void attemptedBytesRead(int bytes) {
    attemptedBytesRead = bytes;
}

赋值给attemptedBytesRead变量,这个变量表示预估这一次要读取的消息大小,然后就调用ByteBuf的writeBytes方法把SocketChannel的读缓冲区的数据写入到ByteBuf中,并且读取的消息大小不能大于ByteBuf的可写大小,最后把读取到的消息大小返回出去,在返回出去之后就把读取到的消息大小传入到AdaptiveRecvByteBufAllocator的lastByteRead方法中

@Override
public void lastBytesRead(int bytes) {
    // If we read as much as we asked for we should check if we need to ramp up the size of our next guess.
    // This helps adjust more quickly when large amounts of data is pending and can avoid going back to
    // the selector to check for more data. Going back to the selector can add significant latency for large
    // data transfers.
    // 如果实际读取的数据大小等于想要读取的数据大小
    if (bytes == attemptedBytesRead()) {
        record(bytes);
    }
    super.lastBytesRead(bytes);
}

此时会判断从SocketChannel中读取到的消息大小是否等于在读消息之前设置的预估读取的消息大小,如果相等,那么就调用record方法

/**
 * 根据实际读取的数据大小去评估下一次读数据的缓冲区容量是需要缩小还是扩大
 * @param actualReadBytes
 */
private void record(int actualReadBytes) {
    // 当本次实际已读取的数据大小 小于或等于 SIZE_TABL[index-1-1]的时候,说明可能需要读取的数据并不多,所以对缓冲区容量进行缩小
    // 当第二次实际已读取的数据大小 小于或等于 SIZE_TABL[index-1-1]的时候,说明需要读取的数据并不多,所以对缓冲区容量直接进行缩小
    if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
        if (decreaseNow) {
            // 缩小到SIZE_TABL[index-1]
            index = max(index - INDEX_DECREMENT, minIndex);
            nextReceiveBufferSize = SIZE_TABLE[index];
            decreaseNow = false;
        } else {
            decreaseNow = true;
        }
    }
    // 当本次实际已读取的数据大小 大于或等于下一次读取的数据大小的时候,直接对缓冲区容量进行扩容
    else if (actualReadBytes >= nextReceiveBufferSize) {
        index = min(index + INDEX_INCREMENT, maxIndex);
        nextReceiveBufferSize = SIZE_TABLE[index];
        decreaseNow = false;
    }
}

这个方法就是实现自适应功能的核心了,如果从SocketChannel读取的数据大小=分配的内存大小,就需要对内存缓冲区容量进行扩容,扩容规则就是扩容到原本容量大小值对应在SIZE_TABLE的元素下标 +4对应的值。在对内存缓冲区的下一次需要分配的大小进行了扩容或者缩小之后,最后还调用了父类DefaultMaxMessagesRecvByteBufAllocator的lastBytesRead方法

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

该方法就是使用lastBytesRead这个变量记录这一次读取的消息大小,以及把读取的消息大小累加到totalBytesRead变量。此时一次读循环就结束了,然后会在while中判断是否需要继续读循环

/**
 * 是否继续读循环
 * @return  true=>继续,false=>不继续
 */
@Override
public boolean continueReading() {
    return continueReading(defaultMaybeMoreSupplier);
}

@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
    // 继续读循环条件成立:
    // 1.config.isAutoRead(),默认等于true
    // 2.maybeMoreDataSupplier.get(),想要去读取的数据的大小 等于 上一次读取的数据大小,说明可能会还有很多数据没有读取
    // 3.此时经过的读循环次数 小于 可读循环的最大次数
    // 4.此时已读取的数据大小 大于 0
    // 上面的4个条件都成立那么读循环就可以继续
    return config.isAutoRead() &&
           (!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
           totalMessages < maxMessagePerRead &&
           totalBytesRead > 0;
}

private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
    @Override
    public boolean get() {
        return attemptedBytesRead == lastBytesRead;
    }
};

判断是否继续读循环的有4个条件判断,其中关键的一个就是我们设置的预估要读取的消息大小和本次实际读取的数据相等,表示还有可能SocketChannel读缓冲区中还有数据没有读取到,需要继续读循环去读取。当第二次及以上进行读循环的时候,有一个if判断

if (allocHandle.lastBytesRead() <= 0) {
    // 释放缓冲区
    byteBuf.release();
    byteBuf = null;
    close = allocHandle.lastBytesRead() < 0;
    if (close) {
        // There is nothing left to read as we received an EOF.
        readPending = false;
    }
    // 跳出读循环
    break;
}

这个if条件会去判断本次读循环的数据是否小于等于0,如果是等于0则说明上一次读循环刚好已经把SocketChannel读缓冲区的消息读取完了,所以就需要跳出读循环了,并且把ByteBuf释放掉。并且,在整个读循环都结束了之后,就会调用自适应接收内存分配器的readComplete方法

/**
 * 当读取channel数据完毕的时候调用,此时会通过record方法对本次读取的所有数据评估,作为下一次分配的缓冲区大小
 */
@Override
public void readComplete() {
    record(totalBytesRead());
}

在自适应接收内存分配器中会重写这个方法,在里面会再次调用record方法,作用就是整个读数据过程完成之后,根据读取到的数据大小去调整下一次读数据的时候需要分配的内存大小,因为netty会认为你这一次读取到这么多的数据,那么下一次可能还会读取到这么多

三.传递pipeline事件

在上面的读循环代码中,每进行一次读循环,都会把读取到的数据通过pipeline传递出去,具体就是会调用到pipeline中每一个handler的channelRead方法,所以说如果客户端发送一个很大的消息,我们一次读循环不能完全读取的话,那么就会分多次读取,这也就会使得handler中的channelRead方法被多次调用。当然,在整个读循环跳出结束之后,还会向pipeline中传递一个channelReadComplete事件,从而会调用后所有handler的channelReadComplete方法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值