从零开始学习Java网络编程(三)之超级简单的NIO

0 阅读5分钟
什么是NIO?

这是一个新的IO处理类库,相对于BIO来说,它是新增的,所以它是(New IO)的简称。

但同时,NIO相对BIO来说,BIO是阻塞式IO,NIO是一个非阻塞式的IO,所以它也可称之为(Non-block IO)非阻塞IO。

NIO提供了那些新的特性?
缓冲区(Buffer)

缓冲区Buffer本质是一个对象,这个对象包含写入或者是读出的数据。在BIO中数据读出或者写入是通过Stream流来操作,流是单相的,要么写入,要么读出。但是Buffer是一个双向的。

在NIO中所有的数据都是通过缓冲区来处理,读取数据时,直接读到缓冲区中;写入数据时,写入到缓冲区。访问数据时,都是通过缓冲区进行操作。

public static void main(String[] args) {
        final ByteBuffer allocate = ByteBuffer.allocate(10);
​
        allocate.put("a".getBytes(StandardCharsets.UTF_8));
        allocate.put("A".getBytes(StandardCharsets.UTF_8));
        allocate.put("b".getBytes(StandardCharsets.UTF_8));
        allocate.put("c".getBytes(StandardCharsets.UTF_8));
​
        allocate.flip();
​
        System.out.println(String.valueOf(allocate.get(0)));
        System.out.println(String.valueOf(allocate.get(1)));
        System.out.println(String.valueOf(allocate.get(2)));
        System.out.println(String.valueOf(allocate.get(3)));
    }

缓冲区通常是一个字节数组,但是它也可以是其它类型的数组,比如下面的类图中,所有类型的Buffer父类都是java.nio.Buffer

image-20241105200546230.png

Buffer中定义了几种重要的数据:

private int mark = -1;标志位,允许在执行一些操作后返回此位置。mark 值在调用 mark() 方法时设置为当前的 position。调用 reset() 方法可以将 position 恢复到之前标记的 mark 位置。如果未设置标记,调用 reset() 会抛出 InvalidMarkException。
private int position = 0;定义了读写当前位置,从0开始到limit,每次读写一个元素,position自动增加,使用 flip() 方法切换缓冲区的读写模式时,position 会被重置为 0
private int limit;定义当前读写数据的限制,比如写入模式下,limit <= capacity,在读取模式下,limit表示的是写入数据的末尾位置。
private int capacity;定义了数组的容量
  1. 初始化

image-20230118175831895.png

  1. 写入数据

image-20230118175916070.png

  1. 切换为读

image-20230118180141653.png

  1. 读取数据之后

image-20230118180305644.png

  1. clear动作之后

image-20230118180405655.png

  1. compact动作

image-20230118181625219.png

channel通道

Channel是一个“通道“,网络数据通过Channel读取和写入。它有点类似于Stream流,但是它是更为灵活,支持异步的读写操作和双向的数据传输。

因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全全双工的,同时支持读写操作。

  1. 双向传输

Channel 可以同时支持读和写操作,这与传统 I/O 的单向流不同。流在输入时只能读取,在输出时只能写入,而通道则可以同时读写。

  1. 非阻塞操作

Channel 支持非阻塞模式,可以让线程在执行 I/O 操作时不会被阻塞。比如在读取文件时,通道可以立即返回,线程可以继续执行其他任务。

  1. 与缓冲区(Buffer)配合使用

通道与缓冲区密切配合,用来进行高效的数据操作。数据会先从通道读取到缓冲区,再从缓冲区写入到通道中。通过 Buffer 对象,开发者可以更灵活地控制数据的读写操作。

Channel的类图关系:

image-20241105204809622.png

可以看出,Channel提供了接口,下面有不同的实现,其主要可以分为两类,用于网络读写SelectableChannel 和用于文件操作的FileChannel

操作文件的实例:

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
​
public class FileChannelExample {
    public static void main(String[] args) throws Exception {
        // 创建一个 RandomAccessFile 实例并获取 FileChannel
        RandomAccessFile file = new RandomAccessFile("example.txt", "r");
        FileChannel fileChannel = file.getChannel();
​
        // 创建一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
​
        // 将数据从通道读取到缓冲区
        int bytesRead = fileChannel.read(buffer);
​
        // 读取数据
        while (bytesRead != -1) {
            buffer.flip(); // 切换缓冲区为读模式
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear(); // 清空缓冲区用于下次读取
            bytesRead = fileChannel.read(buffer);
        }
​
        fileChannel.close();
        file.close();
    }
}
selector 多路复用器

主要是在单个线程上管理多个Channel的IO操作。通过 Selector,可以在一个线程中同时处理多个通道的 I/O 事件(如读、写、连接、接受等),从而有效提升性能,减少线程资源的消耗。

Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

Selector的特点:

  1. 单线程管理多连接

• 使用 Selector,可以让一个线程同时处理多个通道,而无需为每个通道创建单独的线程,极大地提高了资源利用率。

  1. 非阻塞 I/O

• Selector 支持非阻塞模式,可以检测出哪些通道准备好进行 I/O 操作,避免传统阻塞 I/O 模型中线程等待的问题。

  1. 事件驱动

• Selector 监听通道的事件,通道一旦准备好特定的操作(如读、写),就会通知 Selector 处理相关事件。这种事件驱动机制让 Selector 更加高效。

Selector 监听的事件类型

Selector 可以监听的事件包括:

• OP_READ:通道的读事件,表示通道中有数据可供读取。

• OP_WRITE:通道的写事件,表示通道可以进行写操作。

• OP_CONNECT:连接事件,表示客户端通道成功连接到服务器。

• OP_ACCEPT:接收事件,表示服务器通道可以接受新的客户端连接。