Java NIO Buffer与Netty ByteBuf

739 阅读3分钟
Java NIO Buffer

使用NIO进行文件读取所涉及的步骤:

  1. 从FileInputStream对象获取Channel对象
  2. 创建Buffer
  3. 将数据从Channel中读取到Buffer对象中

Java NIO 中,ByteBuffer容量大小及读写用到的变量:
0 <= mark <= position <= limit <= capacity

其中的flip()方法(在从Buffer读出数据前调用):

  1. 将limit值设置为当前的position
  2. 将position设置为0
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

clear方法(读完数据重新写入时调用):

  1. 将limit值设为capacity
  2. 将position设置为0
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

compact方法:

  1. 将所有未读的数据复制到buffer的起始位置
  2. 将position设为最后一个未读元素的后面
  3. 将limit设为capacity
  4. 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 提供三种缓冲区类型:

  1. heap buffer
  2. direct buffer
  3. 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之间的差异比对:

  1. Netty的ByteBuf采用了读写索引分离的策略(readerIndex与writerIndex), 一个初始化(里面尚未有数据)的ByteBuf的readerIndex与writerIndex值都为0。
  2. 当读索引与写索引处于同一个位置时,如果我们继续读取,那么就会抛出IndexOutOfBoundsException。
  3. 对于ByteBuf的任何读写操作都会分别单独维护读索引与写索引,maxCapacity最大容量默认的限制就是Integer.MAX_VALUE。