文章目录
NIO Buffer
一、Buffer
- NIO 中,对于连接数据的读写不能直接操作 Channel,而需要操作Buffer,Buffer本质是一块内存区域,我们用其做数据的读写。JDK将这块内存封装成 NIO Buffer 对象,并提供了一组API方法,方便我们对该块内存的读写。Buffer 是 java.nio 包下的抽象类,常见实现类继承关系如下:
- JDK 中针对每一种基本类型都有Buffer的实现(除了boolean类型),但是仔细看每一种实现都还是抽象类,比如 ByteBuffer 也是一个抽象类,它也有不同的子类,我们针对它来看整个继承和主要的源码,其他类型也是基本类似。
二、主要属性
- mark 、position 、 limit 、capacity
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int capacity;
private int limit;
private int position = 0;
private int mark = -1;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
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;
}
}
}
- 构造方法:用于初始化几个核心参数
- capacity:容量;buffer能够容纳元素的最大值,创建时被赋值且不能被修改
- limit:上限;当前操作的上限,如果是写模式则limit是capacity,如果是读模式,limit是buffer中元素的个数,flip()方法切换读写模式
- position:位置;初始化为0,读写操作下,每操作一次之后,position值加一,类似于index,指向下一次操作的下标位置
- mark:标记;标记上一次读/写的位置,通过mark() 方法标记,通过reset()恢复标记,即reset()方法会将position恢复为上一次mark()标记的地方
- address:直接内存的时候该字段才有用,记录在顶层抽象类而不是子类目的是提高速度,可以理解为指向直接内存的地址
属性满足 : mark <= position <= limit <= capacity
三、主要方法
3.1 创建Buffer
- 创建Buffer 使用一个静态方法allocate,这个方法不在 Buffer 中定义,定义在子类中,比如ByteBuffer中:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
- 这里实例化的是一个 HeapByteBuffer,从名字容易知道是创建在堆的ByteBuffer实现,另外也可以分配在直接内存,使用直接内存不会受到JVM堆大小的限制了(受物理内存大小限制);
3.2 读写Buffer、flip、rewind
- 写buffer示例,通过put相关的重载方法往buffer写数据,注意在 NIO 中当从通道读取数据的时候,对Buffer就是写,比如远端发送了数据来了,从channel读取数据,此时就是将channel的数据写入Buffer
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}
//在Channel中,channel的读就是buffer的写
SocketChannel#read(java.nio.ByteBuffer)
- 读buffer示例, 通过get相关重载方法从buffer中读取数据到目标数组中 , 当往远端写数据,比如写一个消息给对方,那么就是将buffer数据写到channel,此时对于Buffer就是读,因此这个读取方法是从Buffer读到Channel继而发送,对于Channel是写,因此方法在Channel里面
buf.flip();
buf.get(dst);
//在Channel中,channel的写就是buffer的读
java.nio.channels.SocketChannel#write(java.nio.ByteBuffer)
-
这里需要注意的是 Channel 的读写方法都是在 SocketChannel 中,ServerSocketChannel 中是没有的,因为 ServerSocketChannel 是监听连接的,真正的读写在 SocketChannel 中处理,这也是二者的区别,上一篇 Channel的文章也讲过了。
-
flip切换写模式为读模式
public final Buffer flip() {
limit = position; //原本写的当前位置变成了读模式的limit
position = 0; //切换到读模式时,从0开始读
mark = -1; //还未标记
return this;
}
- flip() 使用示例
out.write(buf);
buf.rewind();
buf.get(array);
- rewind 重置 position,重新进行读写操作, 一般用于读模式,重新读取
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
3.4 其他方法
3.4.1 mark和reset
- mark 方法标记当前的位置
public final Buffer mark() {
mark = position;
return this;
}
- reset 将当前的position 恢复到之前标记的位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
3.4.2 clear和remaining
- clear 方法看起来是清空Buffer,其实不是的,它并未真正的清除数据,而是将几个标志复位,position置0,limit置为capacity,丢弃mark(置为-1),一般在使用前调用一次
/**
* Clears this buffer. The position is set to zero, the limit is set to
* the capacity, and the mark is discarded.
* buf.clear(); // Prepare buffer for reading
* in.read(buf); // Read data</pre></blockquote>
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
- clear() 使用示例
buf.put(magic);
in.read(buf);
buf.flip();
out.write(buf);
- remaining返回剩余可操作的元素数量,比如写就返回还能写的空间大小,读就返回剩余元素个数,通过limit和position计算得到
public final int remaining() {
return limit - position;
}
四、ByteBuffer
4.1 构造方法
- Buffer 几乎所有的子类都有堆分配和直接内存分配两种策略,ByteBuffer 用于存放字节类型数据
@Test
public void typeTest() {
//分配直接内存
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
System.out.println(direct.isDirect());
ByteBuffer heap = ByteBuffer.allocate(1024);
System.out.println(heap.isDirect());
}
输出:
true
false
- 默认返回的是 DirectByteBuffer 对象,DirectByteBuffer是包可访问权限(默认),下面是构造方法代码,最关键的地方看到是通过 unsafe.allocateMemory(size); 分配内存,这里涉及到本地方法调用,在直接内存分配
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
4.2 方法示例
- 下面通过一个例子来演示常用 API,基本涉及到前面所说的 Buffer 接口的方法
static final String content = "helloworld";
static final int capacity = 128;
@Test
public void apiTest() {
//1. 分配一个指定大小的缓冲区
System.out.println("1. allocate() 创建, 容量为 " + capacity);
ByteBuffer buf = ByteBuffer.allocate(capacity);
printBufDetail(buf);
//2. 利用 put() 存入数据到缓冲区中
System.out.println("2. put() 存储 :存入内容为:" + content);
buf.put(content.getBytes());
printBufDetail(buf);
//3. 切换读取数据模式
System.out.println("3.flip() 切换到读模式:");
buf.flip();
printBufDetail(buf);
//4. 利用 get() 读取缓冲区中的数据到dst数组
System.out.println("4. get() 读取");
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println("读取的内容:" + new String(dst, 0, dst.length));
printBufDetail(buf);
//5. rewind() : 重复读
System.out.println("5. rewind() 可重复读");
buf.rewind();
printBufDetail(buf);
//6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,标志状态位会重置
System.out.println("6. clear()");
buf.clear();
//注意这里如果get方法带有index,比如get(1)读取下标为1的内容,那么是不会影响position的,这种是绝对读取
System.out.println("clear 后读取第一个字符: " + (char) buf.get());
printBufDetail(buf);
System.out.println("7.重新写入javahelloworld ");
buf.put("javahelloworld".getBytes());
printBufDetail(buf);
buf.flip();
System.out.println("8.flip ");
printBufDetail(buf);
System.out.println("9.绝对读取1个,不影响position ");
byte b = buf.get(3);
printBufDetail(buf);
System.out.println("10.读取批量5个 ");
byte[] dst1 = new byte[5];
buf.get(dst1, 0, dst1.length);
System.out.println("读取内容:" + new String(dst1));
printBufDetail(buf);
System.out.println("11.mark一下,再读取5个 ");
buf.mark();
buf.get(dst1, 0, 5);
System.out.println("读取内容:" + new String(dst1));
printBufDetail(buf);
System.out.println("12.回到mark处,重新读取5个,应该和前一次读取的是一样的 ");
buf.reset();
buf.get(dst1, 0, 5);
System.out.println("读取内容:" + new String(dst1));
printBufDetail(buf);
}
private static void printBufDetail(ByteBuffer buf) {
System.out.println("Buffer 参数:position = " + buf.position() + ", limit= " + buf.limit() + ", capacity = " + buf.capacity() + "\n");
}
- 输出:双斜杠后面的注释是后面添加的说明部分
1. allocate() 创建, 容量为 128
Buffer 参数:position = 0, limit= 128, capacity = 128 //新创建的Buffer
2. put() 存储 :存入内容为:helloworld
Buffer 参数:position = 10, limit= 128, capacity = 128 //存入了10字节的helloworld后,写模式下position是10
3.flip() 切换到读模式:
Buffer 参数:position = 0, limit= 10, capacity = 128 //切换读模式,position是0,limit是可读限制 10
4. get() 读取
读取的内容:helloworld
Buffer 参数:position = 10, limit= 10, capacity = 128 //读取全部内容后,还是读模式,position还在10
5. rewind() 可重复读
Buffer 参数:position = 0, limit= 10, capacity = 128 //rewind将position置为0,还在读模式,重新读取,limit不变,
6. clear()
clear 后读取第一个字符: h
Buffer 参数:position = 1, limit= 128, capacity = 128 //clear没有真正清除数据,但是重置了参数,读取一个元素之后position是1
7.重新写入 javahelloworld
Buffer 参数:position = 15, limit= 128, capacity = 128
//高亮1:这里尤其注意,在读模式下position在1,因此直接写入javahelloworld会从1开始写入,写入之后buf中内容是hjavahelloworld,后面的覆盖了,第一个h保留了
8.flip
Buffer 参数:position = 0, limit= 15, capacity = 128 //flip切换之后limit是15,实际上javahelloworld长度是14,因为写入的时候从1位置开始写导致总长度是15
9.绝对读取1个,不影响position
Buffer 参数:position = 0, limit= 15, capacity = 128 //高亮2:绝对读取不影响position,绝对读取就是get(index)这种方式,读取之后position还是0
10.读取批量5个
读取内容:hjava
Buffer 参数:position = 5, limit= 15, capacity = 128 //批量读取5个,前面说过内容是hjavahelloworld,因此读取到的是 hjava
11.mark一下,再读取5个
读取内容:hello
Buffer 参数:position = 10, limit= 15, capacity = 128 //在前一次读取的时候mark,但是mark不影响后一次读取,所以再读5个,读取到的是 hello
12.回到mark处,重新读取5个,应该和前一次读取的是一样的 //回到mark处,也就是第一次读取完毕的时候mark的地方,因此再读,就和mark后的一次读取内容一样,也是 hello
读取内容:hello
Buffer 参数:position = 10, limit= 15, capacity = 128
- 通过这些例子其实发现 ByteBuffer 本身完全可以理解为一个可复用的定长字节数组的封装,通过给定的API来操作,在读写的时候相关的几个参数会变化(pisition,limit),其他类型的Buffer也可以同样类比理解;
- 需要注意的一个是绝对读取不影响position的,另外读取到一定位置再写入,则会从position位置开始写,有时候会产生奇怪的现象,需要注意,从下面的方法也能看出来;
//ByteBuffer底层写入方法
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
//从position后面开始写入
System.arraycopy(src, offset, hb, ix(position()), length);
position(position() + length);
return this;
}
五、Buffer到Netty ByteBuf
-
Netty 并未直接使用 JDK 的 Buffer,而是自定义了ByteBuf,在ByteBuf中定义了比JDK Buffer更加友好的API和使用方式,比如ByteBuf中包括readerIndex 和 writerIndex 两个属性来读写,避免出现读模式和写模式的切换等。(0 <= readerIndex <= writerIndex <= capacity)
-
关于ByteBuf的更多内容在后面文章中学习分析