ByteBuffer的使用

  • 一、认识ByteBuffer
  • 二、ByteBuffer使用
  • 1、读取文本数据
  • 2、正确使用ByteBuffer
  • 3、ByteBuffer的结构
  • 4、ByteBuffer的常见方法
  • (1)分配空间
  • (2)向 buffer 写入数据
  • (3)从 buffer 读取数据
  • (4)mark 和 reset
  • 5、练习


一、认识ByteBuffer

Bytebuffer
官方解释A byte buffer,一个字节缓冲区。

Android byteBuffer读写切换 读取bytebuffer中数据_数据

Android byteBuffer读写切换 读取bytebuffer中数据_System_02

二、ByteBuffer使用

1、读取文本数据

有一普通文本文件 data.txt,内容为

1234567890abcd

使用 FileChannel 来读取文件内容

@Slf4j
public class ChannelDemo1 {
    public static void main(String[] args) {
        /**
         * FileChannel获取的方法 1、输入输出流 2、RandomAccessFile
         */
        try (RandomAccessFile file = new RandomAccessFile("data.txt", "rw")) {
            FileChannel channel = file.getChannel();
            // 准备一个缓冲区,大小可以自定义
            ByteBuffer buffer = ByteBuffer.allocate(10);
            do {
                // 从channel中读取数据,向缓冲区中写入
                int len = channel.read(buffer);
                log.debug("读到字节数:{}", len);
                if (len == -1) {
                    break;
                }
                // 打印读出的数据即buffer中的内容
                // 切换至ByteBuffer的读模式
                buffer.flip();
                while(buffer.hasRemaining()) {
                    log.debug("{}", (char)buffer.get());
                }
                // 切换 buffer 写模式
                buffer.clear();
            } while (true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

输出

10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:10
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 1
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 2
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 3
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 5
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 6
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 7
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 8
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 9
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 0
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - a
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - b
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - c
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - d
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:-1

2、正确使用ByteBuffer

1、 向 buffer 写入数据,例如调用 channel.read(buffer)
2、调用 flip() 切换至读模式
3、从 buffer 读取数据,例如调用 buffer.get()
4、 调用 clear() 或 compact() 切换至写模式
5、 重复 1~4 步骤

3、ByteBuffer的结构

ByteBuffer 有以下重要属性

  • capacity
  • position
  • limit

一开始

Android byteBuffer读写切换 读取bytebuffer中数据_System_03

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态

Android byteBuffer读写切换 读取bytebuffer中数据_System_04

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制

Android byteBuffer读写切换 读取bytebuffer中数据_bc_05

读取 4 个字节后,状态

Android byteBuffer读写切换 读取bytebuffer中数据_数据_06

clear 动作发生后,状态

Android byteBuffer读写切换 读取bytebuffer中数据_数据_07

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

Android byteBuffer读写切换 读取bytebuffer中数据_bc_08

4、ByteBuffer的常见方法

为了方便控制台输出和查看,提供了一个工具类,ByteBufferUtil,稍后会放在文章末尾。

(1)分配空间

/**
  * class java.nio.HeapByteBuffer Java堆内存,读写效率低,受到GC影响
  * class java.nio.DirectByteBuffer 直接内存,读写效率高(少一次拷贝),不会受到GC影响,
  *                                  分配的效率低,使用不当会造成内存泄漏
  */
  System.out.println(ByteBuffer.allocate(16).getClass());
  System.out.println(ByteBuffer.allocateDirect(16).getClass());

(2)向 buffer 写入数据

向buffer中写入数据可以使用以下两种方式:

  • 调用 channel 的 read 方法
  • 调用 buffer 自己的 put 方法
// 调用 channel 的 read 方法
 int readBytes = channel.read(buffer);
 // 调用 buffer 自己的 put 方法
 buffer.put((byte) 0x61);

如下例:

ByteBuffer buffer = ByteBuffer.allocate(10); // 创建一个ByteBuffer缓冲区
buffer.put((byte) 0x61); //写入单个字节数据
ByteBufferUtil.debugAll(buffer); //查看buffer结构的工具方法
buffer.put(new byte[]{0x62, 0x63}); //写入字节数组数据
ByteBufferUtil.debugAll(buffer); //查看buffer结构的工具方法

从下面的输出可以看到,第一次写入了一个a,在索引为0的地方;第二个写入了一个数组,两个字符 a和b,分别在下标0和1的地方。
同时也能看到buffer中的position和limit的值。

+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00                   |a.........      |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [3], limit: [10]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 00 00 00 00 00 00 00                   |abc.......      |
+--------+-------------------------------------------------+----------------+

Process finished with exit code 0

(3)从 buffer 读取数据

同样有两种办法

  • 调用 channel 的 write 方法
  • 调用 buffer 自己的 get 方法
int writeBytes = channel.write(buf);

byte b = buf.get();

get 方法会让 position 读指针向后走,如果想重复读取数据

  • 可以调用 rewind 方法将 position 重新置为 0
  • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

如下示例:

ByteBuffer buffer = ByteBuffer.allocate(10); // 创建一个buffer缓冲区
buffer.put(new byte[]{'a', 'b', 'c', 'd'}); // 向缓冲区输入四个字符
buffer.flip(); //转换为bytebuffer读模式

buffer.get(new byte[4]); //get,一次读取多个字节
ByteBufferUtil.debugAll(buffer); //查看buffer结构的工具方法
buffer.rewind(); //重置position
ByteBufferUtil.debugAll(buffer); //查看buffer结构的工具方法

执行结果如下:
可以看到,当get多个数据的时候,position的值会改变。当执行了rewind()方法后,position的值被重置为0。

+--------+-------------------- all ------------------------+----------------+
position: [4], limit: [4]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [4]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+

(4)mark 和 reset

mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置。

注意

rewind 和 flip 都会清除 mark 位置

如下示例:

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c', 'd'});
buffer.flip();
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
buffer.mark();
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
ByteBufferUtil.debugAll(buffer);
buffer.reset();
ByteBufferUtil.debugAll(buffer);

执行结果如下:
可以看到,当get了两个数据后,执行了mark()方法,然后再将最后的两个数据进行读取完毕,最后position的值为4,当执行过reset()方法之后,position的值变成了2。

a
b
c
d
+--------+-------------------- all ------------------------+----------------+
position: [4], limit: [4]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [2], limit: [4]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+

5、练习

网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为

  • Hello,world\n
  • I’m zhangsan\n
  • How are you?\n

变成了下面的两个 byteBuffer (黏包,半包)

  • Hello,world\nI’m zhangsan\nHo
  • w are you?\n

现在要求你编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据。

public static void main(String[] args) {
        /**
         * 网络上有多条数据发送给服务端,数据之间使用 \n 进行分割
         * 但是由于某种原因,数据在接收时,被进行了重新组合,例如:
         *  Hello World\n
         *  I'm zhangsan\n
         *  How are you\n
         *  变成了下面两个byteBuffer (黏包,半包)
         *  Hello World\n I'm zhangsan\nHo
         *  w are you\n
         *  现在要求将错乱的程序恢复原始的正常的数据
         */
        ByteBuffer source = ByteBuffer.allocate(32);
        source.put("HelloWorld\nI'm zhangsan\nHo".getBytes());
        split(source);
        source.put("w are you?\n".getBytes());
        split(source);
    }

    private static void split(ByteBuffer source) {
        // ByteBuffer切换到读模式
        source.flip();
        for (int i = 0; i < source.limit(); i++) {
            // 找到一条完整的消息
            if (source.get(i) == '\n') {
                int len = i + 1 - source.position();
                // 把这条消息完整的存入新的ByteBuffer
                ByteBuffer target = ByteBuffer.allocate(len);
                for (int j = 0; j < len; j++) {
                    target.put(source.get());
                }
                ByteBufferUtil.debugAll(target);
            }
        }
        // 标记当前位置,将后续字节整理后放在下次读取的数据前面
        source.compact();
    }

执行后的结果如下:

+--------+-------------------- all ------------------------+----------------+
position: [11], limit: [11]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 57 6f 72 6c 64 0a                |HelloWorld.     |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 7a 68 61 6e 67 73 61 6e 0a          |I'm zhangsan.   |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a          |How are you?.   |
+--------+-------------------------------------------------+----------------+