Java NIO ByteBuffer 使用方法

前言

最近在使用spring boot + websocket + xterm.js 给 k8s pod做了个在线的 web 终端,发现websocket的类核心方法,用的都是ByteBuffer传递数据,如下:

    @OnMessage
    public void onMessage(Session session, ByteBuffer byteBuffer) {
     //xxxxx
    }

以前只知道 NIO 里面大量用到了 ByteBuffer ,并没有仔细了解过,这次特意学习了一下,因为JDK自带的ByteBuffer 可以切换读写两种模式加上内置很多方法组合使用,有很多约定俗成的用法,稍不注意就有可能踩坑,这也是为什么Netty里面又基于 ByteBuffer 重新封装了ByteBuf类,就是因为 JDK 自带的太难用了

UML 图概览

解释:

Buffer 抽象类是所有 ByteBuffer 类的父类,其子类还有8种基本类型的IntBuffer,LongBuffer等,这不是我们这次的重点,我们这次主要关注 ByteBuffer 子类,如上图所示。

Buffer抽象类几个字段:

  • capacity:这个很好理解,它规定了整个 Buffer 的容量,具体可以容纳多少个元素。capacity 指针之前的元素均是 Buffer 可操作的空间。
  • position:用于指向 Buffer 中下一个可操作性的元素,初始值为 0。在 Buffer 的写模式下,position 指针用于指向下一个可写位置。在读模式下,position 指针指向下一个可读位置。
  • limit:表示 Buffer 可操作元素的上限。什么意思呢?比如在 Buffer 的写模式下,可写元素的上限就是 Buffer 的整体容量也就是 capacity ,capacity - 1 即为 Buffer 最后一个可写位置。在读模式下,Buffer 中可读元素的上限即为上一次 Buffer 在写模式下最后一个写入元素的位置。也就是上一次写模式中的 position。
  • mark:用于标记 Buffer 当前 position 的位置
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1; // 搭配 reset 使用
    private int position = 0; // 写模式下指向下一次写的位置,读模式下是当前要读数据的位置
    private int limit; 
    private int capacity;
    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

MappedByteBuffer : 映射 JVM 堆外内存,也就是这部分内存由 linux 内核管理,其中可映射文件,也可也直接在操作堆上分配空间。最常用的是:DirectByteBuffer ,DirectByteBufferR 代表只读视图

HeapByteBuffer : 在 JVM 堆内分配内存,HeapByteBufferR 代表只读视图

常用方法
put

这个比较简单,就是向 ByteBuffer 里面放入数据,例子如下:

    public static  void putData(){

        //默认声明出来的是写模式
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'s','h'});
        System.out.println(buffer);
        //java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]
    }
get

这个就要注意了,在没有切换成读模式下直接get是有问题的,除非指定 index 读

    public static  void getData(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'s','h'});
        System.out.println(buffer);
        System.out.println(buffer.position());
        System.out.println(buffer.get()); // 输出 0
        //在没有切换读模式下,get方法获取的是写 pos的值,也就是pos=2,所以读取不正确
        System.out.println(buffer.get(0)); // 输出 s
    }

此外,get方法也会使得 pos ++,所以几次get之后,在写数据就会出现空间空了几次:

    public static  void getData2(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'s','h'});
        System.out.println(buffer);
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        //在没有切换读模式下,get方法获取的是写 pos的值,也就是pos=2,所以读取不真确
        //java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=3 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=4 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=5 lim=16 cap=16]

        //
        buffer.put(new byte[]{'e'});
        System.out.println(buffer);
        //最终内存结果
        //[115, 104, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    }

获取最后一位数据:

    public static  void getData3(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'s','h'});
        System.out.println(buffer);
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        System.out.println(buffer.get()); // 输出 0
        System.out.println(buffer); // get 方法会导致pos指针++
        //在没有切换读模式下,get方法获取的是写 pos的值,也就是pos=2,所以读取不真确
        //java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=3 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=4 lim=16 cap=16]
        //0
        //java.nio.HeapByteBuffer[pos=5 lim=16 cap=16]

        //
        buffer.put(new byte[]{'e'});
        System.out.println(buffer);
        buffer.put(new byte[]{'\n'});
        //最终内存结果
        //[115, 104, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

        // 使用getIndex获取最后一位数据, 并且不会导致pos++


//        回车,ASCII码13
//        换行,ASCII码10
//        空格,ASCII码32

        // 输出10
        System.out.println(buffer.get(buffer.position()-1));


    }
flip

切换成读模式:limit和pos的值会自动适配变化,需要注意的是即使切换到读模式,你仍然可以写因为这不是强制的,但如果你切换成读模式后立马写数据,会覆盖掉第一位数据

    public static  void flip(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'t', 'o','m'});
        System.out.println(buffer);// java.nio.HeapByteBuffer[pos=3 lim=16 cap=16]
        buffer.flip();
        System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=3 cap=16]

        System.out.println((char)buffer.get());
        System.out.println((char)buffer.get());
        System.out.println(buffer);// java.nio.HeapByteBuffer[pos=2 lim=3 cap=16]

        buffer.put(new byte[]{'e'});

        System.out.println(buffer);
        //


    }

覆盖写例子:

    // 切换读模式
    public static  void flip2(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'t', 'o','m'});
        System.out.println(buffer);// java.nio.HeapByteBuffer[pos=3 lim=16 cap=16]
        buffer.flip();
        System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=3 cap=16]

        buffer.put(new byte[]{'e'});
        System.out.println(buffer); // java.nio.HeapByteBuffer[pos=1 lim=3 cap=16]
        System.out.println((char)buffer.get(0)); //写的数据覆盖了第一个t



    }
rewind

读模式下重置数据,从头开始读:

    public static void  rewind(){
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'t', 'o'});
        buffer.flip();
        System.out.println((char)buffer.get()); // t
        System.out.println((char)buffer.get()); // o

        // 从头开始读
        buffer.rewind();

        System.out.println((char)buffer.get()); // t
        System.out.println((char)buffer.get()); // o
    }
mark & reset

mark标记当前位置,继续读写后,然后reset可以重置到mark的位置,实现原理很简单就是用mark字段备份了原来pos的值:

    public static void markResetRead(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'t', 'o', 'm', 'c','a','t'});
        System.out.println(buffer);
        buffer.flip();
        System.out.println((char) buffer.get()); // t
        System.out.println((char) buffer.get()); // o
        // 控记住当前的 position
        buffer.mark();
        System.out.println((char) buffer.get()); // m
        System.out.println((char) buffer.get()); // c

        buffer.reset();
        System.out.println((char) buffer.get()); // m
        System.out.println((char) buffer.get()); // c
    }

写模式也可以:

    public static void markResetWrite(){

        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.mark();
        buffer.put(new byte[]{'a'});
        buffer.put(new byte[]{'b'});
        // 控记住当前的 position

        System.out.println((char) buffer.get(0)); // a
        System.out.println((char) buffer.get(1)); // b

        buffer.reset();
        buffer.put(new byte[]{'c'});
        buffer.put(new byte[]{'d'});

        System.out.println((char) buffer.get(0)); // c
        System.out.println((char) buffer.get(1)); // d

    }
clear

重置写模式,注意这个并没有删除旧数据,只是把pos位置置0:

   // 从头开始写覆盖数据
    public static void clear(){
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put(new byte[]{'a', 'b', 'c', 'd'});
        System.out.println(buffer);//java.nio.HeapByteBuffer[pos=4 lim=16 cap=16]
        buffer.clear();
        System.out.println(buffer);//java.nio.HeapByteBuffer[pos=0 lim=16 cap=16]

    }
compact

compact方法,主要是用来解决clear方法切换写模式后,总是从头开始的问题,因为切换为读的时候,大部分情况下可能只读一部分数据,然后就要切写模式,直接掉clear方法会覆盖掉一部分未读的数据,所以这个时候需要使用compact方法,将没读的部分移动到前面,然后将pos重置到下一个可覆盖写的地方

    public static void compact(){
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put("hadoop".getBytes(StandardCharsets.UTF_8));
        System.out.println(buffer);//java.nio.HeapByteBuffer[pos=6 lim=16 cap=16]
        // 切换到读模式
        buffer.flip();
        String v=StandardCharsets.UTF_8.decode(buffer).toString();
        System.out.println(v);//hadoop
        buffer.rewind(); //重置读
        System.out.println((char) buffer.get()); // h
        System.out.println(buffer);//java.nio.HeapByteBuffer[pos=1 lim=6 cap=16]
        // 数据读了一部分,这个时候使用clear切换写模式,会覆盖掉没读部分,所以得使用 compat 将没读过的数据, 移到 buffer 的首部
        buffer.compact(); // 此时 buffer 的数据就会变成 adoopp
        System.out.println(buffer);// java.nio.HeapByteBuffer[pos=5 lim=16 cap=16]
        buffer.rewind();
        String v1=StandardCharsets.UTF_8.decode(buffer).toString();
        System.out.println(v1);//adoopp
    }
hasRemaining

判断 pos 位置是否小于 limit,也就是是否达到buffer的上限

    public static  void remaining(){

        ByteBuffer buffer = ByteBuffer.allocate(2);
        buffer.put(new byte[]{'b'});
        System.out.println(buffer.hasRemaining()); //true
        buffer.put(new byte[]{'c'}); 
        System.out.println(buffer.hasRemaining()); //false // check position < limit;
    }
remaining

写模式下,判断剩余容量还有多少:

    public static void remaining(){
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put(new byte[]{'b'});
        buffer.put(new byte[]{'c'});

        System.out.println(buffer.remaining()); // 8
    }
其他方法:

isReadOnly: 判断是否是只读Buffer

isDirect: 是否从对外分配的内存空间

duplicate:

clone 原生 ByteBuffer。它们的 offset,mark,position,limit,capacity 变量的值全部是一样的,这里需要注意虽然值是一样的,但是它们各自之间是相互独立的。用于对同一字节数组做不同的逻辑时候需要

slice:

调用 slice() 方法创建出来的 ByteBuffer 视图内容是从原生 ByteBufer 的当前位置 position 开始一直到 limit 之间的数据。也就是说通过 slice() 方法创建出来的视图里边的数据是原生 ByteBuffer 中还未处理的数据部分,共享原生的数据,访问时需要带上 offset

总结

Java 中的 ByteBuffer 是 java.nio 包中的核心类之一,属于 New I/O (NIO) 框架。它提供了用于操作字节数据的丰富方法,ByteBuffer 在需要高效 I/O 操作的应用程序中非常有用,特别是在网络编程、文件 I/O、内存映射文件、以及其他需要直接操作字节数据的场景中。使用 ByteBuffer 可以带来更好的性能和灵活性。

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值