Channel
在Java NIO(New I/O)中,“Channel”(通道)是一个重要的概念,用于在非阻塞I/O操作中进行数据的传输。Java NIO提供了一种更为灵活和高效的I/O处理方式,相比于传统的I/O,它具有更好的性能和可扩展性。
常见的Java NIO中的通道类型:
通过使用这些通道,Java NIO可以提供非阻塞的I/O操作,允许程序在一个线程中管理多个I/O操作,提高了系统的并发处理能力和性能表现。通道和缓冲区(Buffer)结合使用,能够实现更灵活、高效的数据传输和处理。
Buffer
在Java NIO中,Buffer(缓冲区)是一个核心概念,用于在通道(Channel)和数据源之间进行数据交互。Buffer实际上是一个容器,用于存储特定类型的数据,并提供了方便的方法来读取和写入数据。
常见的Java NIO中的Buffer类型:
ByteBuffer:
CharBuffer:
ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer:
这些缓冲区提供了一种灵活的方式来管理数据,可以在缓冲区和通道之间进行数据传输,也可以在缓冲区内部进行数据处理。通过缓冲区,Java NIO实现了高效的I/O操作,可以在同一线程内处理多个通道,提高了系统的并发性能。
当使用 Java NIO 进行非阻塞 I/O 操作时,Selector(选择器)是一种关键的组件。它允许单个线程有效地管理多个 Channel,监视它们的状态并且在
至少一个 Channel 准备好进行读取、写入或者连接时
,唤醒线程。Selector 的作用是配置一个线程来管理多个 Channel,获取这些 Channel 上发生的事件。这些 Channel 通常工作在非阻塞模式下,因此不会让线程阻塞在一个 Channel 上。Selector 适用于连接数量特别多但流量低的场景,因为它可以有效地利用一个线程来处理多个连接,降低线程创建和上下文切换的开销。
通过 Selector,Java NIO 提供了一种高效的非阻塞 I/O 模型,使得应用程序能够更好地处理大量并发连接,提高系统的性能和可伸缩性。
ByteBuffer 是 Java NIO 中用于处理字节数据的缓冲区类之一。它提供了一种灵活的方式来处理字节数据,例如读取、写入、修改、复制等操作。
以下是关于 ByteBuffer 的一些重要特点和用法:
ByteBuffer 在网络编程、文件 I/O、数据处理等领域广泛应用,它是 Java NIO 中处理字节数据的重要工具之一,提供了高效、灵活的字节数据处理方式。
/**
* ByteBuffer 测试测试读取文件
* @author 13723
* @version 1.0
* 2024/2/19 21:30
*/
public class ByteBufferTest {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Test
@DisplayName("测试读取文件")
public void testReadFile() {
// 1.读取文件
try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
while (true){
// 1.1 创建一个缓冲区(划分一块内存作为缓冲区) 每次读取只取5个字节 5个字节读取完毕后再读取下一个5个字节
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
// 1.2 读取数据到缓冲区
int len = channel.read(byteBuffer);
// 读取到文件末尾 len = -1 退出循环
if (len == -1) {
break;
}
// 1.3 打印缓冲区的内容
// 切换到读模式
byteBuffer.flip();
while (byteBuffer.hasRemaining()){ // 检查是否存在未读取的数据
byte b = byteBuffer.get();
logger.error("读取到的字节为:{}", (char) b);
}
// 1.4 每次读取完成切换为写模式
byteBuffer.clear();
}
} catch (IOException e) {
}
}
}
compact方法,就是把未读取完成的部分向前压缩,然后切换为写模式下次写入的位置,是从上次没有读完的位置开始的
@Test
@DisplayName("测试ByteBuffer写入读取数据")
public void test2(){
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写入一个字节 ‘a’
buffer.put((byte) 'a');
// 打印字符
debugAll(buffer);
// 再写入两个字节
buffer.put(new byte[]{'b', 'c'});
debugAll(buffer);
// 切换成模式,此时position = 0, limit = 3
buffer.flip();
byte b = buffer.get();
System.out.println((char) b);
debugRead(buffer);
buffer.compact();
debugAll(buffer);
}
可以使用 allocate 方法为 ByteBuffer 分配空间,其他 Buffer 类也有该方法 不能动态调整容量 netty对ByteBuffer这里做了增强,可以动态调整
它使用的是 Java 堆内存(Heap Memory)。allocate 方法分配的是一个非直接缓冲区,即缓冲区的数据存储在 Java 虚拟机的堆内存中。
堆内存的优势在于它的管理由 Java 虚拟机负责,而且垃圾回收器可以有效地回收不再使用的对象,但是在进行 I/O 操作时,需要将数据从堆内存复制到内核空间,存在额外的拷贝开销。
读写效率低
ByteBuffer buffer = ByteBuffer.allocate(10);
allocateDirect(10),它使用的是直接缓冲区(Direct Buffer),可以通过 ByteBuffer.allocateDirect() 方法来创建。直接缓冲区的数据存储在操作系统的内存中(不会受到垃圾回收的影响),减少了数据在 Java 堆和本地堆之间的拷贝操作,提高了 I/O 操作的效率。但是,直接缓冲区的分配和释放通常更昂贵,可能导致内存泄漏问题。、
读写效率高(少一次拷贝)
ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);
如果有现有的字节数组,可以使用 wrap 方法将其包装成 ByteBuffer
byte[] byteArray = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(byteArray);
获取 ByteBuffer 的容量(capacity)
int capacity = buffer.capacity();
设置 ByteBuffer 的容量(慎用,可能导致数据丢失)
buffer.capacity(20);
获取 ByteBuffer 的位置(position)
int position = buffer.position();
设置 ByteBuffer 的位置(position),用于读取或写入数据
buffer.position(5);
获取 ByteBuffer 的限制(limit),即可以读取或写入的最大位置
int limit = buffer.limit();
设置 ByteBuffer 的限制(limit)
buffer.limit(8);
从 ByteBuffer 中读取字节数据
也可也调用
channel的wirte
方法
byte b = buffer.get();
int r = channel.wite(buffer);
向 ByteBuffer 中写入字节数据
int read = channel.read();
也可也调用
channel的read
方法
buffer.put((byte) 10);
channel.read(buffer);
将 ByteBuffer 从写模式切换到读模式,通常在写入数据后调用
buffer.flip();
清空 ByteBuffer 中的数据,通常在读取数据后调用
buffer.clear();
将 ByteBuffer 的位置设置为 0,通常在重新读取数据时调用
buffer.rewind();
将未读取的数据复制到缓冲区的开始处,然后将位置设置为复制的数据末尾,通常在写入部分数据后调用
buffer.compact();
标记当前的位置,通常与 reset 方法一起使用,用于返回到之前标记的位置,,比如中间某些数据重要需要返回读取
buffer.mark();
重置位置到之前标记的位置
buffer.reset();