相关链接:

  1. ByteBuffer 【https://blog.csdn.net/mrliuzhao/article/details/89453082

|

位置 限制 容量 标记
position limit capcity mark
mark / / / position
reset position / / /
clear 0 capcity / 丢弃
flip 0 position / /
rewind 0 / / /

mark(), 将mark的值设置成当前position的值;
reset() position属性设置为mark的值
remaining(),查看该Buffer当前的剩余可用长度(当前postion—limit之间的距离)
clear() 把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。mark=-1
注意:clear只是将各个状态恢复到初始状态,但是buffer每个位置上的数据并没有被清除;
flip(),把limit 设置成 positon,然后把position设置成0;相当于写结束了,切换到读的模式;
compact(),从读的模式切换到写的模式,将postion—limit之间的内容(未读取的内容)拷贝到buffer数组的最前面,然后将positon移动至这些数据的后一位,limit移动到capacity的位置;这样position-limit之间的数据就能够再次写入了;
image.png

1 Nio基础知识

1.1 ByteBuffer

1. ByteBuffer相关概念

ByteBuffer包含几个基本的属性:

  • position:当前的下标位置,表示进行下一个读写操作时的起始位置;
  • limit:结束标记下标,表示进行下一个读写操作时的(最大)结束位置;
  • capacity:该ByteBuffer容量;
  • mark: 自定义的标记位置;为某一读过的位置做标记,便于某些时候回退到该位置。

一个临时存放的位置下标。调用mark()会将mark设为当前的position的值,以后调用reset()会将position属性设
置为mark的值。mark的值总是小于等于position的值,如果将position的值设的比mark小,当前的mark值会被抛弃掉。

无论如何,这4个属性总会满足如下关系:mark <= position <= limit <= capacity。目前对mark属性了解的不多,故在此暂不做讨论。其余3个属性可以分别通过ByteBuffer.position()、ByteBuffer.limit()、ByteBuffer.capacity()获取
其中position和limit属性也可以分别通过ByteBuffer.position(int newPos)、ByteBuffer.limit(int newLim)进行设置,但由于ByteBuffer在读取和写出时是非阻塞的,读写数据的字节数往往不确定,故通常不会使用这两个方法直接进行修改。

2. 初始化

  1. ByteBuffer byteBuffer_1 = ByteBuffer.allocate(16);
  2. ByteBuffer byteBuffer_2 = ByteBuffer.wrap(new byte[16]);

首先无论读写,均需要初始化一个ByteBuffer容器。如上所述,ByteBuffer其实就是对byte数组的一种封装,所以可以使用静态方法wrap(byte[] data)手动封装数组,也可以通过另一个静态的allocate(int size)方法初始化指定长度的ByteBuffer。初始化后,ByteBuffer的position就是0;其中的数据就是初始化为0的字节数组;limit = capacity = 字节数组的长度;用户还未自定义标记位置,所以mark = -1,即undefined状态。下图就表示初始化了一个容量为16个字节的ByteBuffer,其中每个字节用两位16进制数表示:
image.png

3 向ByteBuffer写数据

1 手动写入

可以手动通过put(byte b)put(byte[] b)方法向ByteBuffer中添加一个字节或一个字节数组。ByteBuffer也方便地提供了几种写入基本类型的put方法:putChar(char val)、putShort(short val)、putInt(int val)、putFloat(float val)、putLong(long val)、putDouble(double val)。执行这些写入方法之后,就会以当前的position位置作为起始位置,写入对应长度的数据,并在写入完毕之后将position向后移动对应的长度。下图就表示了分别向ByteBuffer中写入1个字节的byte数据和4个字节的Int数据的结果
image.png

  1. ByteBuffer byteBuffer_1 = ByteBuffer.allocate(16);
  2. ByteBuffer byteBuffer_2 = ByteBuffer.wrap(new byte[16]);
  3. byteBuffer_1.put((byte)1);
  4. byteBuffer_1.putInt(11);
  5. System.out.println(byteBuffer_1.position());
  6. System.out.println("---剩余长度"+byteBuffer_1.remaining()); //---剩余长度11
  7. byte[] bytes = "abcdefghigklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8);
  8. System.out.println(bytes.length); // 26
  9. byteBuffer_2.put(bytes); // 26 超过了定义bytebuffer时的16了,报错
  10. System.out.println(byteBuffer_2.position());

但是当想要写入的数据长度大于ByteBuffer当前剩余的长度时,则会抛出BufferOverflowException异常,剩余长度的定义即为limit与position之间的差值(即 limit - position)。如上述例子中,若再执行buffer.put(new byte[12]);就会抛出BufferOverflowException异常,因为剩余长度为11。可以通过调用**ByteBuffer.remaining()**查看该ByteBuffer当前的剩余可用长度

2 从nio通道读入数据到ByteBuffer

省略,去看原文吧

4 从ByteBuffer中读取数据

现在ByteBuffer容器中已经存有数据,那么现在就要从ByteBuffer中将这些数据取出来解析。由于position就是下一个读写操作的起始位置,故在读取数据后直接写出数据肯定是不正确的,要先把position复位到想要读取的位置

第1步 position复位 [flip()推荐、rewind()]

首先看一个rewind()方法,该方法仅仅是简单粗暴地将position直接复原到0,limit不变。这样进行读取操作的话,就是从第一个字节开始读取了。如下图:
image.png
rewind() 效果: position =0 ;但是limit还是等与capacity(容量);读取数据的化,后边界数据都是空
该方法虽然复位了position,可以从头开始读取数据,但是并未标记处有效数据的结束位置。如本例所述,ByteBuffer总容量为16字节,但实际上只读取了9个字节的数据,因此最后的7个字节是无效的数据。故rewind()方法常常用于字节数组的完整拷贝。

实际应用中更常用的是flip()方法,该方法不仅将position复位为0,同时也将limit的位置放置在了position之前所在的位置上,这样position和limit之间即为新读取到的有效数据。如下图:
image.png

第2步 读取数据

在将position复位之后,我们便可以从ByteBuffer中读取有效数据了。类似put()方法,ByteBuffer同样提供了一系列get方法,从position开始读取数据。get()方法读取1个字节,getChar()、getShort()、getInt()、getFloat()、getLong()、getDouble()则读取相应字节数的数据,并转换成对应的数据类型。如getInt()即为读取4个字节,返回一个Int,getLong(),读取8字节并返回一个long。在调用这些方法读取数据之后,ByteBuffer还会将position向后移动读取的长度,以便继续调用get类方法读取之后的数据。

补充:java基本类型占用多少自字节 image.png

这一系列get方法也都有对应的接收一个int参数的重载方法,参数值表示从指定的位置读取对应长度的数据。如getDouble(2)则表示从下标为2的位置开始读取8个字节的数据,转换为double返回。不过实际应用中往往对指定位置的数据并不那么确定,所以带int参数的方法也不是很常用
get()方法则有两个重载方法:

  • 1get(byte[] dst, int offset, int length):表示尝试从 position 开始读取 length 长度的数据拷贝到 dst 目标数组 offset 到 offset + length 位置,相当于执行了 ```java 循环每次读取一个字节;并将读取到的内容放到目标数组dst的特定位置; for (int i = offset; i < offset + len; i++) dst[i] = buffer.get();
  1. - 2 `get(byte[] dst)`:尝试读取 dst 目标数组长度的数据,拷贝至目标数组,相当于执行了
  2. 相较于第一个方法;这相当于 offset=0 length = dst.length ;把这两个参数固定了;
  3. ```java
  4. buffer.get(dst, 0, dst.length);

此处应注意读取数据后,已读取的数据也不会被清零。下图即为从例子中连续读取1个字节的byte和4个字节的int数据
image.png
此处同样要注意,当想要读取的数据长度大于ByteBuffer剩余的长度时,则会抛出 BufferUnderflowException 异常。如上例中,若再调用buffer.getLong()就会抛出 BufferUnderflowException 异常,因为 remaining 仅为4

第3步 确保数据长度

为了防止出现上述的 BufferUnderflowException 异常(当想要读取的数据长度大于ByteBuffer剩余的长度时),最好要在读取数据之前确保 ByteBuffer 中的有效数据长度足够。在此记录一下我的做法

private void checkReadLen(
    long reqLen,
    ByteBuffer buffer,
    SocketChannel dataSrc
) throws IOException {
  int readLen;
  if (buffer.remaining() < reqLen) { // 剩余长度不够,重新读取   ByteBuffer.remaining()查看该ByteBuffer当前的剩余可用长度。
      buffer.compact(); // 准备继续读取
    System.out.println("Buffer remaining is less than" + reqLen + ". Read Again...");
    while (true) {
      readLen = dataSrc.read(buffer);
      System.out.println("Read Again Length: " + readLen + "; Buffer Position: " + buffer.position());
      if (buffer.position() >= reqLen) { // 可读的字节数超过要求字节数
        break;
      }
    }
    buffer.flip();
    System.out.println("Read Enough Data. Remaining bytes in buffer: " + buffer.remaining());
  }
}

第4步 字节序处理 【这个留意就好了】

基本类型的值在内存中的存储形式还有字节序的问题,这种问题在不同CPU的机器之间进行网络通信时尤其应该注意。同时在调用ByteBuffer的各种get方法获取对应类型的数值时,ByteBuffer也会使用自己的字节序进行转换。因此若ByteBuffer的字节序与数据的字节序不一致,就会返回不正确的值。如对于int类型的数值8848,用16进制表示,大字节序为:0x 00 00 22 90;小字节序为:0x 90 22 00 00。若接收到的是小字节序的数据,但是却使用大字节序的方式进行解析,获取的就不是8848,而是-1876819968,也就是大字节序表示的有符号int类型的 0x 90 22 00 00。
JavaNIO提供了java.nio.ByteOrder枚举类来表示机器的字节序,同时提供了静态方法ByteOrder.nativeOrder()可以获取到当前机器使用的字节序,使用ByteBuffer中的order()方法即可获取该buffer所使用的字节序。同时也可以在该方法中传递一个ByteOrder枚举类型来为ByteBuffer指定相应的字节序。如调用buffer.order(ByteOrder.LITTLE_ENDIAN)则将buffer的字节序更改为小字节序。
一开始并不知道还可以这样操作,比较愚蠢地手动将读取到的数据进行字节序的转换。不过觉得还是可以记下来,也许在别的地方用得到。JDK中的 Integer 和 Long 都提供了一个静态方法reverseBytes()来将对应的 int 或 long 数值的字节序进行翻转。而若想读取 float 或 double,也可以先读取 int 或 long,然后调用 Float.intBitsToFloat(int val) 或 Double.longBitsToDouble(long val) 方法将对应的 int 值或 long 值进行转换。当ByteBuffer中的字节序与解析的字节序相反时,可以使用如下方法读取:

int i = Integer.reverseBytes(buffer.getInt()); 
float f = Float.intBitsToFloat(Integer.reverseBytes(buffer.getInt()));
long l = Long.reverseBytes(buffer.getLong());
double d = Double.longBitsToDouble(buffer.getLong());

第5步 继续写入数据

由于ByteBuffer往往是非阻塞式的,故不能确定新的数据是否已经读完,但这时候依然可以调用ByteBuffer的compact()方法切换到读取模式。该方法就是将 position 到 limit 之间还未读取的数据拷贝到 ByteBuffer 中数组的最前面,然后再将 position 移动至这些数据之后的一位,将 limit 移动至 capacity。这样 position 和 limit 之间就是已经读取过的老的数据或初始化的数据,就可以放心大胆地继续写入覆盖了。仍然使用之前的例子,调用 compact() 方法后状态如下:
image.png

总结:

总之ByteBuffer的基本用法就是:
初始化(allocate)–> 写入数据(read / put)–> 转换为写出模式(flip)–> 写出数据(get)–> 转换为写入模式(compact)–> 写入数据(read / put)…

这些属性总是满足以下条件:
0 <= mark <= position <= limit <= capacity limit和position的值除了通过limit()和position()函数来设置,也可以通过下面这些函数来改变: Buffer clear() 视情况而用
把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。 Buffer flip()
把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。 Buffer rewind() —不推荐用
把position设为0,limit不变,一般在把数据重写入Buffer前调用。 compact() 该方法的作用是将 position 与 limit之间的数据复制到buffer的开始位置,复制后 position = limit -position,limit = capacity 但如果position 与limit 之间没有数据的话发,就不会进行复制 详细参考:java nio Buffer 中 compact的作用

mark()与reset()方法 通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如: 1.buffer.mark(); 2.//call buffer.get() a couple of times, e.g. during parsing. 3.buffer.reset(); //set position back to mark equals()与compareTo()方法 可以使用equals()和compareTo()方法两个Buffer。 equals() 当满足下列条件时,表示两个Buffer相等:

  1. 有相同的类型(byte、char、int等)。
  2. Buffer中剩余的byte、char等的个数相等。
  3. Buffer中所有剩余的byte、char等都相同。

如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。 compareTo()方法 compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  1. 第一个不相等的元素小于另一个Buffer中对应的元素 。
  2. 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

Buffer对象有可能是只读的,这时,任何对该对象的写操作都会触发一个ReadOnlyBufferException。
isReadOnly()方法可以用来判断一个Buffer是否只读。