网络编程-04丨详解NIO Buffer

Posted by jiefang on June 14, 2020

Buffer

简介

A container for data of a specific primitive type. A buffer is a linear, finite sequence of elements of a specific primitive type. Aside from its content, the essential properties of a buffer are its capacity, limit, and position.

  • A buffer’s capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
  • A buffer’s limit is the index of the first element that should not be read or written. A buffer’s limit is never negative and is never greater than its capacity.
  • A buffer’s position is the index of the next element to be read or written. A buffer’s position is never negative and is never greater than its limit.

一种用于特定基本类型的数据容器。缓冲区是一个特定的基本类型的元件的线性,有限序列。 除了其内容,缓冲的基本性质是它的容量,限制和位置:

  • 缓冲区的容量是它所包含的元素数量。 缓冲区的容量从不为负,从来没有改变。
  • 缓冲区的限制是不应读取或写入的第一个元素的索引。 缓冲区的限制是从不为负,并且永远不会比它更大的容量。
  • 缓冲区的位置要被读出或写入的下一个元素的索引。 缓冲区的位置永远不会为负,并且永远不会比它的极限。

在NIO中有8种缓冲区类,分别如下:ByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBufferMappedByteBufferMappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。

Buffer类Diagram图

属性

Buffer的重要成员属性分别是:capacity(容量)、position(读写位置)、limit(读写的限制)。此外标记属性为:mark(标记),可以将当前position临时存入mark中;需要的时候从mark标记恢复position位置。

1
2
3
4
5
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

| 属性 | 说明 | | ———— | ———— | | capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时设置并且不能改变 | | limit | 上限,缓冲区中当前的数据量| | position | 位置,缓冲区中下一个要被读或些的元素的索引| | mark | 调用mark()方法来设置mark=position,再调用reset()可以让position恢复到mark标记的位置,即position=mark |

capacity

Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity容量,缓冲区就满了,不能再写入了。 Buffer类的对象在初始化时,会按照capacity分配内部的内存。在内存分配好之后,大小不在改变。capacity容量不是指内存块byte[]数组的字节的数量,capacity容量指的是写入的数据对象的数量

position

Buffer类的position属性,表示当前的位置。position属性与缓冲区的读写模式有关。在不同的模式下,position属性的值是不同的。当缓冲区进行读写的模式改变时,position会进行调整。

  • 在写入模式下,position的值变化:
    • 在刚进入到写模式时,position值为0,表示当前的写入位置为从头开始。
    • 每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
    • 初始的position值为0,最大可写值position为limit– 1。当position值达到limit时,缓冲区就已经无空间可写了。
  • 在读模式下,position的值变化:
    • 当缓冲区刚开始进入到读模式时,position会被重置为0。
    • 当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
    • position最大的值为最大可读上限limit,当position达到limit时,表明缓冲区已经无数据可读。

      limit

      Buffer类的limit属性,表示读写的最大上限。limit属性,也与缓冲区的读写模式有关。在不同的模式下,limit的值的含义是不同的。

  • 在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入到写模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。
  • 在读模式下,limit的值含义为最多能从缓冲区中读取到多少数据。

方法

flip()翻转缓冲区

allocate(int capacity)

Buffer使用之前要分配内存空间,使用allocate(int capacity)创建缓冲区。ByteBuffer#allocate(int capacity)示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
		//new一个ByteBuffer的子类HeapByteBuffer对象
        return new HeapByteBuffer(capacity, capacity);
    }
	HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }
	ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }
	Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

put()

在调用allocate方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象。要写入缓冲区,需要调用put方法。put方法很简单,只有一个参数,即为所需要写入的对象。不过,写入的数据类型要求与缓冲区的类型保持一致。 ByteBuffer#put(byte b)源码:

1
2
3
4
5
6
7
8
9
10
11
	public abstract ByteBuffer put(byte b);
	//byte写入数组,position递增
	public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }
    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }	

flip()

反转此缓冲区。使Buffer由写入模式转成读取模式。

  1. 设置可读的长度上限limit。将写模式下的缓冲区中内容的最后写入位置position值,作为读模式下的limit上限值。
  2. 把读的起始位置position的值设为0,表示从头开始读。
  3. 清除之前的mark标记,因为mark保存的是写模式下的临时位置。在读模式下,如果继续使用旧的mark标记,会造成位置混乱。
    1
    2
    3
    4
    5
    6
    
     public final Buffer flip() {
         limit = position;
         position = 0;
         mark = -1;
         return this;
     }
    

get()

读取此缓冲区当前位置的字节,然后该位置递增。

1
2
3
4
5
6
7
8
9
    public abstract byte get();
	public byte get() {
        return hb[ix(nextGetIndex())];
    }
    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }	

rewind()

倒带:重新读取缓冲区,位置被设置为零,并且标记被丢弃。

  1. position重置为0,所以可以重读缓冲区中的所有数据。
  2. limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取多少个元素。
  3. mark标记被清理,表示之前的临时位置不能再用了。
    1
    2
    3
    4
    5
    
     public final Buffer rewind() {
         position = 0;
         mark = -1;
         return this;
     }
    

    mark()和reset()

    Buffer.mark()方法的作用是将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark的值恢复到position中。

1
2
3
4
5
6
7
8
9
10
11
    public final Buffer mark() {
        mark = position;
        return this;
    }
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }	

clear()

在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法会将position清零,limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。

1
2
3
4
5
6
7
	//Clears this buffer.  The position is set to zero, the limit is set to the capacity, and the mark is discarded.
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

compact()

压缩缓冲区。缓冲区当前位置和界限之间的字节,如果有的话,将被复制到缓冲区的开始。 即,在索引p字节= 位置()被复制到索引0,在索引p + 1的字节被复制到索引1,依此类推,直到将索引limit()的字节- 1被复制到索引n = 极限() - 1 - p。 缓冲区的位置然后被设置为n + 1,并将其界限设置为它的容量。

1
2
3
4
5
6
7
8
	public abstract ByteBuffer compact();
	public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

小结

使用Java NIO Buffer类的基本步骤:

  1. 使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。
  2. 调用put()方法,将数据写入到缓冲区中。
  3. 写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。
  4. 调用get()方法,从缓冲区中读取数据。
  5. 读取完成后,调用Buffer.clear()或Buffer.compact()方法,将缓冲区转换为写入模式。