Netty学习笔记六-ByteBuf学习

背景

Java自带的Nio ByteBuffer具有局限性和操作的复杂性,主要缺点如下:
1、ByteBuffer长度固定,一旦分配成功长度不能动态扩展和缩容,很容易发生越界异常。
2、ByteBuffer只有一个标识位置的指针,读写切换时需要手工调用flip方法
为了弥补这些不足,Netty作者重新造轮子,提供了自己实现的ByteBuf。

ByteBuf原理

ByteBuf也是通过字节数组byte[]作为缓冲区来存取数据,通过门面模式聚合了JDK NIO元素的ByteBuffer,进行封装.
与ByteBuffer不同的是,ByteBuf提供了两个指针来协助缓冲区的读写操作,读操作readerIndex,写操作使用writeIndex.
刚开始readerIndex和writeIndex都为0,随着数据的写入writeIndex会增加,读取数据时readerIndex会增加,但是它不会超过writeIndex的大小,在读取之后,0~readerIndex的就视作为discard的,调用discardBytes方法可以释放这部分空间。下图表示了ByteBuffer这三部分的关系:


image.png

基本功能

读操作

byte readByte() 从当前读索引读取一个字节并且读索引readerIndex加1,如果可读字节数小于1则抛出异常
boolean readBoolean() 从当前读索引读取一个布尔字节并且读索引readerIndex加1,如果可读字节数小于1则抛出异常
ByteBuf readBytes(int length) 从当前读索引readerIndex取指定长度字节数到另外有一个Buf容器并且读索引readerIndex加length,返回的buf读索引为0,写索引为length
ByteBuf readSlice(int length) 从当前读索引readerIndex取指定长度字节数到另外有一个Buf容器并且读索引readerIndex加length,返回的buf读索引为0,写索引为length

写操作

ByteBuf writeByte(int value) 从当前写索引writeIndex写入字节并且writeIndex增加
ByteBuf writeBytes(ByteBuf src) 将src拷贝到当前buf并且writeIndex增加
ByteBuf writeBytes(ByteBuf src, int length)将src拷贝length个字节到当前buf并且writeIndex增加
ByteBuf writeBytes(ByteBuf src, int srcIndex, int length)将src从srcindex开始拷贝length个字节到当前buf并且writeIndex增加
还有很多这里不再列出,大家可以去看API

丢弃字节(Discardable byte)

discardReadBytes会将readerindex之后的数据移动到从0开始,写索引减少readerindex个,也就是会丢弃已读的字节, 增加可写的字节空间
丢弃之前


image.png

丢弃之后


image.png

clear操作

clear操作不会清空缓冲区,而只是将writeindex和readindex置为0
clear操作前


image.png

clear操作后


image.png

源码分析

继承关系图:


image.png

AbstractByteBuf继承自ByteBuf,ByteBuf的公共属性和公共功能都在AbstractByteBuf上实现,比如读、写,清除等基本功能

AbstractReferenceCountedByteBuf继承自AbstractByteBuf,在AbstractByteBuf的基础上提供了一下功能:
refCnt:获得该对象的引用计数;
retain:增加该对象的引用计数(无参数:+1;有参数:+指定的increment)
release:减少该对象的引用计数(无参数:-1;有参数:-指定的increment),当引用计数减少到0时,释放该对象。返回值为true,当且仅当引用计数变为0和该对象已释放。

读操作源码分析
public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
        checkReadableBytes(length);
        getBytes(readerIndex, dst, dstIndex, length);
        readerIndex += length;
        return this;
    }

首先校验缓冲区的可用空间

 protected final void checkReadableBytes(int minimumReadableBytes) {
        ensureAccessible();
        if (minimumReadableBytes < 0) {
            throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
        }
        if (readerIndex > writerIndex - minimumReadableBytes) {
            throw new IndexOutOfBoundsException(String.format(
                    "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
                    readerIndex, minimumReadableBytes, writerIndex, this));
        }
    }

第一步判断如果参数小于0直接抛出异常,第二部如果写的字节数小于需要读取的长度也抛出异常
校验通过后调用geyBytes方法从buffer的readerindex位置读取length个字节到目标缓冲区,其中目标缓冲区从dstindex开始存储数据,由于子类的复制操作的技术实现不一样,所以具体实现由子类实现

/**
     * Transfers this buffer's data to the specified destination starting at
     * the specified absolute {@code index}.
     * This method does not modify {@code readerIndex} or {@code writerIndex}
     * of this buffer.
     *
     * @param dstIndex the first index of the destination
     * @param length   the number of bytes to transfer
     *
     * @throws IndexOutOfBoundsException
     *         if the specified {@code index} is less than {@code 0},
     *         if the specified {@code dstIndex} is less than {@code 0},
     *         if {@code index + length} is greater than
     *            {@code this.capacity}, or
     *         if {@code dstIndex + length} is greater than
     *            {@code dst.length}
     */
    public abstract ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length);
写操作源码分析
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
        ensureWritable(length);
        setBytes(writerIndex, src, srcIndex, length);
        writerIndex += length;
        return this;
    }

第一步判断是否可写

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;
    }

第一步判断参数合法性,第二步判断如果可写的字节数大于需要写入的字节数则直接通过,如果需要写入的字节数大于最大可扩容所容纳的字节数则直接抛异常
否则进行自动扩容,这就是bytebuf对比bytebuffer的好处。

 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);
    }

首先设置阀值为4M,如果需要的新容量正好等于阀值则使用阀值作为缓冲区容量,如果大于新容量大于阀值则每次以阀值为倍数进行扩容,扩张后的内存需要跟maxCapacity进行比较,不能大于最大的maxCapacity,如果新容量小于阀值,则每次以64字节为步长进行扩容
重新计算完新的需要扩容容量后需要重新创建缓冲区,将原缓冲区内容复制到新创建的ByteBuf中,由于不同子类会有不同的复制方法,所以该方法也是抽象方法

/**
     * Adjusts the capacity of this buffer.  If the {@code newCapacity} is less than the current
     * capacity, the content of this buffer is truncated.  If the {@code newCapacity} is greater
     * than the current capacity, the buffer is appended with unspecified data whose length is
     * {@code (newCapacity - currentCapacity)}.
     */
    public abstract ByteBuf capacity(int newCapacity);
UnpooledHeapByteBuf源码分析

UnpooledHeapByteBuf基于堆内存进行内存分配的缓冲区。没有基于对象池技术实现,也就意味着每次IO操作都要创建一个新的UnpooledHeapByteBuf,频繁的进行大款的堆内存分配和回收对性能有一定影响,并且可能会有Full GC风险。
成员变量

private final ByteBufAllocator alloc;
 private byte[] array;
 private ByteBuffer tmpNioBuf;

ByteBufAllocator用于内存的分配,byte[]作为字节缓冲区,tmpNioBuf用于Netty ByteBuf到JDK ByteBuffer的转换用
动态扩展缓冲区实现:
上面提到ByteBuf写操作时会有自动扩容原理,而具体是由子类实现,那么UnpooledHeapByteBuf的源码如下:

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;
    }

首先对入参新的容量参数newCapacity进行合法校验,校验通过后判断如果新的容量大于原来的缓冲区容量,则进行动态扩容,通过byte[] newArray = new byte[newCapacity]创建新的缓冲区,Syste.arraycopy将原来缓冲区的数据复制到新的缓冲区,再通过setArray将缓冲区替换就的缓冲区。
如果新的容量小于原来缓冲区容量,则首先判断读索引是否小于新的容量newCapacity,如果小于在判断写索引是否大于newCapacity,如果读索引小于newCapacity且写索引大于newCapacity,也就意味着原来的字节缓冲区还有(newCapacity-readerIndex)个字节没有被读取,那么需要将这些数据拷贝到新的缓冲区。如果读索引大于新的容量newCapacity,那意味着原来缓冲区所有字节都被读取了,那么直接将byte[] newArray = new byte[newCapacity]更新为新的字节缓冲区。

你可能感兴趣的:(Netty学习笔记六-ByteBuf学习)