Netty源码_ByteBuf详解

在分析 Netty 框架的时候,我们首先介绍 ByteBuf 相关类,因为网络传输最终都是字节数据,所以如何管理字节数据是非常重要的。
我们知道在Java NIO 编程中提供了 ByteBuffer 作为字节容器,但是这个类使用起来过于复杂,而且极其容易犯错,因为它只有一个索引记录数据位置,导致读写操作还需要使用 flip 方法进行切换,并且容量是固定的,需要使用者手动扩容。
因此 Netty 提供了 ByteBuf 类来弥补 ByteBuffer 的不足和缺陷。

一. 类型

1.1 内存分配

ByteBuf 按照内存分配划分,可以分为两种:

  1. 堆缓冲区: 即内部使用 byte[] 字节数组存储字节数据。
    • 它的优点是字节数组可以快速的分配和释放,也可以被 JVM 自动回收;
    • 缺点是进行 I/O 操作时,需要进行额外一次内存复制,将堆缓冲区数据复制到内核中。
    • 堆缓存区的 ByteBuf 类名中都包含 Heap
  2. 直接缓冲区: 即内部使用 Java NIODirectByteBuffer 来存储字节数据。
    • 它的优点是避免在每次调用 I/O 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
    • 缺点是内存分配和释放都较为昂贵。
    • 直接缓冲区的 ByteBuf 类名中都包含 Direct

1.2 内存回收

ByteBuf 按照内存回收角度划分,可以分为两种:

  1. 池中缓存区:维护一个内存池,所有的缓存区 ByteBuf 的内存都来自于这个内存池,可以重复使用内存,提高内存使用效率,降低高负载下频繁的 GC

    注意不管是堆缓冲区还是直接缓冲区都可以使用内存池。池中缓存区的 ByteBuf 类名中都包含 Pooled

  2. 非池中缓存区:不使用内存池,直接创建内存;对于堆缓冲区,就是直接创建 byte[] 字节数组,对于直接缓冲区,就是创建 DirectByteBuffer对象实例。

    注意不管是堆缓冲区还是直接缓冲区都可以是非池中缓存区。非池中缓存区的 ByteBuf 类名中都包含 Unpooled

1.3 Unsafe

最后 JDK 中是否包含 sun.misc.Unsafe 类,缓存区 ByteBuf 又分为安全的和非安全的。使用 sun.misc.Unsafe 可以加快数据的访问速度。

1.4 小结

因此这三种类型进行排列组合,因此重要的缓存区 ByteBuf一共分为八种类型:

  1. UnpooledHeapByteBuf
  2. UnpooledUnsafeHeapByteBuf
  3. UnpooledDirectByteBuf
  4. UnpooledUnsafeDirectByteBuf
  5. PooledHeapByteBuf
  6. PooledUnsafeHeapByteBuf
  7. PooledDirectByteBuf
  8. PooledUnsafeDirectByteBuf

这些类我们在后面的文章会一一介绍,但是这些类型只是缓存区 ByteBuf 的内存不同,但是对于一般使用者来说,其实不用区分这么多,我们只需要知道如何使用缓存区 ByteBuf 就可以了。

二. 介绍

对于如何使用 ByteBuf ,我们直接看 ByteBuf类的文档注释

2.1 文档注释

A random and sequential accessible sequence of zero or more bytes (octets). This interface provides an abstract view for one or more primitive byte arrays (byte[]) and NIO buffers.
Creation of a buffer
It is recommended to create a new buffer using the helper methods in Unpooled rather than calling an individual implementation's constructor.
Random Access Indexing
Just like an ordinary primitive byte array, ByteBuf uses zero-based indexing . It means the index of the first byte is always 0 and the index of the last byte is always capacity - 1. For example, to iterate all bytes of a buffer, you can do the following, regardless of its internal implementation:
   ByteBuf buffer = ...;
   for (int i = 0; i < buffer.capacity(); i ++) {
       byte b = buffer.getByte(i);
       System.out.println((char) b);
   }
   
Sequential Access Indexing
ByteBuf provides two pointer variables to support sequential read and write operations - readerIndex for a read operation and writerIndex for a write operation respectively. The following diagram shows how a buffer is segmented into three areas by the two pointers:
        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        |                   |     (CONTENT)    |                  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity
   
Readable bytes (the actual content)
This segment is where the actual data is stored. Any operation whose name starts with read or skip will get or skip the data at the current readerIndex and increase it by the number of read bytes. If the argument of the read operation is also a ByteBuf and no destination index is specified, the specified buffer's writerIndex is increased together.
If there's not enough content left, IndexOutOfBoundsException is raised. The default value of newly allocated, wrapped or copied buffer's readerIndex is 0.
   // Iterates the readable bytes of a buffer.
   ByteBuf buffer = ...;
   while (buffer.isReadable()) {
       System.out.println(buffer.readByte());
   }
   
Writable bytes
This segment is a undefined space which needs to be filled. Any operation whose name starts with write will write the data at the current writerIndex and increase it by the number of written bytes. If the argument of the write operation is also a ByteBuf, and no source index is specified, the specified buffer's readerIndex is increased together.
If there's not enough writable bytes left, IndexOutOfBoundsException is raised. The default value of newly allocated buffer's writerIndex is 0. The default value of wrapped or copied buffer's writerIndex is the capacity of the buffer.
   // Fills the writable bytes of a buffer with random integers.
   ByteBuf buffer = ...;
   while (buffer.maxWritableBytes() >= 4) {
       buffer.writeInt(random.nextInt());
   }
Discardable bytes
This segment contains the bytes which were read already by a read operation. Initially, the size of this segment is 0, but its size increases up to the writerIndex as read operations are executed. The read bytes can be discarded by calling discardReadBytes() to reclaim unused area as depicted by the following diagram:
    BEFORE discardReadBytes()
  
        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity
  
  
    AFTER discardReadBytes()
  
        +------------------+--------------------------------------+
        |  readable bytes  |    writable bytes (got more space)   |
        +------------------+--------------------------------------+
        |                  |                                      |
   readerIndex (0) <= writerIndex (decreased)        <=        capacity
   
Please note that there is no guarantee about the content of writable bytes after calling discardReadBytes(). The writable bytes will not be moved in most cases and could even be filled with completely different data depending on the underlying buffer implementation.
Clearing the buffer indexes
You can set both readerIndex and writerIndex to 0 by calling clear(). It does not clear the buffer content (e.g. filling with 0) but just clears the two pointers. Please also note that the semantic of this operation is different from ByteBuffer.clear().
 BEFORE clear()
  
        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity
  
  
    AFTER clear()
  
        +---------------------------------------------------------+
        |             writable bytes (got more space)             |
        +---------------------------------------------------------+
        |                                                         |
        0 = readerIndex = writerIndex            <=            capacity
   
Search operations
For simple single-byte searches, use indexOf(int, int, byte) and bytesBefore(int, int, byte). bytesBefore(byte) is especially useful when you deal with a NUL-terminated string. For complicated searches, use forEachByte(int, int, ByteProcessor) with a ByteProcessor implementation.
Mark and reset
There are two marker indexes in every buffer. One is for storing readerIndex and the other is for storing writerIndex. You can always reposition one of the two indexes by calling a reset method. It works in a similar fashion to the mark and reset methods in InputStream except that there's no readlimit.
Derived buffers
You can create a view of an existing buffer by calling one of the following methods:
· duplicate()
· slice()
· slice(int, int)
· readSlice(int)
· retainedDuplicate()
· retainedSlice()
· retainedSlice(int, int)
· readRetainedSlice(int)
A derived buffer will have an independent readerIndex, writerIndex and marker indexes, while it shares other internal data representation, just like a NIO buffer does.
In case a completely fresh copy of an existing buffer is required, please call copy() method instead.
Non-retained and retained derived buffers
Note that the duplicate(), slice(), slice(int, int) and readSlice(int) does NOT call retain() on the returned derived buffer, and thus its reference count will NOT be increased. If you need to create a derived buffer with increased reference count, consider using retainedDuplicate(), retainedSlice(), retainedSlice(int, int) and readRetainedSlice(int) which may return a buffer implementation that produces less garbage.
Conversion to existing JDK types
Byte array
If a ByteBuf is backed by a byte array (i.e. byte[]), you can access it directly via the array() method. To determine if a buffer is backed by a byte array, hasArray() should be used.
NIO Buffers
If a ByteBuf can be converted into an NIO ByteBuffer which shares its content (i.e. view buffer), you can get it via the nioBuffer() method. To determine if a buffer can be converted into an NIO buffer, use nioBufferCount().
Strings
Various toString(Charset) methods convert a ByteBuf into a String. Please note that toString() is not a conversion method.

这个文档注释很好地说明缓存区 ByteBuf 作用和特性。

2.2 缓存区 ByteBuf 创建

创建缓存区 ByteBuf 的时候,推荐使用 Unpooled 类的工具方法,而不是直接调用它的构造方法 new 出来。当然也可以通过 ByteBufAllocator 实例创建缓存区 ByteBuf对象。

2.3 随机访问索引

如同在普通的 Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是 capacity() - 1。因此你可以直接遍历缓存区 ByteBuf 数据,而不用管它具体是那种实现。

   ByteBuf buffer = ...;
   for (int i = 0; i < buffer.capacity(); i ++) {
       byte b = buffer.getByte(i);
       System.out.println((char) b);
   }

2.4 顺序访问索引

缓存区 ByteBuf 提供了两个指针索引来支持顺序读写操作: readerIndex 用于读操作,writerIndex 用于写操作,因此这两个索引将缓存区分成三个区域:

        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        |                   |     (CONTENT)    |                  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity

2.4.1 Readable bytes (真实数据)

这个片段是存储实际数据的地方。任何名称以 readskip 开头的操作将获得或跳过当前 readerIndex 处的数据,并在 readerIndex 上增加读字节数。新分配、包装或复制的缓冲区的 readerIndex 的默认值是 0

  • 如果读操作的参数也是一个 ByteBuf 并且没有指定目标索引,那么这个参数缓冲区 的writerIndex 将一起增加。例如 readBytes(ByteBuf dst)
  • 如果缓存区没有足够的可读内容,将引发IndexOutOfBoundsException
// Iterates the readable bytes of a buffer.
ByteBuf buffer = ...;
while (buffer.isReadable()) {
      System.out.println(buffer.readByte());
}

2.4.2 Writable bytes

这个片段是一个未定义的空间,需要被填充。任何名称以 write 开头的操作都将在当前 writerIndex 处的写入数据,并在 writerIndex 上增加写入的字节数。新分配的缓冲区的 writerIndex 的默认值是 0。包装或复制缓冲区的 writerIndex 的默认值是缓冲区的容量值。

  • 如果写操作的参数也是一个 ByteBuf,并且没有指定源索引,则指定参数缓冲区的 readerIndex 将一起增加。例如 writeBytes(ByteBuf src)
  • 如果没有足够的可写字节,则会引发IndexOutOfBoundsException,即超过缓存区最大容量。
// Fills the writable bytes of a buffer with random integers.
ByteBuf buffer = ...;
while (buffer.maxWritableBytes() >= 4) {
      buffer.writeInt(random.nextInt());
}

2.4.3 Discardable bytes

这个片段包含已经被读操作读取的字节。最初这个段的大小是0,但是随着读取操作的执行,它的大小增加到writerIndex。可以通过调用 discardReadBytes() 来回收这个片段。
调用 discardReadBytes() 之前:

        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity

调用 discardReadBytes() 之后:

        +------------------+--------------------------------------+
        |  readable bytes  |    writable bytes (got more space)   |
        +------------------+--------------------------------------+
        |                  |                                      |
   readerIndex (0) <= writerIndex (decreased)        <=        capacity
  • 请注意,在调用discardReadBytes()后,不能保证可写字节的内容。在大多数情况下,可写字节不会被移动,甚至可以由完全不同的数据填充,这取决于底层缓冲区的实现。
  • 虽然你可能会倾向于频繁地调用 discardReadBytes() 方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节(图中标记为 readable bytes 的部分)必须被移动到缓冲区的开始位置。我们建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。

2.4.4 Clearing the buffer indexes

可以通过调用 clear()readerIndexwriterIndex 都设置为0。它不清除缓冲区内容(例如用0填充),只是清除两个指针。还请注意,此操作的语义与ByteBuffer.clear()不同。

调用 clear() 之前:

        +-------------------+------------------+------------------+
        | discardable bytes |  readable bytes  |  writable bytes  |
        +-------------------+------------------+------------------+
        |                   |                  |                  |
        0      <=      readerIndex   <=   writerIndex    <=    capacity

调用 clear() 之后:

        +---------------------------------------------------------+
        |             writable bytes (got more space)             |
        +---------------------------------------------------------+
        |                                                         |
        0 = readerIndex = writerIndex            <=            capacity

缓存区 ByteBuf 中的数据没有变,只不过索引 readerIndexwriterIndex 变成 0

2.5 搜索操作

缓存区 ByteBuf 提供从缓存区中搜索指定字节数据位置索引的方法。
对于简单的单字节搜索,使用 indexOf(int, int, byte)bytesBefore(int, int, byte)bytesBefore(byte) 在处理以 null 结尾的字符串时特别有用。
对于复杂的搜索,使用带有 ByteProcessor 实现的 forEachByte(int, int, ByteProcessor) 方法。

2.6 标记和重置

每个缓冲区中都有两个标记索引。
一个用于存储读索引 readerIndex ,另一个用于存储写索引 writerIndex
您总是可以通过调用 reset 方法来重新定位这两个索引中的一个。它的工作方式类似于 InputStream 中的标记和重置方法,只是没有读取限制。

2.7 派生的缓冲区

派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:

  • duplicate()
  • slice()
  • slice(int, int)
  • readSlice(int)
  • retainedDuplicate()
  • retainedSlice()
  • retainedSlice(int, int)
  • readRetainedSlice(int)

派生缓冲区将有一个独立的 readerIndexwriterIndex 和标记索引,而它与 NIO 缓冲区一样共享其他缓存区内部数据。

因此如果改变了共享缓存区内部数据,那么派生缓冲区内容也会跟着改变。所以如果你需要一个现有缓冲区的全新副本,请调用copy()方法。

非保留和保留的派生缓冲区

  • 注意: duplicate(), slice(), slice(int, int)和readSlice(int)不会对返回的派生缓冲区调用retain(),因此它的引用计数不会增加。
  • 如果你需要创建一个增加引用计数的派生缓冲区,考虑使用retainedDuplicate(), retainedSlice(), retainedSlice(int, int)和readRetainedSlice(int),这可能会返回一个产生更少垃圾的缓冲区实现。

2.8 转换到现有的 JDK 类型

2.8.1 字节数组

如果缓存区 ByteBuf 使用字节数组(即 byte[] ) 储存数据的,你可以通过 array() 方法直接访问它。

要确定缓冲区是否由字节数组支持,应该使用hasArray() 方法。

2.8.2 NIO 缓冲区

如果一个缓存区 ByteBuf 可以转换成一个 NIO ByteBuffer,共享它的内容(即视图缓冲区),你可以通过 nioBuffer() 方法获得它。

要确定缓冲区是否可以转换为 NIO 缓冲区,请使用 nioBufferCount() 方法。

2.8.3 字符串

各种toString(Charset)方法将一个 ByteBuf转换为一个 String

请注意,toString()不是一个转换方法, 它显示当前缓存区的变量信息的,而不是缓存区存储内容的文本表示。

2.8.4 I/O

通过 ByteBufInputStreamByteBufOutputStream 实现。

三. 方法

接下来我们分析 ByteBuf 的方法,弄懂了这些方法的作用,就可以不用管 ByteBuf 具体实现是什么,直接操作缓存区 ByteBuf

3.1 get 系列方法

从指定索引位置读取数据,它有几个特点:

  • 使用 get 系列方法必须指定一个索引,也就是说它可以从任意位置读取缓存区中的数据。只要读取的数据不超出缓存区的范围。
  • 它不会改变读索引 readerIndex 和 写索引 writerIndex 的值。

3.1.1 获取基本数据类型的方法

  1. boolean getBoolean(int index): 获取该缓冲区中指定绝对索引处的布尔值。
  2. byte getByte(int index): 获取此缓冲区中位于指定绝对索引处的字节。
  3. short getUnsignedByte(int index): 获取该缓冲区中指定绝对索引处的无符号字节。注:返回值是 short 类型。
  4. short getShort(int index): 获取此缓冲区中指定绝对索引处的16位短整数。
  5. short getShortLE(int index): 获取该缓冲区中以小端字节顺序指定的绝对索引处的16位短整数。
  6. int getUnsignedShort(int index): 获取此缓冲区中指定绝对索引处的16位无符号短整数。注:返回值是 int 类型。
  7. int getUnsignedShortLE(int index): 获取该缓冲区中以小端字节顺序指定的绝对索引处的无符号16位短整数。注:返回值是 int 类型。
  8. int getMedium(int index): 获取此缓冲区中指定绝对索引处的24位中整数。
  9. int getMediumLE(int index): 以小端字节顺序在此缓冲区中指定的绝对索引处获取一个24位中等整数。
  10. int getUnsignedMedium(int index): 获取此缓冲区中指定绝对索引处的无符号24位中等整数。
  11. int getUnsignedMediumLE(int index): 获取该缓冲区中以小端字节顺序指定的绝对索引处的无符号24位中等整数。
  12. int getInt(int index): 获取此缓冲区中指定绝对索引处的32位整数。
  13. int getIntLE(int index): 获取此缓冲区中具有小端字节顺序的指定绝对索引处的32位整数。
  14. long getUnsignedInt(int index): 获取此缓冲区中指定绝对索引处的无符号32位整数。注:返回值是 long 类型。
  15. long getUnsignedIntLE(int index): 获取此缓冲区中以小端字节顺序指定绝对索引处的无符号32位整数。
  16. long getLong(int index): 获取此缓冲区中指定绝对索引处的64位长整数。
  17. long getLongLE(int index): 以小端字节顺序在此缓冲区中指定的绝对索引处获取一个64位长整数。
  18. char getChar(int index): 获取此缓冲区中指定绝对索引处的2字节UTF-16字符。
  19. float getFloat(int index): 获取此缓冲区中指定绝对索引处的32位浮点数。
  20. float getFloatLE(int index): 获取该缓冲区中以小端字节顺序指定的绝对索引处的32位浮点数。
  21. double getDouble(int index): 获取此缓冲区中指定绝对索引处的64位浮点数。
  22. double getDoubleLE(int index): 获取该缓冲区中以小端字节顺序指定的绝对索引处的64位浮点数。

22 个方法让使用者可以很轻松从缓存区中指定位置获取想要基本类型数据。

3.1.2 和其他ByteBuf 交互

get 系列方法都是从该缓存区读取数据,与其他的缓存区 ByteBuf 交互,就是将该缓存区传输到其他缓存区中。
一共有三个方法:

  1. ByteBuf getBytes(int index, ByteBuf dst)
  2. ByteBuf getBytes(int index, ByteBuf dst, int length)
  3. ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length)

这三个方法都是从指定的绝对索引开始,将该缓冲区的数据传输到指定的目标缓存区dst
我们来看在 AbstractByteBuf 类中的基本实现:

    @Override
    public ByteBuf getBytes(int index, ByteBuf dst) {
        getBytes(index, dst, dst.writableBytes());
        return this;
    }

    @Override
    public ByteBuf getBytes(int index, ByteBuf dst, int length) {
        getBytes(index, dst, dst.writerIndex(), length);
        dst.writerIndex(dst.writerIndex() + length);
        return this;
    }

我们看到:

  • ByteBuf getBytes(int index, ByteBuf dst) 方法通过直接调用 getBytes(int index, ByteBuf dst, int length) 方法来实现,传递 length 大小就是目标缓存区 dst 的剩下可写区域 Writable bytes 的大小。
  • getBytes(int index, ByteBuf dst, int length) 方法也调用了 ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) 方法,传递的dstIndex 就是目标缓存区 dst 当前写索引 writerIndex 值。
    但是最后它调用 dst.writerIndex() 方法,增加了目标缓存区 dst 的写索引 writerIndex 值。

因此我们可以得出:

  • 首先这三个方法都不会改变该缓冲区的读索引readerIndex或者写索引writerIndex的值。
  • 前两个方法会改变目标缓存区 dst 的写索引 writerIndex 值,而第三个方法则不会。
    • 这是因为前两个方法是将该缓冲区的数据从目标缓存区 dst 当前写索引 writerIndex 位置处开始写入,因此传输完之后,可以更改目标缓存区 dst 的写索引 writerIndex
    • 而第三个方法则是从目标缓存区 dst 任意位置处开始写入,就不好直接改变写索引 writerIndex了。

3.1.3 和字节数组交互

get 系列方法都是从该缓存区读取数据,与字节数组交互,就是将缓存区读取到字节数组中。

  1. ByteBuf getBytes(int index, byte[] dst)
  2. ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length)

从指定的绝对索引开始,将该缓冲区的数据传输到指定目标字节数组dst
第一个方法实现就是

    public ByteBuf getBytes(int index, byte[] dst) {
        getBytes(index, dst, 0, dst.length);
        return this;
    }

3.1.4 和 ByteBuffer 交互

将该缓存区数据读取到 ByteBuffer 中去。
只有一个方法: ByteBuf getBytes(int index, ByteBuffer dst)
即从指定的绝对索引开始将缓冲区的数据传输到指定的目标 ByteBuffer,直到目标 ByteBuffer 数据达到其限制,即ByteBuffer 已经被写满了。

3.1.5 和 IO 流交互

  1. ByteBuf getBytes(int index, OutputStream out, int length): 从指定的绝对索引处开始将该缓冲区的数据传输到指定的流。
  2. int getBytes(int index, GatheringByteChannel out, int length): 从指定的绝对索引处开始将缓冲区的数据传输到指定的通道。
  3. int getBytes(int index, FileChannel out, long position, int length): 将从指定的绝对索引处开始的缓冲区数据传输到从给定文件位置开始的指定通道。

3.1.5 获取 CharSequence 对象

CharSequence getCharSequence(int index, int length, Charset charset) 获取在给定索引处具有给定长度的 CharSequence

3.2 set 系列方法

从指定索引位置设置数据,它有几个特点:

  • 使用 set 系列方法必须指定一个索引,也就是说它可以从任意位置设置缓存区中的数据。只要设置的数据不超出缓存区的范围。
  • 它不会改变读索引 readerIndex 和 写索引 writerIndex 的值。

3.2.1 设置基本数据类型的方法

  1. ByteBuf setBoolean(int index, boolean value): 在此缓冲区的指定绝对索引处设置指定的布尔值。
  2. ByteBuf setByte(int index, int value): 在此缓冲区的指定绝对索引处设置指定的字节。指定值的24个高阶位被忽略。
    3.ByteBuf setShort(int index, int value): 在此缓冲区的指定绝对索引处设置指定的16位短整数。忽略指定值的16个高阶位。
  3. ByteBuf setShortLE(int index, int value): 在此缓冲区中使用小端字节顺序的指定绝对索引处设置指定的16位短整数。忽略指定值的16个高阶位。
  4. ByteBuf setMedium(int index, int value): 在此缓冲区的指定绝对索引处设置指定的24位中等整数。忽略指定值的8个高阶位。
  5. ByteBuf setMediumLE(int index, int value): 以小端字节顺序在此缓冲区的指定绝对索引处设置指定的24位中位数。忽略指定值的8个高阶位。
  6. ByteBuf setInt(int index, int value): 在此缓冲区的指定绝对索引处设置指定的32位整数。
  7. ByteBuf setIntLE(int index, int value): 以小端字节顺序在此缓冲区的指定绝对索引处设置指定的32位整数。
  8. ByteBuf setLong(int index, long value): 在此缓冲区的指定绝对索引处设置指定的64位长整数。
  9. ByteBuf setLongLE(int index, long value): 以小端字节顺序在此缓冲区的指定绝对索引处设置指定的64位长整数。
  10. ByteBuf setChar(int index, int value): 在此缓冲区的指定绝对索引处设置指定的2字节UTF-16字符。
  11. ByteBuf setFloat(int index, float value): 在此缓冲区中指定的绝对索引处设置指定的32位浮点数。
  12. setFloatLE(int index, float value): 以小端字节顺序在此缓冲区的指定绝对索引处设置指定的32位浮点数。
  13. ByteBuf setDouble(int index, double value): 在此缓冲区中指定的绝对索引处设置指定的64位浮点数。
    15 ByteBuf setDoubleLE(int index, double value): 以小端字节顺序在此缓冲区的指定绝对索引处设置指定的64位浮点数。

15 个方法让使用者可以很轻松在缓存区中指定位置设置各种基本类型数据。set 系列方法比 get 少是因为没有 setUnsigned... 的方法。

3.2.2 和其他 ByteBuf 交互

set 系列方法都是向本缓存区设置数据,因此与其他的缓存区 ByteBuf 交互,就是将其他缓存区的数据写入到本缓存区。
一共有三个方法:

  1. ByteBuf setBytes(int index, ByteBuf src)
  2. ByteBuf setBytes(int index, ByteBuf src, int length)
  3. ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length)

这三个方法都是从指定的绝对索引处开始将指定源缓冲区 src的数据传输到此缓冲区。
我们来看在 AbstractByteBuf 类中的基本实现:

    @Override
    public ByteBuf setBytes(int index, ByteBuf src) {
        setBytes(index, src, src.readableBytes());
        return this;
    }
    @Override
    public ByteBuf setBytes(int index, ByteBuf src, int length) {
        checkIndex(index, length);
        ObjectUtil.checkNotNull(src, "src");
        if (checkBounds) {
            checkReadableBounds(src, length);
        }

        setBytes(index, src, src.readerIndex(), length);
        src.readerIndex(src.readerIndex() + length);
        return this;
    }

我们可以看到和 get 系列方法几乎差不多:

  • ByteBuf setBytes(int index, ByteBuf src) 方法通过直接调用 ByteBuf setBytes(int index, ByteBuf src, int length) 方法来实现,传递 length 大小就是源缓存区src 可读区域 Readable Bytes 的大小。
  • ByteBuf setBytes(int index, ByteBuf src, int length) 方法也调用了 ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) 方法,传递的 srcIndex 就是源缓存区src 的当前读索引 readerIndex 的值。
    但是最后它调用了 src.readerIndex() 方法,增加了源缓存区src 的读索引 readerIndex 的值。

因此我们可以得出:

  • 首先这三个方法都不会改变本缓冲区的读索引 readerIndex 或者写索引 writerIndex 的值。
  • 前两个方法会改变源缓存区src 的读索引 readerIndex 的值,而第三个方法则不会。

3.2.3 和字节数组交互

将字节数组中的数据写入到本缓存区。

  1. ByteBuf setBytes(int index, byte[] src)
  2. ByteBuf setBytes(int index, byte[] src, int srcIndex, int length)
    从指定的绝对索引处开始将指定源字节数组的数据传输到此缓冲区。
    @Override
    public ByteBuf setBytes(int index, byte[] src) {
        setBytes(index, src, 0, src.length);
        return this;
    }

3.2.4 和 ByteBuffer 交互

  1. ByteBuf setBytes(int index, ByteBuffer src)
    从指定的绝对索引处开始将指定源缓冲区 src 的数据传输到此缓冲区,直到源缓冲区src的位置达到其极限,即源缓冲区src数据被读取完。

3.2.5 和 IO 流交互

  1. int setBytes(int index, InputStream in, int length) throws IOException : 从指定的绝对索引处开始将指定源流的内容传输到此缓冲区。

    返回从指定通道读入的实际字节数。如果是 -1 则指定的通道被关闭。

  2. int setBytes(int index, ScatteringByteChannel in, int length) throws IOException: 从指定的绝对索引开始,将指定源通道的内容传输到此缓冲区。

    返回从指定通道读入的实际字节数。如果是 -1 则指定的通道被关闭。

  3. int setBytes(int index, FileChannel in, long position, int length) throws IOException: 将从给定文件位置开始的指定源通道的内容传输到从指定绝对索引开始的缓冲区。

    返回从指定通道读入的实际字节数。如果是 -1 则指定的通道被关闭。

3.2.5 设置 CharSequence 对象

int setCharSequence(int index, CharSequence sequence, Charset charset): 在给定索引处设置 sequence 对象数据,并返回设置的字节长度。

3.2.6 设置 NUL 的方法

ByteBuf setZero(int index, int length) :从指定的绝对索引开始,用NUL (0x00)填充此缓冲区 length 长度的数据。

3.3 read 系列方法

read 系列方法与 get 系列方法一模一样,作用也是一样的,区别如下:

  • read 系列方法不用指定索引,只能从缓存区当前读索引 readerIndex 位置读取数据。
  • read 系列方法读取完数据之后,都会改变当前读索引 readerIndex 的值。

还有一个 ByteBuf skipBytes(int length) 方法,将该缓冲区中的当前readerIndex 增加指定的长度。

注意 read 系列 和 skipBytes 方法能改变读索引 readerIndex 的值。
除了它们,只剩下 readerIndex(int readerIndex), setIndex(int readerIndex, int writerIndex), clear() ,resetReaderIndex(), discardReadBytes(), discardSomeReadBytes()方法能改变读索引。

3.4 write 系列方法

write 系列方法与 set 系列方法一模一样,作用也是一样的,区别如下:

  • write 系列方法不用指定索引,只能从缓存区当前写索引 writerIndex 位置写入数据。
  • write 系列方法写入数据之后,都会改变当前写索引 writerIndex 的值。

注意 write 系列方法能改变写索引 writerIndex 的值。
除了它们,只剩下 writerIndex(int writerIndex), setIndex(int readerIndex, int writerIndex), clear() , resetWriterIndex(), discardReadBytes(), discardSomeReadBytes()方法能改变写索引 writerIndex 的值。

3.5 搜索

3.5.1 简单的单字节搜索

  1. int indexOf(int fromIndex, int toIndex, byte value): 定位指定字节值value在此缓冲区中的第一次出现的位置。如果找不到就返回 -1

    • 搜索从指定的fromIndex(inclusive)到指定的toIndex(exclusive)。
    • 如果fromIndex大于toIndex,搜索将按照从fromIndex(exclusive)到toIndex (inclusive)的相反顺序执行。
    • 请注意,较低的索引总是被包含,较高的索引总是被排除。
  2. int bytesBefore(byte value): 定位指定值在此缓冲区中的第一次出现。搜索从当前的读索引readerIndex(inclusive)到当前的写索引writerIndex(exclusive)。如果找不到就返回 -1

  3. int bytesBefore(int length, byte value): 定位指定值在此缓冲区中的第一次出现。搜索从当前readerIndex(inclusive)开始,并持续指定的长度。如果找不到就返回 -1

  4. int bytesBefore(int index, int length, byte value): 定位指定值在此缓冲区中的第一次出现。搜索从指定的索引index(inclusive)开始,并持续指定的长度。如果找不到就返回 -1

3.5.2 复杂搜索

  1. int forEachByte(ByteProcessor processor): 用指定的处理器按升序遍历该缓冲区的可读字节。
    我们来看 forEachByte 方法的实现:
        public int forEachByte(ByteProcessor processor) {
         ensureAccessible();
         try {
             return forEachByteAsc0(readerIndex, writerIndex, processor);
         } catch (Exception e) {
             PlatformDependent.throwException(e);
             return -1;
         }
      }
          int forEachByteAsc0(int start, int end, ByteProcessor processor) throws Exception {
         for (; start < end; ++start) {
             if (!processor.process(_getByte(start))) {
                 return start;
             }
         }
    
         return -1;
     }
    
    你会发现它的确按照升序遍历该缓冲区的可读字节,当 processorprocess 返回 false 即不通过的时候,这个字节就是我们需要找的字节,返回这个字节的索引位置。如果遍历完可读字节, processorprocess 都返回 true ,表示找不到,那么返回 -1
  2. int forEachByte(int index, int length, ByteProcessor processor): 用指定的处理器按升序遍历该缓冲区的指定区域(即 index(index + 1), ..(index + length - 1))。
  3. int forEachByteDesc(ByteProcessor processor): 使用指定的处理器按降序遍历该缓冲区的可读字节。
  4. int forEachByteDesc(int index, int length, ByteProcessor processor): 使用指定的处理器按降序遍历该缓冲区的指定区域。(即(index + length - 1), (index + length - 2),…index)。

3.6 复制缓存区

  1. ByteBuf copy(): 返回该缓冲区的可读字节的副本。修改返回的缓冲区或该缓冲区的内容根本不会影响彼此。这个方法与buf.copy(buf.readerIndex(), buf.readableBytes()) 相同。
  2. ByteBuf copy(int index, int length): 返回该缓冲区的子区域的副本。修改返回的缓冲区或该缓冲区的内容根本不会影响彼此。

copy 创建新的内存来存储源缓存区的内容,因此它们的内容不会影响彼此。

3.7 派生的缓冲区

  1. ByteBuf slice(): 返回该缓冲区可读字节的一个切片。

    修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。这个方法与 buf.slice(buf.readerIndex(), buf.readableBytes()) 相同。

  2. ByteBuf retainedSlice(): 返回该缓冲区可读字节的保留片。
    • 修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。这个方法与buf.slice(buf.readerIndex(), buf.readableBytes())相同。
    • slice()不同,此方法返回一个保留的缓冲区。这个方法的行为类似于slice().retain(),除了这个方法可能返回产生更少垃圾的缓冲区实现。
  3. ByteBuf slice(int index, int length): 返回这个缓冲区的子区域的一个切片。
    • 修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。
    • 这个方法不会调用retain(),因此引用计数不会增加。
  4. ByteBuf retainedSlice(int index, int length): 返回该缓冲区子区域的保留片。
    • 修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。
    • slice(int, int)不同,此方法返回一个保留的缓冲区。这个方法的行为类似于slice(…).retain(),除了这个方法可能返回产生更少垃圾的缓冲区实现。
  5. ByteBuf readSlice(int length): 返回该缓冲区子区域从当前readerIndex处开始的一个新切片,并将readerIndex增加新切片的大小(length)。这个方法不会调用retain(),因此引用计数不会增加。
    @Override
    public ByteBuf readSlice(int length) {
        checkReadableBytes(length);
        ByteBuf slice = slice(readerIndex, length);
        readerIndex += length;
        return slice;
    }
    
  6. ByteBuf readRetainedSlice(int length): 返回该缓冲区子区域从当前readerIndex处开始的一个新的保留片,并增加readerIndex的大小(=length)。这个方法的行为类似于readSlice(…).retain(),除了这个方法可能返回一个产生更少垃圾的缓冲区实现。
    @Override
    public ByteBuf readRetainedSlice(int length) {
        checkReadableBytes(length);
        ByteBuf slice = retainedSlice(readerIndex, length);
        readerIndex += length;
        return slice;
    }
    
  7. ByteBuf duplicate(): 返回一个共享该缓冲区的整个区域的缓冲区。
    • 修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。读索引标记 markedReaderIndex和写索引标记markedWriterIndex不会被复制。
    • 这个方法不会调用retain(),因此引用计数不会增加。
  8. ByteBuf retainedDuplicate(): 返回一个保留的缓冲区,该缓冲区共享该缓冲区的整个区域。
    • 修改返回缓冲区或此缓冲区的内容会影响彼此的内容,同时它们维护单独的索引和标记。读索引标记 markedReaderIndex和写索引标记markedWriterIndex不会被复制。
    • 这个方法的行为类似于duplicate().retain(),除了这个方法可能返回产生更少垃圾的缓冲区实现。

3.8 转换成 ByteBuffer

将此缓存区转换成 Java NIO 的缓存区 ByteBuffer 对象。

  1. int nioBufferCount(): 返回由该缓冲区组成的NIO ByteBuffers的最大数目。

    • 注意 nioBuffers()nioBuffers(int, int) 可能会返回较少数量的ByteBuffers
    • 如果返回 -1 ,表示这个缓冲区没有底层的NIO ByteBuffer
  2. ByteBuffer nioBuffer(): 将该缓冲区的可读字节转换成为单个NIO缓冲区,这个方法与buf.nioBuffer(buf.readerIndex(), buf.readableBytes())相同。

  3. ByteBuffer nioBuffer(int index, int length): 将该缓冲区的子区域转换成为单个NIO缓冲区。

  4. ByteBuffer[] nioBuffers(): 将该缓冲区的可读字节转换成为NIO缓冲区数组,这个方法与buf.nioBuffers(buf.readerIndex(), buf.readableBytes())相同。

  5. ByteBuffer[] nioBuffers(int index, int length):将该缓冲区的子区域转换成为NIO缓冲区数组。

  • 上面四个方法将该缓冲区转换成单个NIO缓冲区或者NIO缓冲区数组。而返回的NIO缓冲区共享该缓冲区内容或者复制该缓冲区内容,并且更改返回的NIO缓冲区的positionlimit不会影响该缓冲区的索引和标记。
  • 重点注意:如果这个缓冲区是动态的,并且它调整了容量,那么返回的NIO缓冲区将看不到这个缓冲区的变化。

最后还有一个即仅内部使用获取NIO缓冲区方法 ByteBuffer internalNioBuffer(int index, int length)

3.9 转换成字节数组

  1. boolean hasArray(): 当且仅当该缓冲区有支撑字节数组(backing byte array)时返回true

    如果这个方法返回true,您可以安全地调用array()arrayOffset()

  2. byte[] array(): 返回此缓冲区支撑字节数组(backing byte array)。
  3. int arrayOffset(): 返回此缓冲区的支撑字节数组(backing byte array)中第一个字节的偏移量。
  4. boolean hasMemoryAddress(): 当且仅当该缓冲区使用底层内存地址存储数据时返回true。
  5. long memoryAddress(): 返回指向存储的支撑数据的第一个字节的底层内存地址。

3.10 转换成字符串

  1. String toString(Charset charset): 将此缓冲区的可读字节解码为具有指定字符集名称的字符串。这个方法与buf.toString(buf.readerIndex(), buf.readableBytes(), charsetName)相同。
  2. String toString(int index, int length, Charset charset): 将此缓冲区的子区域解码为具有指定字符集的字符串。

3.11 其他方法

  1. int capacity(): 返回该缓冲区可以包含的字节数。

  2. ByteBuf capacity(int newCapacity): 调整此缓冲区的容量。

    • 如果newCapacity小于当前容量,则该缓冲区的内容将被截断。
    • 如果newCapacity大于当前容量,则在缓冲区中追加(newCapacity - currentCapacity)长度的未指定数据。
  3. int maxCapacity(): 返回此缓冲区的最大允许容量。该值提供了capacity()的上限。

  4. ByteBufAllocator alloc(): 返回创建此缓冲区的ByteBufAllocator

  5. ByteBuf unwrap(): 如果该缓冲区是另一个缓冲区的包装器,则返回底层缓冲区实例。

  6. boolean isDirect(): 当且仅当该缓冲区由NIO直接缓冲区支持时返回true。

  7. boolean isReadOnly(): 当且仅当此缓冲区是只读缓冲区时返回true。

  8. ByteBuf asReadOnly(): 返回此缓冲区的只读版本。

  9. int readerIndex(): 返回此缓存区的读索引值。

  10. ByteBuf readerIndex(int readerIndex): 重新设置该缓冲区的读索引readerIndex 值。

  11. int writerIndex(): 返回此缓存区的写索引值。

  12. ByteBuf writerIndex(int writerIndex): 重新设置该缓冲区的写索引writerIndex 值。

  13. ByteBuf setIndex(int readerIndex, int writerIndex): 同时设置该缓冲区的读索引和写索引的值。

  14. int readableBytes(): 返回该缓存区的可读字节数, 即(writerIndex -readerIndex)

  15. int writableBytes(): 返回该缓存区的可写字节数, 即(capacity - writerIndex)

  16. int maxWritableBytes(): 返回该缓存区的最大可写字节数, 即(maxCapacity - writerIndex)

  17. int maxFastWritableBytes(): 返回可以确定写入的最大字节数,而不涉及内部重新分配或数据拷贝。

    writableBytes() ≤ maxFastWritableBytes() ≤ maxWritableBytes()

  18. boolean isReadable(): 当且仅当 writerIndex - readerIndex 的值大于0

  19. boolean isReadable(int size): 当且仅当该缓冲区包含等于或大于指定数量的元素时返回true,即 writerIndex - readerIndex >= size

  20. boolean isWritable(): 当且仅当capacity() - writerIndex大于0

  21. boolean isWritable(int size): 当且仅当该缓冲区有足够的空间允许写入指定数量的元素时返回true,即 capacity() - writerIndex >= size

  22. ByteBuf clear(): 将该缓冲区的readerIndexwriterIndex设置为0,这个方法与setIndex(0,0)相同。

  23. ByteBuf discardReadBytes(): 丢弃第0个索引和readerIndex之间的字节。它将readerIndexwriterIndex之间的字节移动到第0个索引和 writerIndex - readerIndex 之间。

  24. ByteBuf discardSomeReadBytes(): 类似于discardReadBytes(),不同之处是此方法可能会根据其内部实现丢弃一些、全部或不丢弃读字节,从而以潜在的额外内存消耗为代价减少总体内存带宽消耗。

  25. ByteBuf ensureWritable(int minWritableBytes): 扩展缓存区的容量,以便写入更多数据,如果没有超过扩展缓存区的最大容量 maxCapacity,那么就会扩展成功。

  26. int ensureWritable(int minWritableBytes, boolean force): 扩展缓存区的容量。与上一个方法不同,它会返回状态代码,来表示扩容情况

    • 0 表示缓冲区有足够的可写字节,且其容量不变。
    • 1 表示缓冲区没有足够的字节,且其容量不变。
    • 2 表示缓冲区有足够的可写字节,且其容量已增加。
    • 3 表示缓冲区没有足够的字节,但其容量已增加到最大。
  27. ByteBuf markReaderIndex(),ByteBuf resetReaderIndex(), ByteBuf markWriterIndex()ByteBuf resetWriterIndex() 用来标记和重置索引。

四. 总结

通过上面的介绍,应该相信你应该可以很轻松地使用缓存区 ByteBuf, 接下来的文章,我们将讲解缓存区 ByteBuf 的不同类型的实现原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容