暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Netty 源码分析 —— NIO 基础(三)之 Buffer(四大属性图解)

努力努力再努力xLg 2020-02-12
574

我准备战斗到最后,不是因为我勇敢,是我想见证一切。--双雪涛《猎人》

[TOC] Thinking

  1. 一个技术,为什么要用它,解决了那些问题?

  2. 如果不用会怎么样,有没有其它的解决方法?

  3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?

  4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?

  5. 这些问题你又如何去解决的呢?

本文基于Netty 4.1.45.Final-SNAPSHOT

1、概述(Buffer 缓冲区)

 java.nio.Buffer
,一个 Buffer
对象是固定数量数据容器。实质上是内存中的一块。我们可以向这块内存存取数据。

缓冲区的工作与通道精密联系的。通道时I/O传输发生时通过的入口。而缓冲区是这些数据传输的来源或目标。(可写可读的双向性。)

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象。并提供了一组方法。用于方便当问该区域。

类图:

  • 其实可以将Buffer理解为一个数组的封装。例如:IntBuffer —>int[]

  • MappedByteBuffer 用于实现内存映射文件。

2、Buffer的基础使用

  1. 写入数据到Buffer

  2. 调用 flip()
    方法

  3. 从Buffer中读取数据

  4. 调用 clear()
    方法或者 compact()
    方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。(这种定义可以理解为,缓冲区本身维护着一个类似于队列的数据结构,先进先出原则)

  1. /**

  2. * Buffer 基本使用

  3. *

  4. * @author by Mr. Li

  5. * @date 2020/2/11 12:23

  6. */

  7. @Slf4j

  8. public class BufferExample {


  9. public static void main(String[] args) throws Exception {

  10. RandomAccessFile accessFile = new RandomAccessFile("E:/idea_workspace/springcloud2.0/netty/netty-mytest/src/main/resources/data/nio-data.txt", "rw");

  11. // 获取 NIO 文件通道

  12. FileChannel channel = accessFile.getChannel();

  13. // 获取缓冲区Buffer

  14. ByteBuffer byteBuffer = ByteBuffer.allocate(48);

  15. int count = channel.read(byteBuffer);// read into buffer.

  16. while (count != -1) {

  17. // make buffer ready for read

  18. byteBuffer.flip();


  19. while (byteBuffer.hasRemaining()) {

  20. log.info("read 1 byte at a time : {}", byteBuffer.get());

  21. }

  22. byteBuffer.clear(); // make buffer ready for writing .即清空缓冲区,是Buffer又可以读写数据

  23. count = channel.read(byteBuffer);

  24. }

  25. // 关闭文件

  26. accessFile.close();

  27. }

  28. }

3、Buffer 属性

Buffer缓冲区中提供了四个属性。用于来提供其包含的数据元素的信息。

容量(Capacity)

缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区被初始化时,就会被创建,并且永远不会改变。

上界(Limit)

缓冲区的第一个不能被读或写的元素。(可以理解为缓冲区中现有可读的数据)

位置(Position)

下一个要被读或写的元素的索引。(该属性被 #get()``#put()
自动维护)

标记(Mark)

一个备忘位置。调用 #mark()
来设定 mark=position
。调用 #reset()
设定 position=mark
。标记在设定前是未定义的 undefined

源码

  1. public abstract class Buffer {


  2. /**

  3. * The characteristics of Spliterators that traverse and split elements

  4. * maintained in Buffers.

  5. */

  6. static final int SPLITERATOR_CHARACTERISTICS =

  7. Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;


  8. // Invariants: mark <= position <= limit <= capacity

  9. private int mark = -1;

  10. private int position = 0;

  11. private int limit;

  12. private int capacity;


  13. // Used only by direct buffers

  14. // 仅由直接缓冲区使用

  15. // NOTE: hoisted here for speed in JNI GetDirectBufferAddress

  16. long address;


  17. // Creates a new buffer with the given mark, position, limit, and capacity,

  18. // after checking invariants.

  19. //

  20. Buffer(int mark, int pos, int lim, int cap) { // package-private

  21. if (cap < 0)

  22. throw new IllegalArgumentException("Negative capacity: " + cap);

  23. this.capacity = cap;

  24. limit(lim);

  25. position(pos);

  26. if (mark >= 0) {

  27. if (mark > pos)

  28. throw new IllegalArgumentException("mark > position: ("

  29. + mark + " > " + pos + ")");

  30. this.mark = mark;

  31. }

  32. }

  33. //....doSomething

从源码可以看出四种属性的关系:

mark <= position <= limit <= capacity

0 <= mark <= position <= limit <= capacity

Buffer四种属性图解

Buffer缓冲区是分读写模式的。(使用filp() 切换读写模式)

porsition和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

写模式

  • 假设定义的ByteBuffer对象。(长度为10 的缓冲区长度 ByteBuffer.allocate(10);

  • 此时创建出来的Buffer对象 Limit 和capacity为指向内存末端,数值为9。

代码示例如下:

  1. /**

  2. * Buffer 读写模式切换即 属性赋值变化。

  3. * @throws Exception

  4. */

  5. @Test

  6. public void bufferTest() throws Exception {

  7. // 初始化 长度为10 的Buffer

  8. ByteBuffer byteBuffer = ByteBuffer.allocate(10);

  9. RandomAccessFile accessFile = new RandomAccessFile("E:/idea_workspace/springcloud2.0/netty/netty-mytest/src/main/resources/data/buf-data.txt", "rw");


  10. // 获取 文件通道

  11. FileChannel channel = accessFile.getChannel();


  12. // 将通道的中的数据读取到缓冲区中

  13. int i = channel.read(byteBuffer);

  14. while (i != -1) {

  15. // 切换 成读模式

  16. byteBuffer.flip();

  17. while (byteBuffer.hasRemaining()) {

  18. log.info("write -> read {}", byteBuffer.get());

  19. }

  20. byteBuffer.clear();

  21. // 确保 数据读取完毕。返回-1

  22. i = channel.read(byteBuffer);

  23. }

  24. accessFile.close();

  25. }

Debug查看属性变化:

读模式

总结一下

  • 从上述读写切换可以看出

  • 此内存区域实则可以看作是一块数组内存区域。在初始化时,capacity是被固定的,用来标识内存的容量,可存放的最大元素数。并且不管读写如何切换,capacity是不会改变的。

  • Limit 在读写模式下的含义(上限)

  • 写模式:即在缓存被初始化时( ByteBuffer.allocate(10);
    )Limit就指向了内存的最大容量与capacity相等。

  • 读模式:当调用 #filp()
    切换为读模式后,limit就会指向内存中的实际容量,此时position就会指向下一个内存中即将被读的索引位置。

  • position 永远指向下一个即将被写/读的索引位置。初始值为0。

  • 模式下,每往 Buffer 中写入一个值, position
     就自动加 1 ,代表下一次的写入位置。

  • 模式下,每从 Buffer 中读取一个值, position
     就自动加 1 ,代表下一次的读取位置。( 和写模式类似 )

  • mark 标记,通过 #mark()
    方法,记录当前 position
    ;通过 #reset()
    恢复 position
    为标记。

  • 模式下,标记上一次写位置。

  • 模式下,标记上一次读位置。

  • 所以从代码层次上充分的体现了四种属性的关系

  • limit最大只能与 capacity相等。

    • 最小则是缓冲区为空。

  • position则是四大属性中,最善变的一个,一直随将要读写的操作变化着。

  • mark 则永远都不会超过position。并且初始化下的值为-1.

mark <= positioon <= limit <= capacity

思考

在这种反转切换读写的模式下,真的非常的繁琐。操作复杂。完全可以使用独立的标记,用于标记读写不同的操作。类似于Netty使用的模式,就是摒弃了这种反转的操作。

Netty的ByteBuf下优雅的设计

  1. 0 <= readerIndex <= writerIndex <= capacity

4、创建Buffer

1. 每个Buffer实现类,都相应的提供了 #allocate(int capacity)
静态方法。

  1. /**

  2. * Allocates a new byte buffer. Allocates a new direct byte buffer.

  3. *

  4. * <p> The new buffer's position will be zero, its limit will be its

  5. * capacity, its mark will be undefined, and each of its elements will be

  6. * initialized to zero. It will have a {@link #array backing array},

  7. * and its {@link #arrayOffset array offset} will be zero.

  8. *

  9. * @param capacity

  10. * The new buffer's capacity, in bytes

  11. *

  12. * @return The new byte buffer

  13. *

  14. * @throws IllegalArgumentException

  15. * If the <tt>capacity</tt> is a negative integer

  16. */

  17. public static ByteBuffer allocate(int capacity) {

  18. if (capacity < 0)

  19. throw new IllegalArgumentException();

  20. return new HeapByteBuffer(capacity, capacity);

  21. }

由于ByteBuffer 为抽象类,返回的是它基于堆内(Non-Direct)内存的实现类HeapByteBuffer的对象。

  1. HeapByteBuffer(int cap, int lim) { // package-private


  2. super(-1, 0, lim, cap, new byte[cap], 0);

  3. /*

  4. hb = new byte[cap];

  5. offset = 0;

  6. */

  7. }

  8. // 这里可以清晰的看出,Buffer 其实质就是一个内存数组。用于存储数据


  1. 每个 Buffer 实现类,都提供了 #wrap(array)
     静态方法,帮助我们将其对应的数组包装成一个 Buffer 对象。还是以 ByteBuffer 举例子,代码如下:


    1. // ByteBuffer.java Allocates a new direct byte buffer.

    2. public static ByteBuffer wrap(byte[] array,int offset, int length){

    3. try {

    4. return new HeapByteBuffer(array, offset, length);

    5. } catch (IllegalArgumentException x) {

    6. throw new IndexOutOfBoundsException();

    7. }

    8. }


    9. public static ByteBuffer wrap(byte[] array) {

    10. return wrap(array, 0, array.length);

    11. }

  • 和 #allocate(int capacity)
     静态方法一样,返回的也是 HeapByteBuffer 的对象。

为什么Buffer要创建为 Direct Buffer

Direct Buffer 与Non-Direct Buffer 的区别

FROM 《Java NIO 的前生今世 之三 NIO Buffer 详解》

Direct Buffer:

  • 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的, 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)

  • 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)

  • 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.

  • 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.

Non-Direct Buffer:

  • 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.

  • 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.

总结对比:

  • 之所以使用堆外内存,是为了避免每次使用buffe如对象时,都会将此对象复制到中间林是缓冲区中,因此Non-Direct Buffer效率会非常低下。

  • 堆外内存(直接内存--direct byte buffer)则可以直接使用,避免了对象的复制,提高了效率。

5、写入数据

每个Buffer的实现类,都可以基于Channel来写入数据,但每个实现类也单独的提供了写入数据的方法 #put(...)
。向Buffer写入数据,以 ByteBuffer
举例子。

  1. /**

  2. * buffer 的 读写 操作

  3. */

  4. @Test

  5. public void bufferTest02() throws Exception {

  6. log.info("Write is coming.this is two method");

  7. // Buffer 的写 有两种,一种基于 Channel 一种是自己提供的#put()方法

  8. ByteBuffer byteBuffer = ByteBuffer.allocate(48);

  9. byteBuffer.put((byte) 1); // position = 1


  10. // 获取 文件Channel

  11. RandomAccessFile accessFile = new RandomAccessFile("E:/idea_workspace/springcloud2.0/netty/netty-mytest/src/main/resources/data/buf-data.txt", "rw");

  12. // 将Channel 中的数据写入Buffer 中

  13. FileChannel channel = accessFile.getChannel();

  14. int readResult = channel.read(byteBuffer);// position = 4


  15. log.info("Read is coming。。。。");

  16. while (readResult != -1) {

  17. // Write -> Read

  18. byteBuffer.flip();

  19. /**

  20. * public final boolean hasRemaining() {

  21. * return position < limit;

  22. * }

  23. * 用来判断 队首是否还有未读数据。

  24. */

  25. while (byteBuffer.hasRemaining()) {

  26. log.info("byteBuffer read ready -> {}", byteBuffer.get());

  27. }


  28. byteBuffer.clear(); // 清除/并且重置到初始化状态。写状态

  29. readResult = channel.read(byteBuffer);

  30. }

  31. accessFile.close();

  32. }

  33. }

上述代码,提供了两种向Buffer 写入数据的方式。

两种方式,分别的丰富了Buffer 的实用性。Channle则是满足了来自各个网络或文件等外部资源的输入。

  1. int num = channel.read(buffer);

  2. // 返回的是从Channel中写入到Buffer的数据大小

这里需要注意的是:在NIO 中读Buffer 的读写操作。

当Channel调用 #read()
方法时,实则是Buffer示例的写入操作。(需要重点明确。)

因为在这里所谓的读写操作都是先对于Buffer示例而言的。所以不要看到调用的方法是read()。实则是写入操作。与Buffer而言。

6、读取数据

每个Buffer实现类,都提供了 #get(...)
方法,从Buffer读取数据。

  1. // 读取 byte

  2. public abstract byte get();

  3. public abstract byte get(int index);

  4. // 读取 byte 数组

  5. public ByteBuffer get(byte[] dst, int offset, int length) {...}

  6. public ByteBuffer get(byte[] dst) {...}

  7. // ... 省略,还有其他 get 方法

对于 Buffer 来说,还有一个非常重要的操作就是,我们要讲来向 Channel 的写入 Buffer 中的数据。在系统层面上,这个操作我们称为写操作,因为数据是从内存中写入到外部( 文件或者网络等 )。示例如下:

  1. int num = channel.write(buffer);


  • 上述方法会返回向 Channel 中写入 Buffer 的数据大小。对应方法的代码如下:


    1. public interface WritableByteChannel extends Channel {


    2. public int write(ByteBuffer src) throws IOException;


    3. }

7、rewind() flip()/clear()

7.1、filp

 #filp()
将Buffer写状态切换成度状态。

  1. /**

  2. * Flips this buffer. The limit is set to the current position and then

  3. * the position is set to zero. If the mark is defined then it is

  4. * discarded.

  5. 反转整个缓冲区,将limit设置为当前的position(即为当前缓冲区的实际数据容量),然后将position设置为0,如果使用了标记mark,将其抛弃,设置为初始值-1。

  6. *

  7. * <p> After a sequence of channel-read or <i>put</i> operations, invoke

  8. * this method to prepare for a sequence of channel-write or relative

  9. * <i>get</i> operations. For example:

  10. *

  11. * <blockquote><pre>

  12. * buf.put(magic); / Prepend header

  13. * in.read(buf); // Read data into rest of buffer

  14. * buf.flip(); // Flip buffer

  15. * out.write(buf); // Write header + data to channel</pre></blockquote>

  16. *

  17. * <p> This method is often used in conjunction with the {@link

  18. * java.nio.ByteBuffer#compact compact} method when transferring data from

  19. * one place to another. </p>

  20. *

  21. * @return This buffer

  22. */

  23. public final Buffer flip() {

  24. limit = position; // 设置读取上限

  25. position = 0; // 重置position

  26. mark = -1; // 清空mark

  27. return this;

  28. }

示例代码,如下:

  1. @Test

  2. public void flipTest() throws Exception {

  3. RandomAccessFile accessFile = new RandomAccessFile("E:/idea_workspace/springcloud2.0/netty/netty-mytest/src/main/resources/data/buf-data.txt", "rw");

  4. FileChannel channel = accessFile.getChannel();

  5. ByteBuffer allocate = ByteBuffer.allocate(48);

  6. allocate.put("magic".getBytes());

  7. allocate.flip();

  8. channel.write(allocate);

  9. log.info("channel 当前容量为{}",channel.position());


  10. }

7.2、rewind

 #rewind()
 方法,可以重置 position
 的值为 0 。因此,我们可以重新读取和写入 Buffer 了。

大多数情况下,该方法主要针对于读模式,所以可以翻译为“倒带”。也就是说,和我们当年的磁带倒回去是一个意思。(意思就是,重置缓冲区中的数据。)代码如下:

  1. public final Buffer rewind() {

  2. position = 0; // 重置 position

  3. mark = -1; // 清空 mark

  4. return this;

  5. }

从代码上,和 #flip()
 相比,非常类似,除了少了第一行的 limit=position
 的代码块。

使用示例,代码如下:

  1. channel.write(buf); // Write remaining data

  2. buf.rewind(); // Rewind buffer

  3. buf.get(array); // Copy data into array

7.3、clear

 #clear()
 方法,可以“重置” Buffer 的数据。因此,我们可以重新读取和写入 Buffer 了。

大多数情况下,该方法主要针对于写模式。代码如下:

  1. public final Buffer clear() {

  2. position = 0; // 重置 position

  3. limit = capacity; // 恢复 limit 为 capacity

  4. mark = -1; // 清空 mark

  5. return this;

  6. }

  • 从源码上,我们可以看出,Buffer 的数据实际并未清理掉,所以使用时需要注意。

  • 读模式下,尽量不要调用 #clear()
     方法,因为 limit
     可能会被错误的赋值为 capacity
     。相比来说,调用 #rewind()
     更合理,如果有重读的需求。

使用示例,代码如下:

  1. buf.clear(); // Prepare buffer for reading

  2. in.read(buf); // Read data

8、 mark() 搭配 reset()

8.1 mark

#mark()
 方法,保存当前的 position
 到 mark
 中。代码如下:

  1. public final Buffer mark() {

  2. mark = position;

  3. return this;

  4. }

  • 唯一的设置 标记位的方法。

8.2 reset

#reset()
 方法,恢复当前的 postion
 为 mark
 。代码如下:

  1. public final Buffer reset() {

  2. int m = mark;

  3. if (m < 0)

  4. throw new InvalidMarkException();

  5. position = m;

  6. return this;

  7. }

  • 恢复到标记位,有类似于秒表的功能,撤回到标记位。并且可重新设置标记位后面的值。

9 、hasRemaining

多半使用在读操作中,用作判断缓冲区中是否还有可读数据。(可想而知:是直接判断 下个即将要读数据的索引位置与上限做比较。如果相等则没有可读数据 returnposition<limit

  1. /**

  2. * Tells whether there are any elements between the current position and

  3. * the limit.

  4. *告诉当前位置和上限之间是否有任何元素

  5. * @return <tt>true</tt> if, and only if, there is at least one element

  6. * remaining in this buffer

  7. */

  8. public final boolean hasRemaining() {

  9. return position < limit;

  10. }

10、equals()与compareTo()方法

10.1、equals()

当满足下列条件时,表示两个Buffer相等:

  1. 有相同的类型(byte、char、int 等)

  2. Buffer中剩余的Byte、char等的个数相同。

  3. Buffer中所有剩余的byte、char等都相同。

如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

10.2、compareTo()

compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  1. 第一个不相等的元素小于另一个Buffer中对应的元素 。

  2. 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

(译注:剩余元素是从 position到limit之间的元素)


其它源码相对简单。不一一介绍了。

本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!

Java NIO系列教程(三) Buffer

——努力努力再努力xLg

加油!


文章转载自努力努力再努力xLg,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论