Netty学习之旅------NioSocketChannel源码分析之读事件处理逻辑

本文解析了Netty中的通道读事件处理,重点讨论了ByteBuf的管理、特别是如何通过RecvByteBufAllocator接口预测并动态调整接收缓冲区容量,以及AdaptiveRecvByteBufAllocator的自适应策略。
摘要由CSDN通过智能技术生成

close = false;

}

} catch (Throwable t) {

handleReadException(pipeline, byteBuf, t, close); //@13

} 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 (!config.isAutoRead() && !isReadPending()) {

removeReadOp();

}

}

}

代码@1,如果未开启自动读,并且没有读事件等待,则移除读事件。

代码@2,移除读事件,在具体的子类中实现,在AbstractNioUnsafe中的具体实现:

protected final void removeReadOp() {

SelectionKey key = selectionKey();

// Check first if the key is still valid as it may be canceled as part of the deregistration

// from the EventLoop

// See https://github.com/netty/netty/issues/2104

if (!key.isValid()) {

return;

}

int interestOps = key.interestOps();

if ((interestOps & readInterestOp) != 0) {

// only remove readInterestOp if needed

key.interestOps(interestOps & ~readInterestOp);

}

}

代码@3,允许消息预读取最大次数,循环次数,主要是避免一个通道大量数据的读写而拖慢其他通道的处理。

代码@4,内存分配的方式,这里的目的就是可以分配足够的内存字节以便于从通道读取数据。下文会重点讲解。

代码@5,利用代码@4处的分配器,分配一个ByteBuf,用于接收通道中的数据。

代码@6,从通道中读取数据,具体在子类中实现,在NioSocketChannel中的实现如下:

@Override

protected int doReadBytes(ByteBuf byteBuf) throws Exception {

return byteBuf.writeBytes(javaChannel(), byteBuf.writableBytes());//javaChannel()方法返回的就是java.nio.SocketChannel

}

这里涉及到ByteBuf与通道打交道,最终还是会落到java.nio.Channel和java.nio.ByteBuffer上去,所以下文我也觉得有必要深究一下整个API的调用,增强对java.nio原生api的使用。

代码@7,如果没有读到可用数据,则回收刚申请的ByteBuf,如果读到的数据小于0,则说明需要将通道关闭。设置close=true,并跳出循环。

代码@8,将读到的内容(ByteBuf)通过管道传播到各个Handler,此时Handler的处理,默认都会在IO线程中处理。

代码@9,如果本次读到的字节数小于分配ByteBuf的可写字节数,说明该通道已经没有数据可读,结束本次循环。

代码@10,再次从通道中读取,直到读取次数已经超过配置的最大预读取次数,maxMessagesPreRead,如果是ServerChannel或AbstractNioChannel,则默认为16,其他的默认为1。

代码@11,触发读完成事件。

代码@12,该方法非常重要,IO线程将本次读取的字节数反馈给 接收ByteBuf分配器,方便下一次分配合理的ByteBuf,足够用,但不能超过太多。

代码@13,如果发生异常触发异常事件。

对整个通道读事件处理,上述是主要的处理流程,接下来重点分析如下两个方面代码@4: RecvByteBufAllocator和代码@8

2.1 RecvByteBuffAllocator系列类分析

/**

  • Allocates a new receive buffer whose capacity is probably large enough to read all inbound data and small enough

  • not to waste its space.

*/

public interface RecvByteBufAllocator {

/**

  • Creates a new handle. The handle provides the actual operations and keeps the internal information which is

  • required for predicting an optimal buffer capacity.

*/

Handle newHandle();

interface Handle {

/**

  • Creates a new receive buffer whose capacity is probably large enough to read all inbound data and small

  • enough not to waste its space.

*/

ByteBuf allocate(ByteBufAllocator alloc);

/**

  • Similar to {@link #allocate(ByteBufAllocator)} except that it does not allocate anything but just tells the

  • capacity.

*/

int guess();

/**

  • Records the the actual number of read bytes in the previous read operation so that the allocator allocates

  • the buffer with potentially more correct capacity.

  • @param actualReadBytes the actual number of read bytes in the previous read operation

*/

void record(int actualReadBytes);

}

}

该接口主要要解决的问题就是:在处理通道读事件的时候,如何确定需要分配多大的ByteBuf呢?

对如下英文进行通俗化的理解:

  1. Allocates a new receive buffer whose capacity is probably large enough to read all inbound data and s

mall enough

* not to waste its space.

分配一个接收ByteBuf,希望这个容量足够大,能够容纳通道中可读数据,但又尽量少,够用就好,别浪费空间。

2)guess 方法,只返回猜测,建议的容量(capacity),不执行实际的内存申请。

3)Records the the actual number of read bytes in the previous read operation so that the allocator allocates the buffer with potentially more correct capacity.

记录上传读到的实际字节的大小,以便分配器更加准确的分配正确的容量。

2.1.1 FixedRecvByteBufAllocator  固定容量分配,该方式简单,但忽略了IO线程的反馈。

/**

* The {@link RecvByteBufAllocator} that always yields the same buffer

* size prediction.  This predictor ignores the feed back from the I/O thread.

*/

public class FixedRecvByteBufAllocator implements RecvByteBufAllocator

2.1.2 AdaptiveRecvByteBufAllocator 自适应分配算法

1)概述

/**

* The {@link RecvByteBufAllocator} that automatically increases and

* decreases the predicted buffer size on feed back.

* It gradually increases the expected number of readable bytes if the previous

* read fully filled the allocated buffer.  It gradually decreases the expected

* number of readable bytes if the read operation was not able to fill a certain

* amount of the allocated buffer two times consecutively.  Otherwise, it keeps

* returning the same prediction.

*/

AdaptiveRecvByteBufAllocator 根据IO线程的反馈自动增加或减少预期的buffer size。

如果上一次读填满了分配的缓存区,则增大猜测缓存区的大小,如果上一次读没有填满分配的缓冲区,则减少。

2)AdaptiveRecvByteBufAllocator 源码分析

1、核心属性详解

static final int DEFAULT_MINIMUM = 64; // 分配的最小容量

static final int DEFAULT_INITIAL = 1024; // 初始容量

static final int DEFAULT_MAXIMUM = 65536; // 最大容量64k

private static final int INDEX_INCREMENT = 4; //增长索引数,在SIZE_TABLE下的索引

private static final int INDEX_DECREMENT = 1; // 减少索引数

private static final int[] SIZE_TABLE; //小于512个字节,按16个字节递增,大于512字节,成倍增长

// SIZE_TABLE的初始化,其实这里有效使用的最大内存为65536, 64K

static {

List sizeTable = new ArrayList();

for (int i = 16; i < 512; i += 16) { //小于512字节,从16开始,以16递增

sizeTable.add(i);

}

for (int i = 512; i > 0; i <<= 1) { //大于512字节,成倍增长,该循环在超过int最大值时退出

sizeTable.add(i);

}

SIZE_TABLE = new int[sizeTable.size()];

for (int i = 0; i < SIZE_TABLE.length; i ++) {

SIZE_TABLE[i] = sizeTable.get(i);

}

}

private final int minIndex; // DEFAULT_MINIMUM 所在SIZE_TABLE中的下标

private final int maxIndex; // DEFAULT_MAXIMUM 所在SIZE_TABLE中的下标

private final int initial; // 初始时,分配的buffer大小,默认为1K

2、构造方法

/**

  • Creates a new predictor with the default parameters. With the default

  • parameters, the expected buffer size starts from {@code 1024}, does not

  • go down below {@code 64}, and does not go up above {@code 65536}.

*/

private AdaptiveRecvByteBufAllocator() {

this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);

}

/**

  • Creates a new predictor with the specified parameters.

  • @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) {

if (minimum <= 0) {

throw new IllegalArgumentException("minimum: " + minimum);

}

if (initial < minimum) {

throw new IllegalArgumentException("initial: " + initial);

}

if (maximum < initial) {

throw new IllegalArgumentException("maximum: " + maximum);

}

int minIndex = getSizeTableIndex(minimum); //@1

if (SIZE_TABLE[minIndex] < minimum) {

this.minIndex = minIndex + 1;

} else {

this.minIndex = minIndex;

}

int maxIndex = getSizeTableIndex(maximum);

if (SIZE_TABLE[maxIndex] > maximum) {

this.maxIndex = maxIndex - 1;

} else {

this.maxIndex = maxIndex;

}

this.initial = initial;

}

构造方法,主要是初始化minIndex、maxIndex,initial。重点看一下getSizeTableIndex方法的详解:

private static int getSizeTableIndex(final int size) {

for (int low = 0, high = SIZE_TABLE.length - 1;😉 { //典型的二分查找算法

if (high < low) { //@1

return low;

}

if (high == low) { //@2

return high;

}

int mid = low + high >>> 1;

int a = SIZE_TABLE[mid];

int b = SIZE_TABLE[mid + 1];

if (size > b) {

low = mid + 1;

} else if (size < a) {

high = mid - 1;

} else if (size == a) {

return mid;

} else {

return mid + 1;

}

}

}

二分查找的原理,对于一个有序排序,用中间的数与需要查找的数进行比较,然后在另外一半里进行再分半查询,每次将范围缩小一半。

二分查找,对于high的初始值有学问,使用的是SIZE_TABLE.length - 1,就是为了确保越界。(min = low + hign >>> 1),如果在后半部分,确保该值最大为该值。

3、内部Handler实现类

private static final class HandleImpl implements Handle {

private final int minIndex;

private final int maxIndex;

private int index;

private int nextReceiveBufferSize;

private boolean decreaseNow;

HandleImpl(int minIndex, int maxIndex, int initial) {

this.minIndex = minIndex;

this.maxIndex = maxIndex;

index = getSizeTableIndex(initial);

nextReceiveBufferSize = SIZE_TABLE[index];

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-xn3GHeoD-1714933734811)]

[外链图片转存中…(img-H93N5Dxh-1714933734811)]

[外链图片转存中…(img-dnHNqiNb-1714933734812)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值