Netty ByteBuf原理及其源码分析

类图

缓冲区介绍

当我们进行数据传输的时候,往往需要缓冲区。java NIO 中自带的提供的就是java.nio.Buffer
但是由于java自带的过于复杂,而且自身也有一定的缺陷(定长,一个标识位position等)。Netty便提供的自己的缓冲ByteBuf

Nio ByteBuffer 和 Netty ByteBuf 对比

1.指针问题

public class Test2 {
    public static void main(String[] args) {
        String content = "hello,world";
        ByteBuffer byteBuffer = ByteBuffer.allocate(256);
        byteBuffer.put(content.getBytes());
        byteBuffer.flip();
        byte[] bufferValue = new byte[byteBuffer.remaining()];
        byteBuffer.get(bufferValue);
        System.out.println(new String(bufferValue));
    }
}

示例中就是一种比较常见的NIO操作,比较关键的代码 byteBuffer.flip();它会把limit设置为position的位置。否则读取到的将会是错误的内容。

ByteBuf通过2个索引来维护缓冲区的读写操作。读操作通过readerIndex,写操作通过writeIndex。

他们的初始值都为0,数据的写入将导致writeIndex增加,数据的读取将会导致readerIndex增加。但是它不会操作writeIndex。读取之后在0和readIndex范围称之为discard。调用discardReadBytes方法。可以释放这部分空间。readIndex和writeIndex之间的数据为可读数据。writeIndex和limit之间的数据为可写的空间。由于读写由不同的指针来维护,这样就可以避免NIO中显示的调用flip()来切换不同的操作了。

2.定长问题

操作NIO的时候,当我们对缓冲区put的时候,如果缓冲区空间不够,将会抛出异常。为了避免这个问题。Netty在write数据的时候,首先会对数据的长度和可写空间做个校验。如果不足,就会创建一个新的ByteBuf,并把之前的复制到新建的这个ByteBuf。最后释放老的ByteBuf。

下来,我们一起来追踪下源码

buffer.writeInt(1);
首先进入writeInt方法
@Override
public ByteBuf writeInt(int value) {
    ensureWritable(4);
    _setInt(writerIndex, value);
    writerIndex += 4;
    return this;
}

其中非常关键的一行 ensureWritable(4);netty就是通过这个方法达到扩容。我们继续往下追踪

public ByteBuf ensureWritable(int minWritableBytes) {
    if (minWritableBytes < 0) {
        throw new IllegalArgumentException(String.format(
                "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
    }

    if (minWritableBytes <= writableBytes()) {
        return this;
    }

    if (minWritableBytes > maxCapacity - writerIndex) {
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }

    // Normalize the current capacity to the power of 2.
    int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);

    // Adjust to the new capacity.
    capacity(newCapacity);
    return this;
}

代码中首先判断写的长度是否小于0,紧接着判断。当前缓存对象是否有足够的空间存放当前需要写入的最大长度。否则就计算下次需要生产的空间的大小。也就是

代码中的 caculateNewCapacity()方法。

接着 我们可继续看看它的计算空间算法

private int calculateNewCapacity(int minNewCapacity) {
    final int maxCapacity = this.maxCapacity;
    final int threshold = 1048576 * 4; // 4 MiB page

    if (minNewCapacity == threshold) {
        return threshold;
    }

    // If over threshold, do not double but just increase by threshold.
    if (minNewCapacity > threshold) {
        int newCapacity = minNewCapacity / threshold * threshold;
        if (newCapacity > maxCapacity - threshold) {
            newCapacity = maxCapacity;
        } else {
            newCapacity += threshold;
        }
        return newCapacity;
    }

    // Not over threshold. Double up to 4 MiB, starting from 64.
    int newCapacity = 64;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }

    return Math.min(newCapacity, maxCapacity);
}

首先判断当前传入的大小是否小于64,否则就返回64,如果大于64且小于threadshould 就每次增大2倍。否则就每次添加4m或者当新需要的空间大于最大空间减去4m时,就直接赋值最大的空间

有了新需要的容器大小,就可以准备扩容了。

public ByteBuf capacity(int newCapacity) {
    ensureAccessible();
    if (newCapacity < 0 || newCapacity > maxCapacity()) {
        throw new IllegalArgumentException("newCapacity: " + newCapacity);
    }

    int oldCapacity = array.length;
    if (newCapacity > oldCapacity) {
        byte[] newArray = new byte[newCapacity];
        System.arraycopy(array, 0, newArray, 0, array.length);
        setArray(newArray);
    } else if (newCapacity < oldCapacity) {
        byte[] newArray = new byte[newCapacity];
        int readerIndex = readerIndex();
        if (readerIndex < newCapacity) {
            int writerIndex = writerIndex();
            if (writerIndex > newCapacity) {
                writerIndex(writerIndex = newCapacity);
            }
            System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
        } else {
            setIndex(newCapacity, newCapacity);
        }
        setArray(newArray);
    }
    return this;
}

首先对引用和参数的校验。然后创建新的

byte[] newArray = new byte[newCapacity]

容器。接着赋值,更新索引。最后返回新的容器
到这里,缓存就成功的扩容了。

针对Java架构,我这边给大家准备了一些关于Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术的资料,希望能帮助一些技术遇到了瓶颈但是你又拒绝平庸,期待蜕变,想进入一线互联网公司或者给自己涨薪的程序大咖!

欢迎大家加入java技术交流群:722842263,免费分享Spring框架、Mybatis框架SpringBoot框架、SpringMVC框架、SpringCloud微服务、Dubbo框架、Redis缓存、RabbitMq消息、JVM调优、Tomcat容器、MySQL数据库教学视频及架构学习思维导图

猜你喜欢

转载自blog.csdn.net/weixin_43640104/article/details/90176321