前语
本篇博文是《从0到1学习 Netty》中 NIO 系列的榜首篇博文,主要内容是介绍 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 的步骤;
编写代码进行测验:
@Slf4j
public 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,因为篇幅原因,自行在我的 GitHub 上下载:logback.xml;
将结尾部分的 <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.... |
+--------+-------------------------------------------------+----------------+
// 读取两个字节的数据;
s
i
// 此刻 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,这意味着下一次读取或写入操作将从缓冲区的开头开端。
@Test
public 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';
s
Process 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() 用于返回到符号方位;
@Test
public 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());
}
运转成果:
s
i
d
i
+--------+-------------------- 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..........|
+--------+-------------------------------------------------+----------------+
d
i
Process finished with exit code 0
get(i)
get(i) 不会改变读索引的方位;
@Test
public 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 中:
@Test
public 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() 切换形式:
@Test
public 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 |
+--------+-------------------------------------------------+----------------+
sidiot
Process 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 同样为读形式:
@Test
public 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
后记
以上便是知道 ByteBuffer的一切内容了,希望本篇博文对大家有所协助!
参阅:
- Netty API reference;
- 黑马程序员Netty全套教程 ;
上篇精讲:这是榜首篇,没有上一篇喔~
我是,等待你的关注;
创造不易,请多多支撑;
系列专栏:探索 Netty:源码解析与运用案例共享







