前言
本篇博文是《从0到1学习 Netty》系列的第一篇博文,次要内容是介绍 NIO 的外围之一 Buffer 中的 ByteBuffer,往期系列文章请拜访博主的 Netty 专栏,博文中的所有代码全副收集在博主的 GitHub 仓库中;
什么是 Netty?
Netty 是一个高性能、异步事件驱动的网络应用程序框架,次要用于疾速开发可保护、可扩大的高性能服务器和客户端。Netty 提供了简略易用的 API,反对多种协定和传输方式,并且有着高度灵便的扩大和自定义能力。
Netty 的设计指标是提供一种易于应用、高效、可扩大的异步 IO 网络编程框架。它采纳了 NIO(Non-blocking IO)的形式来进行网络操作,防止了传统的阻塞式 IO 经常面临的性能瓶颈。同时,Netty 还提供了优良的线程模型和内存管理机制,保障了高并发下的稳定性和性能。
通过 Netty,开发者能够不便地实现基于 TCP、UDP、HTTP、WebSocket 等多种协定的通信利用。同时,Netty 还提供了编解码器、SSL 反对等组件,使得开发者能够更加专一于业务逻辑的实现。
什么是 ByteBuffer?
ByteBuffer 是 Java 中的一个类,它提供了一种不便的形式来解决原始字节数据。ByteBuffer 能够被看作是一个缓冲区,它能够包容肯定数量的字节数据,并提供了一系列办法来操作这些数据。
应用 ByteBuffer,能够轻松地读取和写入二进制数据。它还提供了对不同类型数据的反对,如整数、浮点数等。ByteBuffer 还反对对数据进行切片,以及对缓冲区中的数据进行复制、压缩、解压等操作。
在 Java 中,ByteBuffer 通常用于解决 I/O 操作,例如从文件或网络中读取和写入数据。它也能够用于解决加密和解密数据,以及解决图像和音频文件等二进制数据。总之,ByteBuffer 是 Java 中十分有用的一个类,能够帮忙开发人员更轻松地解决二进制数据。
根本应用
- 向 buffer 写入数据,例如调用
channel.read(buffer)
; 调用
flip()
切换至读模式;flip
会使得 buffer 中的 limit 变为 position,position 变为 0;
- 从 buffer 读取数据,例如调用
buffer.get()
; 调用
clear()
或者compact()
切换至写模式;- 调用
clear()
办法时,position=0
,limit 变为 capacity; - 调用
compact()
办法时,会将缓冲区中的未读数据压缩到缓冲区后面;
- 调用
- 反复 1~4 的步骤;
编写代码进行测试:
@Slf4jpublic class TestByteBuffer { public static void main(String[] args) { try (FileChannel channel = new FileInputStream("data.txt").getChannel()) { // 筹备缓冲区 ByteBuffer buffer = ByteBuffer.allocate(10); while (true) { // 从 channel 读取数据写入到 buffer int len = channel.read(buffer); log.debug("读取到的字节数 {}", len); if (len == -1) break; // 打印 buffer 内容 buffer.flip(); // 切换至读模式 while(buffer.hasRemaining()) { // 是否还有残余未读数据 byte b = buffer.get(); log.debug("理论字节 {}", (char)b); } buffer.clear(); } } catch (IOException e) { } }}
运行后果:
留神,日志须要进行配置,在 /src/main/resources/
门路下,创立 logback.xml
,其中的内容如下:
<?xml version="1.0" encoding="utf-8" ?><configuration xmlns="http://ch.qos.logback/xml/ns/logback" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd"> <!-- 输入管制,格局管制 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date{HH:mm:ss} [%-5level] [%thread] %logger{17} - %m%n </pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 日志文件名称 --> <file>logFile.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 每天产生一个新的日志文件 --> <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 保留 15 天的日志 --> <maxHistory>15</maxHistory> </rollingPolicy> <encoder> <pattern>%date{HH:mm:ss} [%-5level] [%thread] %logger{17} - %m%n </pattern> </encoder> </appender> <!-- 用来管制查看哪个类的日志内容(对 mybatis name 代表命名空间)--> <logger name="com.sidiot.netty" level="DEBUG" additivity="false"> <appender-ref ref="STDOUT" /> </logger> <root level="ERROR"> <appender-ref ref="STDOUT" /> </root></configuration>
将开端局部的 <logger name="com.sidiot.netty" level="DEBUG" additivity="false">
中的 name
的属性值改成本人的包名即可。
局部读者可能会遇到如下问题:
这是因为 lombok
引起的,须要检查一下是否装置了 lombok
的插件,以及是否是最新版的 lombok
,博主这里用的版本如下:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.26</version> </dependency>
内部结构
字节缓冲区的父类 Buffer 中有四个外围属性,从以下源码中能够清晰获知:
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
- position:示意以后缓冲区中下一个要被读或写的字节索引地位,默认值为 0。当咱们调用
put()
办法往缓冲区中写入数据时,position 会主动向后挪动,指向下一个可写的地位;当咱们调用get()
办法从缓冲区中读取数据时,position 也会主动向后挪动,指向下一个可读的地位。 - limit:示意以后缓冲区的限度大小,默认值为 capacity。在写模式下,limit 示意缓冲区最多可能写入的字节数;在读模式下,limit 示意缓冲区最多可能读取的字节数。在一些场景下,咱们能够通过设置 limit 来避免越界拜访缓冲区。
- capacity:示意缓冲区的容量大小,默认创立 Buffer 对象时指定。capacity 只能在创立缓冲区时指定,并且不能扭转。例如,咱们能够创立一个容量为 1024 字节的 Buffer 对象,而后往里面写入不超过 1024 字节的数据。
- mark:mark 和 reset 办法一起应用,用于记录和复原 position 的值。在 ByteBuffer 中,咱们能够通过调用
mark()
办法来记录以后 position 的值,而后随便挪动 position,最初再通过调用reset()
办法将 position 复原到 mark 记录的地位。应用 mark 和 reset 能够在某些状况下进步代码的效率,防止频繁地从新计算或查问某个值。
这些属性一起组成了 Buffer 的状态,咱们能够依据它们的值来确定以后缓冲区的状态和可操作范畴。
初始化时,position
,limit
,capacity
的地位如下:
写模式下,position
代表写入地位,limit
代表写入容量,写入3个字节后的状态如下图所示:
当应用 flip()
函数切换至读模式后,position
切换为读取地位,limit
切换为读取限度:
这个变换也能够从 flip()
的源码清晰的获知:
public Buffer flip() { limit = position; position = 0; mark = -1; return this;}
当读完之后,应用 clean()
函数清空缓存区,可从源码获知,缓冲区又变成了初始化时的状态:
public Buffer clear() { position = 0; limit = capacity; mark = -1; return this;}
这里还有一种办法 compact()
,其作用是将未读完的局部向前压缩,而后切换至写模式,不过须要留神的是,这是 ByteBuffer
中的办法:
接下来,将要联合代码对上述内容进行深刻了解;
这里用到了一个自定义的工具类 ByteBufferUtil
,因为篇幅起因,自行从我的 Github 上进行获取: ByteBufferUtil.java;
编写一个测试类,对 ByteBuffer
的罕用办法进行测试:
public class TestByteBufferReadWrite { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(10); // 写入一个字节的数据 buffer.put((byte) 0x73); debugAll(buffer); // 写入一组五个字节的数据 buffer.put(new byte[]{0x69, 0x64, 0x69, 0x6f, 0x74}); debugAll(buffer); // 获取数据 buffer.flip(); ByteBufferUtil.debugAll(buffer); System.out.println((char) buffer.get()); System.out.println((char) buffer.get()); ByteBufferUtil.debugAll(buffer); // 应用 compact 切换写模式 buffer.compact(); ByteBufferUtil.debugAll(buffer); // 再次写入 buffer.put((byte) 102); buffer.put((byte) 103); ByteBufferUtil.debugAll(buffer); }}
运行后果:
// 向缓冲区写入了一个字节的数据,此时 postition 为 1;+--------+-------------------- all ------------------------+----------------+position: [1], limit: [10] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 00 00 00 00 00 00 00 00 00 |s......... |+--------+-------------------------------------------------+----------------+// 向缓冲区写入了五个字节的数据,此时 postition 为 6;+--------+-------------------- all ------------------------+----------------+position: [6], limit: [10] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 |sidiot.... |+--------+-------------------------------------------------+----------------+// 调用 flip() 切换至读模式,此时 position 为 0,示意从第 0 个数据开始读取;// 同时要留神,此时的 limit 为 6,示意 position=6 时内容就读完了;+--------+-------------------- all ------------------------+----------------+position: [0], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 |sidiot.... |+--------+-------------------------------------------------+----------------+// 读取两个字节的数据;si// 此时 position 变为 2; +--------+-------------------- all ------------------------+----------------+position: [2], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 |sidiot.... |+--------+-------------------------------------------------+----------------+// 调用 compact() 切换至写模式,此时 position 及其前面的数据被压缩到 ByteBuffer 的后面;// 此时 position 为 4,会笼罩之前的数据; +--------+-------------------- all ------------------------+----------------+position: [4], limit: [10] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 64 69 6f 74 6f 74 00 00 00 00 |diotot.... |+--------+-------------------------------------------------+----------------+// 再次写入两个字节的数据,之前的 0x6f 0x74 被笼罩; +--------+-------------------- all ------------------------+----------------+position: [6], limit: [10] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 64 69 6f 74 66 67 00 00 00 00 |diotfg.... |+--------+-------------------------------------------------+----------------+Process finished with exit code 0
空间调配
在上述内容中,咱们应用 allocate()
办法来为 ByteBuffer 调配空间,当然还有其余办法也能够为 ByteBuffer 调配空间;
public class TestByteBufferAllocate { public static void main(String[] args) { System.out.println(ByteBuffer.allocate(16).getClass()); System.out.println(ByteBuffer.allocateDirect(16).getClass()); /* class java.nio.HeapByteBuffer - java 堆内存, 读写效率低, 受垃圾回收 GC 的影响; class java.nio.DirectByteBuffer - 间接内存,读写效率高(少一次拷贝),不会受 GC 的影响; - 应用完后 须要彻底的开释,免得内存泄露; */ }}
写入数据
- 调用
channel
的read()
办法:channel.read(buf)
; - 调用
buffer
的put()
办法:buffer.put((byte) 127)
;
读取数据
rewind
public Buffer rewind() { position = 0; mark = -1; return this;}
rewind()
的作用是将 position
设置为0,这意味着下一次读取或写入操作将从缓冲区的结尾开始。
@Testpublic void testRewind() { // rewind 从头开始读 ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'i', 'd', 'i', 'o', 't'}); buffer.flip(); buffer.get(new byte[6]); debugAll(buffer); buffer.rewind(); System.out.println((char) buffer.get());}
运行后果:
+--------+-------------------- all ------------------------+----------------+position: [6], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 00 00 00 00 00 00 |sidiot..........|+--------+-------------------------------------------------+----------------+// 从头读到第一个字符 's';sProcess finished with exit code 0
mark
和 reset
public Buffer mark() { mark = position; return this;}
mark()
用于在缓冲区中设置标记;
public Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this;}
reset()
用于返回到标记地位;
@Testpublic void testMarkAndReset() { // mark 做一个标记,用于记录 position 的地位;reset 是将 position 重置到 mark 的地位; ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'i', 'd', 'i', 'o', 't'}); buffer.flip(); System.out.println((char) buffer.get()); System.out.println((char) buffer.get()); buffer.mark(); // 增加标记为索引2的地位; System.out.println((char) buffer.get()); System.out.println((char) buffer.get()); debugAll(buffer); buffer.reset(); // 将 position 重置到索引2; debugAll(buffer); System.out.println((char) buffer.get()); System.out.println((char) buffer.get());}
运行后果:
sidi+--------+-------------------- all ------------------------+----------------+position: [4], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 00 00 00 00 00 00 |sidiot..........|+--------+-------------------------------------------------+----------------+// position 从4重置为2;+--------+-------------------- all ------------------------+----------------+position: [2], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 00 00 00 00 00 00 |sidiot..........|+--------+-------------------------------------------------+----------------+diProcess finished with exit code 0
get(i)
get(i)
不会扭转读索引的地位;
@Testpublic void testGet_i() { // get(i) 不会扭转读索引的地位; ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'i', 'd', 'i', 'o', 't'}); buffer.flip(); System.out.println((char) buffer.get(2)); debugAll(buffer);}
运行后果:
d+--------+-------------------- all ------------------------+----------------+position: [0], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 00 00 00 00 00 00 |sidiot..........|+--------+-------------------------------------------------+----------------+Process finished with exit code 0
字符串与 ByteBuffer 的互相转换
getBytes
public byte[] getBytes() { return StringCoding.encode(coder(), value);}
字符串调用 getByte()
办法取得 byte
数组,将 byte
数组放入 ByteBuffer 中:
@Testpublic void testGetBytes() { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put("sidiot".getBytes()); debugAll(buffer);}
运行后果:
+--------+-------------------- all ------------------------+----------------+position: [6], limit: [16] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 00 00 00 00 00 00 00 00 00 00 |sidiot..........|+--------+-------------------------------------------------+----------------+Process finished with exit code 0
charset
public final ByteBuffer encode(String str) { return encode(CharBuffer.wrap(str));}
通过 StandardCharsets
的 encode()
办法取得 ByteBuffer,此时取得的 ByteBuffer 为读模式,无需通过 flip()
切换模式:
@Testpublic void testCharset() { ByteBuffer buffer = StandardCharsets.UTF_8.encode("sidiot"); debugAll(buffer); System.out.println(StandardCharsets.UTF_8.decode(buffer));}
运行后果:
+--------+-------------------- all ------------------------+----------------+position: [0], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 |sidiot |+--------+-------------------------------------------------+----------------+sidiotProcess finished with exit code 0
wrap
public static ByteBuffer wrap(byte[] array, int offset, int length){ try { return new HeapByteBuffer(array, offset, length, null); } catch (IllegalArgumentException x) { throw new IndexOutOfBoundsException(); }}
将字节数组传给 wrap()
办法,通过该办法取得 ByteBuffer,此时的 ByteBuffer 同样为读模式:
@Testpublic void testWrap() { ByteBuffer buffer = ByteBuffer.wrap("sidiot".getBytes()); debugAll(buffer);}
运行后果:
+--------+-------------------- all ------------------------+----------------+position: [0], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 |sidiot |+--------+-------------------------------------------------+----------------+Process finished with exit code 0
后记
以上就是 从0到1(一):意识 ByteBuffer 的所有内容了,心愿本篇博文对大家有所帮忙!