Java NIO Buffer
使用NIO进行文件读取所涉及的步骤:
- 从FileInputStream对象获取Channel对象
- 创建Buffer
- 将数据从Channel中读取到Buffer对象中
Java NIO 中,ByteBuffer容量大小及读写用到的变量:
0 <= mark <= position <= limit <= capacity
其中的flip()方法(在从Buffer读出数据前调用):
- 将limit值设置为当前的position
- 将position设置为0
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
clear方法(读完数据重新写入时调用):
- 将limit值设为capacity
- 将position设置为0
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact方法:
- 将所有未读的数据复制到buffer的起始位置
- 将position设为最后一个未读元素的后面
- 将limit设为capacity
- buffer设置好后不会覆盖未读的数据
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
compact方法在DirectBuffer和HeapBuffer中都有实现,关于DirectBuffer和HeapBuffer的区别可以看看这个链接
Netty ByteBuf
先简单看看Netty ByteBuf使用的示例代码:
public class ByteBufTest0 {
public static void main(String[] args) {
ByteBuf buffer = Unpooled.buffer(10);
for (int i = 0; i < 10; i++) {
buffer.writeByte(i);
}
for (int i = 0; i < 10; i++) {
System.out.println(buffer.getByte(i));
}
for (int i = 0; i < buffer.capacity(); i++) {
System.out.println(buffer.readByte());
}
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", Charset.forName("utf-8"));
if (byteBuf.hasArray()) {
byte[] content = byteBuf.array();
System.out.println(new String(content, Charset.forName("utf-8")));
System.out.println(byteBuf);
System.out.println("offset:" + byteBuf.arrayOffset());
System.out.println("readerIndex:" + byteBuf.readerIndex());
System.out.println("writerIndex:" + byteBuf.writerIndex());
System.out.println("capacity:" + byteBuf.capacity());
int length = byteBuf.readableBytes();
System.out.println("readableBytes:" + length);
for (int i = 0; i < length; i++) {
System.out.println((char)byteBuf.getByte(i));
}
}
}
}
在Java NIO Buffer中使用一个变量来控制数据的读和写,在Netty ByteBuf 中,分别使用readerIndex和writerIndex来控制读写。
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
ByteBuf是抽象类,主要的方法实现在抽象子类AbstractByteBuf中,简单看两个方法实现:
//返回可读的字节数
public int readableBytes() {
return writerIndex - readerIndex;
}
//返回可写的字节数
public int writableBytes() {
return capacity() - writerIndex;
}
//读一个字节数
public byte readByte() {
checkReadableBytes0(1);
int i = readerIndex;
byte b = _getByte(i);
readerIndex = i + 1;
return b;
}
通过索引来访问Byte时并不会改变真实的读写索引,我们可以通过ByteBuf的readerIndex()与writerIndex()方法直接修改读写索引。
Netty ByteBuf 提供三种缓冲区类型:
- heap buffer
- direct buffer
- composite buffer
Heap Buffer(堆缓冲区)
这时最常用的类型,ByteBuf将数据存储到JVM的堆空间中,实际数据存储在byte array中来实现。
优点:由于数据时存储在JVM堆中,因此可以快速的创建和释放,并且提供直接访问内部字节数组的方法。
缺点:每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网络传输。
Direct Buffer(直接缓冲区)
优点:在使用Socket进行数据传输时,性能非常好,因为数据直接位于操作系统的本地内存中,不需要从JVM复制到直接缓冲区这一步。
缺点: 因为Direct Buffer是直接在操作系统内存中,内存的分配和释放比堆空间复杂,而且速度慢一些。
Netty提供内存池来解决这个问题。直接缓冲区不支持通过数组的方式访问数据。
对于后端的业务消息编解码来说,推荐使用HeapByteBuf。对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf.
JDK的Byte Buffer与Netty的ByteBuf之间的差异比对:
- Netty的ByteBuf采用了读写索引分离的策略(readerIndex与writerIndex), 一个初始化(里面尚未有数据)的ByteBuf的readerIndex与writerIndex值都为0。
- 当读索引与写索引处于同一个位置时,如果我们继续读取,那么就会抛出IndexOutOfBoundsException。
- 对于ByteBuf的任何读写操作都会分别单独维护读索引与写索引,maxCapacity最大容量默认的限制就是Integer.MAX_VALUE。