05-NIO Buffer

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的更多内容在后面文章中学习分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值