ByteBuf
Java ByteBuffer
Java NIO支持的字节缓冲区
- HeapByteBuffer:在jvm堆上面的一个buffer,底层的本质是一个数组。由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且,可以更容易回收。
- DirectByteBuffer:底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据。跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy。题外:外设之所以要把jvm堆里的数据copy出来再操作,不是因为操作系统不能直接操作jvm内存,而是因为jvm在进行gc(垃圾回收)时,会对数据进行移动,一旦出现这种问题,外设就会出现数据错乱的情况
// 分配HeapByteBuffer的方法是:
//参数大小为字节的数量
ByteBuffer.allocate(int capacity);
// 分配DirectByteBuffer的方法是:
/** 可以看到分配内存是通过unsafe.allocateMemory()来实现的,
* 这个unsafe默认情况下java代码是没有能力可以调用到的,
* 不过你可以通过反射的手段得到实例进而做操作,当然你
* 需要保证的是程序的稳定性,既然叫unsafe的,就是告诉你
* 这不是安全的,其实并不是不安全,而是交给程序员来操作,
* 它可能会因为程序员的能力而导致不安全,而并非它本身不安全。
*/
ByteBuffer.allocateDirect(int capacity);
ByteBuffer 属性
byte[] buff //buff即内部用于缓存的数组。
position //当前读取的位置。
mark //为某一读过的位置做标记,便于某些时候回退到该位置。
capacity //初始化时候的容量。
limit //当写数据到buffer中时,limit一般和capacity相等,当读数据时,limit代表buffer中有效数据的长度。
0 <= mark <= position <= limit <= capacity
ByteBuffer 常规方法
ByteBuffer allocate(int capacity) //创建一个指定capacity的ByteBuffer。
ByteBuffer allocateDirect(int capacity) //创建一个direct的ByteBuffer,这样的ByteBuffer在参与IO操作时性能会更好
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length) //把一个byte数组或byte数组的一部分包装成ByteBuffer。
//get put方法不多说
byte get(int index)
ByteBuffer put(byte b)
int getInt() //从ByteBuffer中读出一个int值。
ByteBuffer putInt(int value) // 写入一个int值到ByteBuffer中。
// 特殊方法
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 之间没有数据的话发,就不会进行复制。
mark() & reset() //通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
- put
写模式下,往buffer里写一个字节,并把postion移动一位。写模式下,一般limit与capacity相等。 - flip
写完数据,需要开始读的时候,将postion复位到0,并将limit设为当前postion。 - get
从buffer里读一个字节,并把postion移动一位。上限是limit,即写入数据的最后位置。 - clear
将position置为0,并不清除buffer内容。 - mark & reset
mark相关的方法主要是mark()(标记)和reset()(回到标记)。
Netty ByteBuf
因为Java NIO的ByteBuffer使用复杂,netty对其重新封装了一层,即ByteBuf。
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 ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}.
ByteBuf 优点
- 它可以被用户自定义的缓冲区类型扩展;
- 通过内置的复合缓冲区类型实现了透明的零拷贝;
- 容量可以按需增长(类似于JDK的StringBuilder);
- 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
- 读和写使用了不同的索引;
- 支持方法的链式调用;
- 支持引用计数;
- 支持池化。
ByteBuf 注意点
- ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当从 ByteBuf 读取时,它的 readerIndex 将会被递增已经被读取的字节数。同样地,当写入 ByteBuf 时,它的 writerIndex 也会被递增。
- 当读取字节,readerIndex 达到 writerIndex 后继续读取,会触发 IndexOutOfBoundsException。名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或者 get 开头的操作则不会。
- 可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触发一个异常。(默认的限制是 Integer.MAX_VALUE。)
堆缓冲区
最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中,这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。
/**
* 支撑数组
*/
ByteBuf heapBuf = ...;
// 检查ByteBuf是否有一个支撑数组
if (heapBuf.hasArray()) {
// 如果有,则获取对该数组的引用
byte[] array = heapBuf.array();
// 计算第一个字节的偏移量
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
// 获得可读字节数
int length = heapBuf.readableBytes();
// 使用数组、偏移量和长度作为参数调用方法
handleArray(array, offset, length);
}
// 当 hasArray()方法返回 false 时,尝试访问支撑数组将触发一个 UnsupportedOperationException。
// 这个模式类似于 JDK 的 ByteBuffer 的用法。
直接缓冲区
ByteBuffer 的 Javadoc 明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。”这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把堆缓冲区复制到一个直接缓冲区中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它的分配和释放都较为昂贵。
/**
* 访问直接缓冲区的数据
*/
ByteBuf directBuf = ...;
// 检查ByteBuf是否由数组支撑。如果不是,则是这个是一个直接缓冲区
if (!directBuf.hasArray()) {
// 获取可读字节数
int length = directBuf.readableBytes();
// 分配一个新的数组来保存具有该刻度的字节数据
byte[] array