NIO-下-灵析社区

菜鸟码转

二、 新版 IO 简介

Java的旧 IO(java.io 包)和新 IO(java.nio 包)都是用于文件和网络 I/O 操作的 Java 标准库。它们各自都有优缺点,所以Java开发人员可以根据自己的需求选择使用哪种 IO 库。

旧 IO 是较早的 Java I/O库,它使用流(Stream)的概念来处理数据。旧 IO 库的 API 简单易懂,但在处理大量数据时性能较差。因为它在读取或写入数据时,必须等待整个流操作完成后才能进行下一步操作。这种同步阻塞的方式,导致了较低的性能。

新IO(NIO)是在 Java 1.4 中引入的,相对于旧 IO 而言,它更加灵活、高效。NIO 提供了一种新的 I/O 操作方式,它基于通道(Channel)和缓冲区(Buffer)的概念,使用非阻塞的方式处理 I/O 操作,能够更好地适应现代的高并发环境。它的 API 较为复杂,但能够提供更高效的 I/O 操作。

因此,新 IO 主要用于处理需要高效处理 I/O 的场景,例如:大数据量的文件操作、网络编程等。旧 IO 主要用于简单的 I/O 操作,例如:读写少量数据、文件系统操作等。

Java NIO 的优点

相比于传统的阻塞 IO 操作,Java NIO 具有以下几个优点:

高效的 IO 操作

Java NIO 可以使用选择器(Selector)来监视多个通道(Channel),在通道准备就绪时自动通知应用程序,从而避免了阻塞等待 IO 操作完成的情况。这样可以使得 IO 操作更加高效,特别是在同时处理多个连接或数据流时。

支持多种 IO 操作类型

Java NIO 支持多种 IO 操作类型,如文件 IO、网络 IO、管道 IO 等。这些操作类型都可以使用相同的 API 进行操作,使得编程更加简单和一致。

更好的内存管理

Java NIO 使用缓冲区(Buffer)来缓存读取或写入的数据,缓冲区支持批量读取或写入,这样可以避免频繁的内存分配和释放操作,提高了内存的利用率和程序的性能。

可靠的错误处理

Java NIO API 提供了可靠的错误处理机制,可以更好地处理 IO 操作中出现的异常和错误情况。主要有以下几个方面的原因:

  • 异常处理:NIO API 的各个方法都有明确的异常处理机制,当出现异常时,API 会抛出特定的异常类,而不是简单地返回错误代码。这使得开发人员可以更加精确地定位和处理问题;
  • 缓冲区管理:在 NIO 中,读写操作都是通过缓冲区来完成的。缓冲区本身就是一个错误处理机制,因为缓冲区在读写操作中可以确保数据的有效性和完整性。当读取到的数据不完整或不符合要求时,缓冲区会自动记录错误,而不是将错误的数据传递给下一层处理;
  • 选择器(Selector)机制:NIO提供了选择器机制,可以同时监控多个通道的事件,例如读取、写入和连接事件等。选择器可以有效地减少CPU的资源占用和线程的创建,从而提高程序的性能和可靠性;
  • 异步 I/O 操作:NIO 还支持异步 I/O 操作,异步 I/O 操作可以让程序在进行 I/O 操作时不必一直阻塞等待 I/O 操作的完成,从而提高程序的响应性和可靠性。

总的来说,Java NIO API 使得开发人员可以更有效地处理非阻塞 IO 操作,提高了程序的性能和可靠性,特别是在同时处理多个连接或数据流时。如果你需要处理高并发、高吞吐量的网络应用程序或文件操作程序,那么 Java NIO 就是一个非常好的选择。

Java NIO API 的基本组成部分

以下是 Java NIO API 的基本组成部分:

  • 缓冲区(Buffer):它是数据的临时存储区域,用于在通道(Channel)和 IO 操作之间传递数据。缓冲区支持不同的数据类型,如字节(Byte)、字符(Char)、整数(Int)等。缓冲区的主要作用是提供了非阻塞 IO 操作的数据缓存区,使得 IO 操作更加高效。
  • 通道(Channel):它是一种与底层 IO 设备交互的对象。通道支持非阻塞 IO 操作,可以使用不同的通道类型,如文件(FileChannel)、网络(SocketChannel、ServerSocketChannel、DatagramChannel)等。
  • 选择器(Selector):它是一种用于监视多个通道的对象,可以将一个或多个通道注册到选择器中,并在通道准备就绪时自动通知应用程序。选择器使得应用程序可以在一个线程中处理多个非阻塞 IO 操作。

缓冲区(Buffer)

缓冲区(Buffer)是 Java NIO API 中的一个重要概念,它是用于在通道(Channel)和 IO 操作之间传递数据的临时存储区域。缓冲区可以存储不同类型的数据,如字节(Byte)、字符(Char)、整数(Int)等。

下面是一个简单的缓冲区示例代码,用于存储和读取字节数据:

package cn.leetcode.niodemo;

import java.nio.ByteBuffer;

public class BufferExample {
    
    public static void main(String[] args) {
        // 创建一个缓冲区,大小为 10 字节
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 写入数据到缓冲区
        buffer.put((byte) 'H');
        buffer.put((byte) 'e');
        buffer.put((byte) 'l');
        buffer.put((byte) 'l');
        buffer.put((byte) 'o');

        // 将缓冲区从写模式切换到读模式
        buffer.flip();

        // 读取数据
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            System.out.print((char) b);
        }
    }
    
}

输出:

Hello

上面的代码创建了一个大小为 10 字节的缓冲区,并使用 put() 方法将字节数据写入缓冲区中。然后,使用 flip() 方法将缓冲区从写模式切换到读模式,这样就可以从缓冲区中读取数据了。最后,使用 hasRemaining() 方法检查缓冲区中是否还有剩余的数据,如果有,则使用 get() 方法读取数据。

缓冲区还有很多其他的方法,如 clear() 方法清空缓冲区、compact() 方法将缓冲区中的数据移到缓冲区的起始位置、mark() 和 reset() 方法标记和重置缓冲区等。了解这些方法对于正确使用缓冲区非常重要。

通道(Channel)

通道(Channel)是 Java NIO API 中的一个重要概念,它是一种与底层 IO 设备交互的对象。通道可以使用不同的通道类型,如文件(FileChannel)、网络(SocketChannel、ServerSocketChannel、DatagramChannel)等。

下面是一个简单的通道示例代码,用于读取文件数据:

package cn.leetcode.niodemo;

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ChannelExample {

    public static void main(String[] args) throws Exception {
        try (FileInputStream fileInputStream = new FileInputStream("input.txt");
             // 打开文件通道
             FileChannel channel = fileInputStream.getChannel()) {
            // 创建一个缓冲区,大小为 10 字节
            ByteBuffer buffer = ByteBuffer.allocate(10);
            // 从通道中读取数据到缓冲区
            int bytesRead = channel.read(buffer);
            while (bytesRead != -1) {
                System.out.println("Read " + bytesRead + " bytes");
                // 将缓冲区从写模式切换到读模式
                buffer.flip();

                // 读取缓冲区中的数据
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }

                // 清空缓冲区
                buffer.clear();
                bytesRead = channel.read(buffer);
            }
        }
    }

}

上面的代码打开了一个文件通道,使用 read() 方法从通道中读取数据到缓冲区中,然后使用 flip() 方法将缓冲区从写模式切换到读模式,最后使用 get() 方法读取缓冲区中的数据。如果缓冲区中还有剩余的数据,则需要再次读取,直到读取完成。最后,使用 close() 方法关闭通道和文件输入流。

通道还有很多其他的方法,如 write() 方法将数据写入通道、transferFrom() 和 transferTo() 方法在通道之间传输数据、position() 和 position(long newPosition) 方法设置和获取通道的位置等。了解这些方法对于正确使用通道非常重要。

选择器(Selector)

选择器(Selector)是 Java NIO API 中的一个重要概念,它可以用于监视多个通道(Channel),在通道准备就绪时自动通知应用程序。选择器使得应用程序可以在一个线程中处理多个非阻塞 IO 操作,从而提高了程序的性能和可扩展性。

可以通过以下的通俗易懂的例子来理解 Java NIO 中的 Selector :假设有一家快递公司需要同时处理多个快递订单。每个订单都有不同的状态,例如待发货、已发货、已签收等。为了能够高效地处理这些订单,快递公司雇佣了多名快递员,每名快递员可以处理多个订单。

在这个例子中,快递订单可以看做是 Java NIO 中的通道(Channel),快递员可以看做是 Java NIO 中的选择器(Selector),不同的订单状态可以看做是 Java NIO 中的事件(Event)。选择器(快递员)可以同时监控多个通道(订单),当有事件(订单状态发生变化)时,选择器(快递员)会立即响应并进行相应的处理(例如将待发货的订单状态改为已发货)。

下面是一个简单的选择器示例代码,用于监听多个 Socket 连接:

package cn.leetcode.niodemo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class Server {

    private static final int PORT = 8080;
    private static final int BUFFER_SIZE = 1024;
    private static final String CHARSET = "UTF-8";

    public static void main(String[] args) throws IOException {
        // 创建 Selector
        Selector selector = Selector.open();

        // 创建 ServerSocketChannel,并绑定端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(PORT));
        serverSocketChannel.configureBlocking(false);

        // 将 ServerSocketChannel 注册到 Selector,并监听 ACCEPT 事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 阻塞等待事件发生
            selector.select();
            // 获取发生事件的所有通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 处理 ACCEPT 事件
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }

                // 处理 READ 事件
                if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
                    StringBuilder content = new StringBuilder();
                    while (socketChannel.read(buffer) > 0) {
                        buffer.flip();
                        content.append(new String(buffer.array(), 0, buffer.remaining(), CHARSET));
                    }
                    System.out.println("Received data from client: " + content.toString());

                    // 将数据回显给客户端
                    buffer.clear();
                    buffer.put(("Echo message: " + content.toString()).getBytes());
                    buffer.flip();
                    socketChannel.write(buffer);
                }
                // 处理完事件后,需要将选择键清空
                iterator.remove();
            }
        }
    }

}

在上述代码中,首先创建 Selector 对象,并将 ServerSocketChannel 注册到 Selector 中,监听 ACCEPT 事件。当有客户端连接时,会触发 ACCEPT 事件,然后将 SocketChannel 注册到 Selector 中,监听 READ 事件。当客户端发送数据时,会触发 READ 事件,从而实现对客户端请求的处理和回显。

值得注意的是,这里的 socketChannel.configureBlocking(false) 方法将 SocketChannel 设置为非阻塞模式,从而可以实现非阻塞 IO 操作。同时,在处理完事件后,需要将选择键清空,否则会导致重复处理相同的事件。

可以使用 telnet 工具来测试该代码的效果。

  • 打开命令提示符(Windows)或终端(Linux/MacOS),MacOS 可以使用 brew install telnet 安装;
  • 输入命令telnet localhost 8080,连接到本地的 8080 端口;
  • 输入任意字符串,按下回车键发送到服务端;
  • 在服务端的控制台中,可以看到服务端接收到客户端发送的数据,并回显给客户端。

以下是一个示例会话:

telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Hello LeetCoder!
Echo message: Hello LeetCoder!

在该示例会话中,我们首先连接到本地的8080端口,然后向服务端发送了字符串 Hello LeetCoder!。服务端接收到该字符串,并回显给客户端 Echo message: Hello LeetCoder!。

使用 Java NIO API 进行非阻塞 IO 操作的基本步骤

  1. 下面是使用 Java NIO API 进行非阻塞 IO 操作的基本步骤:
  2. 创建一个通道(Channel):使用通道的工厂方法创建一个通道对象,如 FileChannel.open()、SocketChannel.open() 等。
  3. 将通道注册到选择器(Selector)中:使用通道的 register() 方法将通道注册到选择器中,并指定感兴趣的 IO 操作类型(如读、写等)。
  4. 创建一个缓冲区(Buffer):使用缓冲区的工厂方法创建一个缓冲区对象。
  5. 从通道中读取数据或向通道中写入数据:使用通道的 read() 或 write() 方法读取或写入数据,同时将数据存储到缓冲区中。
  6. 处理缓冲区中的数据:使用缓冲区的 get() 方法获取缓冲区中的数据,并进行处理。
  7. 清空缓冲区或将缓冲区中的数据复位:使用缓冲区的 clear() 或 flip() 方法清空缓冲区或将缓冲区中的数据复位。
  8. 重复执行步骤 4 - 6,直到读取或写入操作完成。

阅读量:2022

点赞量:0

收藏量:0