Java NIO 详解

Java 从1.4开始引入NIO(New IO),是一种基于块(Block)的IO机制,也称为非阻塞IO。相比于传统的Java IO(IO流)方式,Java NIO提供了更快速、高效、灵活的IO操作。

Java NIO的核心组件包括以下几个部分:

  1. Channel(通道):Channel是Java NIO的基础,代表了一个与IO设备(如文件、套接字)交互的双向通信通道。它可以读取和写入数据。Channel可以通过Selector来实现非阻塞IO操作。

  2. Buffer(缓冲区):Buffer是一个用来存储数据的对象,NIO的读写操作都是基于缓冲区的。它提供一组方法来读写数据,并且在读写过程中维护缓冲区的状态信息。

  3. Selector多路复用选择器):Selector是一个用于多路复用的对象,它可以同时监控多个Channel的状态,以便在有IO事件到来时通知程序进行处理。通过Selector,可以使单个线程就可以处理多个Channel的IO操作。

  4. Non-blocking IO(非阻塞IO):Java NIO提供了非阻塞IO的特性,即在等待IO操作完成时,线程不会被阻塞,可以继续执行其他任务。这样,一个线程可以同时处理多个Channel的IO操作,提高了系统的吞吐量和响应性能。

Java NIO相对于传统的Java IO方式,具有如下优势:

  1. 更快速的IO操作:通过使用缓冲区和非阻塞IO,Java NIO能够更高效地进行数据读写操作。

  2. 处理多个连接:使用Selector可以单线程处理多个连接,提高系统的并发能力和资源利用率。

  3. 异步IO:Java NIO还提供了一些异步IO的方式,通过回调或者Future来实现。

Channel : 

java.nio.channels.Channel 是Java NIO的基础,代表了一个与IO设备(如文件、Socket)交互的通道。常用的实现类包括:

  1. FileChannel : 用于读写文件中的数据,可以从文件中读取字节数据到Buffer,或将Buffer中的数据写入文件。

  2. SocketChannel:用于通过 TCP 协议进行网络数据的读写操作,可以与远程Socket建立连接,并进行读写操作。

  3. ServerSocketChannel:用于监听并接受客户端连接请求。

  4. DatagramChannel:用于通过 UDP 协议进行网络数据的读写操作。

FileChannel 的使用:        

用于读写文件中的数据,可以从文件中读取字节数据到Buffer,或将Buffer中的数据写入文件。

1、打开FileChannel 

1.1 通过FileInputStream或FileOutputStream获取FileChannel

FileInputStream fis = new FileInputStream("path/to/file");
FileChannel channel = fis.getChannel();

FileOutputStream fos = new FileOutputStream("path/to/file");
FileChannel channel = fos.getChannel();

1.2  通过RandomAccessFile获取FileChannel: 

RandomAccessFile file = new RandomAccessFile("path/to/file", "rw");
FileChannel channel = file.getChannel();

1.3  使用java.nio.file.Files工具类获取FileChannel:open(Path path, OpenOption... options)

Path path = Paths.get("path/to/file");
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);

Java中的Files类是java.nio.file包提供的一个实用工具类,用于进行文件和目录的各种操作。它包含了大量的静态方法,用于操作文件、目录、路径等。Files类的一些常用方法:

Path path = Paths.get("path/to/file");

// 判断文件或目录是否存在:exists(Path path)
boolean exists = Files.exists(path);

// 创建文件:createFile(Path path, FileAttribute... attrs) 
Files.createFile(path);

// 删除文件或目录:delete(Path path) 
Files.delete(path);

// 创建目录:createDirectory(Path dir, FileAttribute... attrs)
Path dir = Paths.get("path/to/directory");
Files.createDirectory(dir);

Path source = Paths.get("path/to/source");
Path target = Paths.get("path/to/target");
// 复制文件或目录:copy(Path source, Path target, CopyOption... options)
Files.copy(source, target);

// 移动/重命名文件或目录:move(Path source, Path target, CopyOption... options)
Files.move(source, target);

// 写入内容到文件:writeString(Path path, CharSequence csq, Charset charset, OpenOption... options) 
String content = "Hello, World!";
Files.writeString(path, content, StandardCharsets.UTF_8);

// 获取文件属性:readAttributes(Path path, Class type, LinkOption... options)
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

1.4 通过java.nio.file.FileSystems工具类获取FileChannel

// 返回代表默认文件系统的FileSystem对象
FileSystem fs = FileSystems.getDefault();
Path path = fs.getPath("path/to/file");
FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE);

FileSystems类提供了一些方便的方法,用于获取默认文件系统、根据URI或Path构建文件系统以及获取文件系统提供程序。它们可以用于实现更灵活多样的文件系统操作。需要注意的是,使用FileSystems类时,需要考虑安全性、权限和适用性等因素,以确保操作正确可靠。如: 

// 根据URI获取文件系统 newFileSystem(URI uri, Map env)
URI uri = new URI("file:/path/to/directory/");
FileSystem fs = FileSystems.newFileSystem(uri, null);

//根据Path获取文件系统 newFileSystem(Path path, ClassLoader loader)
Path path = Paths.get("/path/to/jarfile.jar");
FileSystem fs2 = FileSystems.newFileSystem(path, null);

// 获取支持的文件系统提供程序:newFileSystemProvider(Class type)
FileSystemProvider provider = FileSystems.newFileSystemProvider(FTPFileSystem.class);

1.5 通过FileDescriptor获取FileChannel

FileInputStream fis = new FileInputStream("path/to/file"); 
FileDescriptor fd = fis.getFD();
FileChannel channel = new FileInputStream(fd).getChannel();

在Java中,FileDescriptor类表示一个文件描述符,它是与底层操作系统文件句柄相关联的标识符。FileDescriptor主要用于在Java程序中直接操作底层文件句柄,例如创建FileInputStream、FileOutputStream等。

2、从FileChannel读取数据到缓冲区

// 创建一个大小为1024 bit 的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// FileChannel 数据读到缓冲区
int bytesRead = channel.read(buffer);

3、将数据从缓冲区写入到FileChannel

// flip() 将缓冲区 读/写 模式切换
buffer.flip();
while (buffer.hasRemaining()) {
    channel.write(buffer);
}
buffer.clear();

4、关闭FileChannel

channel.close();

5、其它操作

  1. 移动FileChannel的位置:
    // 移动文件指针位置到当前位置后的50个字节
    long newPosition = channel.position() + 50; 
    channel.position(newPosition);
  2. 截断文件(截短或扩展文件长度)
    channel.truncate(1024); // 将文件截断为1024个字节
  3. 强制将FileChannel的内容刷新到磁盘:
    channel.force(true); // 强制刷新文件数据到磁盘

​​​​​​​​2、SocketChannel 使用

        SocketChannel是用于进行套接字通信的重要组件,它提供了非阻塞的、基于缓冲区的I/O操作。你可以使用SocketChannel来建立连接、发送和接收数据,以及关闭连接等操作。

// 打开一个SocketChannel实例
SocketChannel socketChannel = SocketChannel.open();
// 切换为非阻塞模式
channel.configureBlocking(false);

// 连接远程服务器:使用connect()方法连接到远程服务器
InetSocketAddress remoteAddress = new InetSocketAddress("127.0.0.1", 9001);
socketChannel.connect(remoteAddress);

while (!socketChannel.finishConnect()) { // 等待连接完成
    // 写入数据到SocketChannel
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("要发送的数据".getBytes());
    buffer.flip();  // 将缓冲区切换为读模式
    socketChannel.write(buffer);

    // 从SocketChannel中读取数据到ByteBuffer
    ByteBuffer buffer2 = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer2);
}

channel.close();

3、ServerSocketChannel 使用

        ServerSocketChannel是一种用于监听传入连接的通道。它作为服务器端通道,用于接受客户端的连接请求。

// 打开ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 切换为非阻塞模式 [根据情况选择是否要这么处理]
serverChannel.configureBlocking(false);

//绑定端口和地址
serverChannel.bind(new InetSocketAddress("localhost", 8080));

// 接受客户端的连接请求。该方法默认会阻塞,直到有客户端连接到达,返回一个SocketChannel对象
// 如果切换到了非阻塞, 这里就不会阻塞
SocketChannel clientChannel = serverChannel.accept();

serverChannel.close();

4、DatagramChannel 使用

DatagramChannel是一种用于进行UDP协议通信的通道。它可以发送和接收UDP数据报

// 打开DatagramChannel
DatagramChannel channel = DatagramChannel.open();

// 绑定IP和端口
channel.bind(new InetSocketAddress("localhost", 8080));

// 该方法会阻塞,直到接收到数据报,返回一个SocketAddress和接收到的数据报的数量
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketAddress address = channel.receive(buffer);

//发送数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, Server!".getBytes());
buffer.flip();
int bytesSent = channel.send(buffer, new InetSocketAddress("example.com", 8080));

// 配置阻塞模式, 默认情况下是阻塞模式
channel.configureBlocking(true);

channel.close();

Buffer : 

Buffer介绍:

        java.nio.Buffer 用于读写数据,是Java NIO读写操作的中间容器。​​​​​​​数据从通道读入缓冲区,从缓冲区写入通道。

Java NIO 详解_第1张图片

        缓冲区本质上是一块可以写入和读取数据的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便该块的内存访问​​​​​​​。缓冲区实际上就是一个容器对象/数组。 

  1. 常用的Buffer子类包括:

    1. ByteBuffer:ByteBuffer是最常用的Buffer实现类,用于读写字节数据。

    2. CharBuffer:用于读写字符数据

    3. ShortBuffer/ IntBuffer/ LongBuffer/ FloatBuffer/ DoubleBuffer:用于读写特定类型数据

  2. Buffer的主要特性:

    1. 容量(Capacity):缓冲区的容量表示它可以存储的最大数据量,一旦缓冲区被创建,其容量不能更改。
    2. 位置(Position):读写操作的位置,表示下一个要读取或写入的元素的索引
    3. 上界(Limit):缓冲区当前有效数据的边界,即下一个要读取或写入的元素的索引。

    4. 标记(Mark):记住某个特定位置的索引,可以通过 mark()和 reset()方法来返回该位置。

    5. 读写模式切换:缓冲区可以处于读模式(读取数据到缓冲区)或写模式(将数据从缓冲区写出)。

Buffer使用: 

        1、创建Buffer,分配空间:通过调用Buffer的静态方法allocate()来分配指定容量的缓冲区。

// 分配一个容量为1024字节的ByteBuffer实例
ByteBuffer buffer = ByteBuffer.allocate(1024); 

        2、写入数据到缓冲区:使用Buffer的put()方法将数据写入缓冲区

// 写入一个字节数据到缓冲区
buffer.put((byte) 65); 

        3、转换为读模式:通过调用Buffer的flip()方法,将Buffer切换为读取模式。在读模式下,可以读取缓冲区中的数据。

// 切换为读取模式
buffer.flip(); 

        4、从缓冲区读取数据:使用Buffer的get()方法读取缓冲区中的数据。

// 从缓冲区读取一个字节数据
byte data = buffer.get(); 

        5、重复读取:Buffer的rewind()方法将缓冲区切换为读模式,但保留之前读取到的数据。然后再次使用get()方法读取数据。

// 重复读取前需要调用rewind()方法
buffer.rewind(); 

// 重新读取一个字节数据
byte data = buffer.get(); 

        6、清空缓冲区:

  • clear()方法,将Buffer切换为写入模式,并清空缓冲区的数据。在写入模式下,可以写入新的数据; 
  • compact()方法:相比clear()方法,compact()方法会清除已经读取的数据,但是会保留未读取的数据。未读取的数据会被移动到缓冲区的开头,可以继续写入新的数据。

Buffer的其他API: 

  1. int capacity() : 获取缓冲区的大小
  2. int limit() :获取缓冲区的限制位置,即缓冲区中可读写的数据范围。写入模式下,默认等于缓冲区的容量;读取模式下,默认等于写入模式下的位置
  3. Buffer limit( int newLimit):设置缓冲区的限制位置
  4. int position() : 获取缓冲区的当前位置,即读取或写入数据的位置
  5. Buffer position( int newPosition):设置当前位置的绝对位置
  6. boolean hasRemaining() : 判断缓冲区是否还有未读取的数据
  7. void mark(): 将当前位置设置为标记位置
  8. void reset() : 重置位置为标记位置
  9. void flip() : 切换为读取模式 
  10. void clear() :清空缓冲区,切换为写入模式
  11. void compact() : 压缩缓冲区,将未读取的数据移到开头
  12. void rewind() : 重绕缓冲区,切换为读取模式

Selector:

        java.nio.channels.Selector 是一种用于多路复用的对象,用于管理多个通道的I/O事件。它允许单个线程同时监视多个通道,以及在有准备就绪的通道上进行读写操作,从而提高系统的性能和可伸缩性。

Java NIO 详解_第2张图片

 

Selector的工作原理如下:

  1. 首先,通过Selector.open()创建一个Selector实例。

  2. 然后,将需要进行IO操作的通道注册到Selector上,通过调用通道的register()方法完成注册,此时Selector上生成一个SelectionKey。通道是抽象类SelectableChannel,其子类有: SocketChannel、ServerSocketChannel、DatagramChannel 等。

  3. Selector调用select()方法进行阻塞,等待注册的通道中有事件发生。当有一个或多个通道有事件发生时,select()方法返回,并返回发生事件的通道的数量。

  4. 根据返回的数量,可以通过selectedKeys()方法获取发生事件的通道的集合,然后进行相应的IO操作。

  5. 建议在处理完一个通道的事件后,调用selectedKeys()的remove()方法将该通道从集合中移除,以避免重复处理。

Selector使用示例: 

Selector selector = Selector.open();
SelectableChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while (true) {
    int readyChannels = selector.select();
    if (readyChannels == 0) {
        continue;
    }

    Set selectedKeys = selector.selectedKeys();
    Iterator keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isReadable()) {
            // 处理可读事件
            // ...
        }
        keyIterator.remove();
    }
}

Selector API 

  1. static Selector open(): 创建一个新的Selector对象。
  2. int select(): 阻塞等待就绪的通道,返回已就绪通道的数量
  3. int select(long timeout): 最多阻塞timeout毫秒,等待就绪的通道
  4. int selectNow(): 非阻塞立即返回就绪的通道数量
  5. Set selectedKeys(): 获取就绪通道的SelectionKey集合
  6. Set keys(): 获取所有注册通道的SelectionKey集合
  7. Selector wakeup() :唤醒其他阻塞线程的 select 方法立刻执行并返回
  8. void close() :关闭Selector 

其它与Selector相关的API 

Selector 的使用过程中, SelectableChannel 和 SelectionKey 一直贯穿着Selector的始终,SelectableChannel 是注册进Selector的通道, SelectionKey 是通道注册进Selector后生成对象,里面包含了通道注册信息和进程状态信息。

  1. SelectableChannel  的 API : 
    1. SelectionKey register(Selector sel, int ops):将通道注册到Selector上,同时指定关注的事件类型,常用的时间类型有:
      1. SelectionKey.OP_READ = 1 << 0:读事件
      2. SelectionKey.OP_WRITE = 1 << 2:写事件
      3. SelectionKey.OP_CONNECT = 1 << 3:连接事件
      4. SelectionKey.OP_ACCEPT = 1 << 4 :接收事件
    2. SelectableChannel configureBlocking(boolean block) : 设置通道为【非】阻塞模
    3. boolean isRegistered(): 判断通道是否已经注册到Selector上
    4. boolean isBlocking() : 通道是否阻塞
    5. SelectionKey keyFor(Selector sel) :返回通道在Selector中最后一次注册的信息
    6. Object blockingLock() :返回的是通道对象关联的锁对象,用于同步操作阻塞模式切换。它并不是用于直接控制通道的阻塞/非阻塞状态。
  2. SelectionKey 的API :SelectionKey类表示一个通道在选择器上的注册信息。 
    1. SelectableChannel channel() : 获取关联的通道
    2. Selector selector() :获取关联的选择器
    3. boolean isReadable() | isWritable() | isConnectable() | isAcceptable() : 判断通道是否可读、写、连接、接受连接
    4. int readyOps() : 获取准备就绪的操作集合
    5. int interestOps() : 获取所有注册的操作集合
    6. SelectionKey interestOps(int ops) : 修改操作集合
    7. void cancel() : 取消注册
    8. Object attach(Object ob) : 这是关联的附加对象
    9. Object attachment() : 获取关联的附加对象

Scatter/Gather

Java NIO(New I/O)中的Scatter/Gather是一种用于处理I/O操作的重要模式,通过将数据分散读取(Scatter Read)或聚集写入(Gather Write),可以有效地处理复杂的数据结构。在Scatter/Gather模式下,可以一次性读取多个数据块到多个缓冲区,或将多个缓冲区中的数据一次性写入到目标通道。

1. Scatter Read(分散读取)

  • 使用多个缓冲区准备好接收数据。
  • 调用通道的read()方法,将数据按照缓冲区的顺序依次读取到各个缓冲区中。
ByteBuffer buffer1 = ByteBuffer.allocate(64);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };

channel.read(buffers);

2. Gather Write(聚集写入)

  • 使用多个缓冲区存储要写入的数据。
  • 调用通道的write()方法,将多个缓冲区中的数据一起写入到目标通道。
ByteBuffer buffer1 = ByteBuffer.allocate(64);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };

channel.write(buffers);

Scatter/Gather模式可以用于处理复杂的数据结构,例如由多个部分组成的消息。通过Scatter将数据分散到多个缓冲区,可以对消息的各个部分进行独立处理。而通过Gather将多个缓冲区中的数据聚集写入到目标通道,可以将多个部分组装成完整的消息并进行发送。

需要注意的是,Scatter/Gather模式依赖于底层通道的能力支持。并非所有的通道都支持Scatter/Gather操作,只有实现了`ReadableByteChannel`(可读通道)或`WritableByteChannel`(可写通道)接口的通道才能进行Scatter/Gather操作。

总结: 

 Java NIO还提供了其他一些类和接口,如FileLock、MappedByteBuffer、Pipe等,用于实现更复杂的IO操作和功能。

需要注意的是,Java NIO的使用与传统的Java IO方式有很大的区别,它对于开发者而言更加复杂。但在高并发、大规模等场景下,Java NIO可以提供更好的性能和可扩展性。

IO 与 NIO对比
IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO 非阻塞IO
(无) 选择器(Selectors)

你可能感兴趣的:(java基础,java,nio,开发语言)