一、简介
数据的传输不是按照原有的磨样进行的,是经过一定的转换的,我们经常用到的也就是ByteBuffer,除此之外还有ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。Bytebuffer还有一些子类MappedByteBuffer、DirectByteBuffer、HeapByteBuffer,详见下图
二、ByteBuffer的核心属性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- capacity:缓冲区的容量。通过构造函数赋予,一旦设置,无法更改
- limit:缓冲区的界限。位于limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量
- position:下一个读写位置的索引(类似PC)。缓冲区的位置不能为负,并且不能大于limit
- mark:记录当前position的值。position被改变后,可以通过调用reset() 方法恢复到mark的位置。
以上四个属性必须满足以下要求:mark <= position <= limit <= capacity
看一段代码
public class TestByteBuffer {
public static void main(String[] args) {
// 获得FileChannel
try (FileChannel channel = new FileInputStream("stu.txt").getChannel()) {
// 获得缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
int hasNext = 0;
StringBuilder builder = new StringBuilder();
while((hasNext = channel.read(buffer)) > 0) {
// 切换模式 limit=position, position=0
buffer.flip();
// 当buffer中还有数据时,获取其中的数据
while(buffer.hasRemaining()) {
builder.append((char)buffer.get());
}
// 切换模式 position=0, limit=capacity
buffer.clear();
}
System.out.println(builder.toString());
} catch (IOException e) {
}
}
}
关键的几个步骤是
1、向 buffer 写入数据,例如调用 channel.read(buffer)
2、调用 flip() 切换至读模式,flip会使得buffer中的limit变为position,position变为0,调用 buffer.get()从 buffer 读取数据
3、调用 clear() 或者compact()切换至写模式,调用clear()方法时position=0,limit变为capacity,调用compact()方法时,会将缓冲区中的未读数据压缩到缓冲区前面
三、ByteBuffer的核心方法
1、put()方法
- put()方法可以将一个数据放入到缓冲区中。
- 进行该操作后,postition的值会+1,指向下一个可以放入的位置。capacity = limit ,为缓冲区容量的值。
2、flip()方法
- flip()方法会切换对缓冲区的操作模式,由写->读 / 读->写
- 进行该操作后
- 如果是写模式->读模式,position = 0 , limit 指向最后一个元素的下一个位置,capacity不变
- 如果是读->写,则恢复为put()方法中的值
3、get()方法
- get()方法会读取缓冲区中的一个值
- 进行该操作后,position会+1,如果超过了limit则会抛出异常
- 注意:get(i)方法不会改变position的值
4、rewind()方法
- 该方法只能在读模式下使用
- rewind()方法后,会恢复position、limit和capacity的值,变为进行get()前的值
5、clean()方法
- clean()方法会将缓冲区中的各个属性恢复为最初的状态,position = 0, capacity = limit
- 此时缓冲区的数据依然存在,处于“被遗忘”状态,下次进行写操作时会覆盖这些数据
mark()和reset()方法
- mark()方法会将postion的值保存到mark属性中
- reset()方法会将position的值改为mark中保存的值
compact()方法
此方法为ByteBuffer的方法,而不是Buffer的方法
- compact会把未读完的数据向前压缩,然后切换到写模式
- 数据前移后,原位置的值并未清零,写时会覆盖之前的值
clear() VS compact()
clear只是对position、limit、mark进行重置,而compact在对position进行设置,以及limit、mark进行重置的同时,还涉及到数据在内存中拷贝(会调用arraycopy)。所以compact比clear更耗性能。但compact能保存你未读取的数据,将新数据追加到为读取的数据之后;而clear则不行,若你调用了clear,则未读取的数据就无法再读取到了,所以需要根据情况来判断使用哪种方法进行模式切换。
四、粘包与半包
网络上有多条数据发送给服务端,数据之间使用 进行分隔,但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为:
Hello,nikou
I’m yumi
How are you?
变成了
Hello,nikou I’m y
umi How are you?
粘包的原因:发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去。
半包的原因:接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象。
解决的办法有很多,这里先看其中的一种,
1、通过get(index)方法遍历ByteBuffer,遇到分隔符时进行处理。注意:get(index)不会改变position的值。
记录该段数据长度,以便于申请对应大小的缓冲区;
将缓冲区的数据通过get()方法写入到target中;
2、调用compact方法切换模式,因为缓冲区中可能还有未读的数据。
public class ByteBufferDemo { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(32); // 模拟粘包+半包 buffer.put("Hello,world I'm Nyima Ho".getBytes()); // 调用split函数处理 split(buffer); buffer.put("w are you? ".getBytes()); split(buffer); } private static void split(ByteBuffer buffer) { // 切换为读模式 buffer.flip(); for(int i = 0; i < buffer.limit(); i++) { // 遍历寻找分隔符 // get(i)不会移动position if (buffer.get(i) == ' ') { // 缓冲区长度 int length = i+1-buffer.position(); ByteBuffer target = ByteBuffer.allocate(length); // 将前面的内容写入target缓冲区 for(int j = 0; j < length; j++) { // 将buffer中的数据写入target中 target.put(buffer.get()); } // 打印查看结果 ByteBufferUtil.debugAll(target); } } // 切换为写模式,但是缓冲区可能未读完,这里需要使用compact buffer.compact(); } }
参考连接:https://nyimac.gitee.io/2021/04/18/Netty%E5%AD%A6%E4%B9%A0%E4%B9%8BNIO%E5%9F%BA%E7%A1%80/