自学内容网 自学内容网

NIO | 什么是Java中的NIO —— 结合业务场景理解 NIO (一)

在 Java 中,NIO(New Input/Output)是 Java 1.4 引入的一个新的 I/O 类库,它提供了比传统的 I/O(如 java.io 包)更高效的 I/O 操作。

NIO 提供了更好的性能,尤其是在需要处理 大量数据 的场景中(例如,文件操作网络通信等)。

与传统 I/O 不同,NIO 是基于 缓冲区通道 的,支持非阻塞模式,使得在处理 I/O 操作时可以更加高效地使用系统资源。

一、NIO 的核心概念

NIO 的设计 和 传统 I/O(流式 I/O) 有较大的不同,主要包括以下几个核心概念:

(一) Channel(通道)

在传统 I/O 中,数据是通过 流(Stream)来传输的,而在 NIO 中,数据通过 通道(Channel)进行传输。Channel 是双向的,它可以用来读取数据,也可以用来写入数据。Channel 与流的主要区别在于,Channel 是可以进行双向操作的。

常见的 Channel 实现类包括:

    • FileChannel:用于文件 I/O 操作。
    • SocketChannel:用于网络 I/O 操作(TCP)。
    • ServerSocketChannel:用于网络 I/O 操作(监听 TCP 连接)。
    • DatagramChannel:用于 UDP 网络通信。

(二) Buffer(缓冲区)

在 NIO 中,数据并 不是直接 从 Channel 读入或写入,而是先通过 Buffer 存储,Buffer 是一个用于 读写数据 的容器。在进行数据操作时,数据会从 通道 读取到 缓冲区,或者从缓冲区 写入 到通道。

常见的 Buffer 类型:

  • ByteBuffer:用于处理字节数据。
  • CharBuffer:用于处理字符数据。
  • IntBuffer:用于处理整型数据等。

(三) Selector(选择器)

NIO 提供了 Selector,这是一个用于管理 多个 Channel 的机制,特别是支持 非阻塞 I/O。使用 Selector一个单独的线程可以处理多个 Channel,从而减少线程数量,提高效率。Selector 主要用于 网络通信 中,尤其是在 一个服务器 需要 同时处理 多个客户端连接时,它能显著提高性能。

工作原理:

  • Selector 允许你检查一个或多个 Channel 是否有就绪的 I/O 操作(例如,是否有数据可读,或者是否可以写数据)。
  • ChannelSelector 的结合,使得程序能够在非阻塞模式下轮询多个通道。

二、NIO 的工作模式

NIO 主要支持两种模式:

(一) 阻塞模式

默认情况下,NIO 的通道是阻塞的。在阻塞模式下,调用 I/O 操作(如读取、写入)时,操作会一直等待,直到操作完成为止。

(二) 非阻塞模式

在非阻塞模式下,I/O 操作不会阻塞线程。例如,SocketChannel 可以在 非阻塞模式下 进行读取,如果没有数据可读,它会立即返回,而不是阻塞等待数据。通过使用 Selector,可以在 一个线程 中 轮询 多个 Channel,使得可以同时处理多个 I/O 操作。

三、NIO 与传统 I/O 的区别

特性

传统 I/O (Stream-based I/O)

NIO (Buffer and Channel)

数据传输方式

基于流(Stream),一端写一端读

基于缓冲区(Buffer),双向读写

操作方式

阻塞式 I/O

支持非阻塞式 I/O

性能

对于大量数据或并发连接性能较差

更适合高并发、大数据处理

文件处理

使用 FileInputStream

, FileOutputStream

使用 FileChannel

网络通信

使用 Socket

ServerSocket

使用 SocketChannel

ServerSocketChannel

(一) NIO 的优势

  1. 高效的内存管理:NIO 使用缓冲区(Buffer)存储数据,能够直接操作内存,更加高效。
  2. 非阻塞 I/O:在非阻塞模式下,NIO 能让你在同 一个线程 中处理 多个 I/O 操作,减少了线程的上下文切换,提高了效率。
  3. 支持选择器:通过 Selector,NIO 使得可以在 单个线程 中管理 多个通道,提高了并发处理能力。
  4. 适合高并发场景:特别适合 网络服务器 或需要 处理大量并发连接 的场景,例如 Web 服务器。

(二) NIO 使用场景

  1. 文件操作:对于大量的文件读写,NIO 提供了更高效的方式,特别是当文件操作涉及大文件时。
  2. 网络通信:对于高并发的网络服务器(如 HTTP 服务器、Chat 应用等),使用 NIO 可以显著提升性能。特别是通过使用 Selector,可以用少量的线程处理大量的并发连接。
  3. 数据库连接:在高并发的数据库访问场景下,NIO 可以更高效地处理数据库连接。

四、 示例一:使用 NIO 读取文件

简单的使用 FileChannelByteBuffer 读取文件内容

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class NIOFileReadExample {
    public static void main(String[] args) throws IOException {
        // 1. 打开文件通道
        // 通过 FileInputStream 打开一个文件输入流,然后通过 getChannel() 方法获取该流的 FileChannel
        // FileChannel 允许我们直接操作文件,提供更高效的读写操作
        FileInputStream fis = new FileInputStream("example.txt");
        FileChannel fileChannel = fis.getChannel();
        
        // 2. 创建缓冲区
        // ByteBuffer 是 NIO 中用于存储数据的缓冲区,我们通过 ByteBuffer.allocate() 分配一个大小为 1024 字节的缓冲区
        // 它用于临时存储从文件中读取的数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 3. 读取文件内容到缓冲区
        // 从文件通道读取数据到缓冲区中,返回值是读取的字节数
        // 如果文件已经读取完毕,返回 -1
        int bytesRead = fileChannel.read(buffer);
        
        // 4. 循环直到文件内容读取完毕
        // 当 bytesRead 不为 -1 时,表示文件仍有数据未读取
        while (bytesRead != -1) {
            // 5. 切换到读取模式
            // 缓冲区默认是写模式,我们通过调用 flip() 方法将缓冲区切换为读取模式
            buffer.flip();
            
            // 6. 读取缓冲区中的数据
            // 判断缓冲区中是否还有数据,hasRemaining() 方法返回是否有未读取的数据
            while (buffer.hasRemaining()) {
                // 使用 buffer.get() 获取一个字节的数据,并将其转换为字符后打印输出
                System.out.print((char) buffer.get());
            }
            
            // 7. 清空缓冲区
            // 读取完数据后,通过调用 clear() 方法清空缓冲区,准备下一次读操作
            buffer.clear();
            
            // 8. 继续读取文件内容
            // 继续从文件通道读取更多的数据,直到文件读取完毕
            bytesRead = fileChannel.read(buffer);
        }
        
        // 9. 关闭通道
        // 关闭文件通道和文件输入流,以释放资源
        fileChannel.close();
        fis.close();
    }
}

(一) 注释说明

  1. FileInputStream 和 FileChannelFileInputStream 是传统的 I/O 流,通过 getChannel() 方法可以获取与之关联的 FileChannelFileChannel 是 NIO 中专用于文件操作的通道,支持高效的异步 I/O 操作。
  2. ByteBufferByteBuffer 是 NIO 中用于存储数据的缓冲区。通过 allocate() 方法分配一定大小的缓冲区,可以将数据从通道中读取到缓冲区,再从缓冲区读取数据。
  3. flip() 和 clear()flip() 方法将缓冲区从写模式切换为读模式,使得可以从缓冲区中读取数据。clear() 方法则清空缓冲区,准备下一次读操作。
  4. hasRemaining() 和 get()hasRemaining() 检查缓冲区是否还有未读取的数据,get() 从缓冲区中取出一个字节并返回。
  5. 关闭资源:使用完通道和流后,必须调用 close() 方法关闭它们,以释放系统资源。

这段代码演示了 如何使用 NIO 来高效地从文件中读取数据,并逐个字符打印出来,利用了 NIO 的缓冲区和通道机制。

五、 示例二:高并发的网络服务器

(一) 业务场景

假设你正在开发一个高并发的 Web 服务器,需要同时处理成千上万的 HTTP 请求。每个请求的处理可能涉及文件读取数据库查询等操作。传统的阻塞 I/O 模式下,如果每个请求都创建一个新的线程来处理,随着并发请求的增加,系统会面临 线程上下文切换、内存占用、CPU 资源浪费等问题,从而影响性能。

(二) NIO 解决方案

NIO 提供了 非阻塞 I/O选择器(Selector) 的机制,能够在 一个线程 中同时处理 多个连接 I/O 操作 。使用 Selector,我们可以让一个线程监听多个客户端连接,而无需为每个连接都创建独立的线程。这样能大幅减少系统的开销,提高并发处理能力。

  • Channel(通道):用于与客户端建立连接和发送接收数据。
  • Buffer(缓冲区):用于在通道之间存取数据。
  • Selector(选择器):用于一个线程管理多个通道。它可以判断哪些通道已经准备好进行读写操作,从而实现非阻塞 I/O。

示例: 假设你要实现一个简单的多客户端的 HTTP 服务器,每个客户端连接发送 HTTP 请求并接收 HTTP 响应。在高并发场景下,你希望只使用少量线程来同时处理大量的请求。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;

public class NIOServer {

    public static void main(String[] args) throws IOException {
        // 1. 创建 ServerSocketChannel 来监听客户端的连接
        // ServerSocketChannel 是 NIO 中用于接受客户端连接的通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        
        // 将通道绑定到本地地址(8080端口)
        // InetSocketAddress 用于指定服务器的端口地址
        serverSocketChannel.bind(new InetSocketAddress(8080));
        
        // 设置通道为非阻塞模式
        // 非阻塞模式意味着不会因为等待连接或数据而阻塞当前线程
        serverSocketChannel.configureBlocking(false);

        // 2. 创建 Selector 用于多路复用 I/O 事件
        // Selector 允许在一个线程中管理多个通道的 I/O 事件,避免了每个通道都需要一个线程的资源消耗
        Selector selector = Selector.open();
        
        // 将 serverSocketChannel 注册到 Selector,监听 ACCEPT 事件(客户端连接)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started, listening on port 8080...");

        // 3. 进入主循环,等待并处理 I/O 事件
        while (true) {
            // 4. 阻塞等待 I/O 事件的发生
            // select() 方法会阻塞当前线程,直到至少有一个通道的 I/O 事件发生
            selector.select();

            // 5. 处理所有的 SelectionKey(已发生的事件)
            // selectedKeys() 返回发生了事件的 SelectionKey 集合
            for (SelectionKey key : selector.selectedKeys()) {
                // 判断事件类型,如果是 ACCEPT 事件,表示有新的客户端连接到达
                if (key.isAcceptable()) {
                    // 接受客户端连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    
                    // accept() 会返回一个新的 SocketChannel,用于与客户端进行通信
                    SocketChannel clientChannel = server.accept();
                    
                    // 设置客户端通道为非阻塞模式
                    clientChannel.configureBlocking(false);
                    
                    // 将客户端的通道注册到 Selector,并监听 READ 事件(数据可读)
                    clientChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 处理可读事件:客户端发送的数据已经准备好读取
                    // 获取对应的客户端 SocketChannel
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    
                    // 创建一个缓冲区,用于存储从客户端读取的数据
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    
                    // 从客户端通道读取数据到缓冲区
                    int bytesRead = clientChannel.read(buffer);
                    
                    // 如果返回值为 -1,表示客户端关闭了连接
                    if (bytesRead == -1) {
                        clientChannel.close();
                    } else {
                        // 如果有数据被读取,处理客户端请求
                        buffer.flip(); // 将缓冲区从写模式切换到读模式
                        
                        // 向客户端发送响应数据,HTTP 状态码 200 OK
                        // 使用 ByteBuffer.wrap() 方法将响应内容写入客户端
                        clientChannel.write(ByteBuffer.wrap("HTTP/1.1 200 OK\n".getBytes()));
                    }
                }
            }

            // 6. 清除已处理的 SelectionKey
            // 调用 selectedKeys().clear() 方法清空已处理的事件,
            // 防止再次处理相同的 SelectionKey
            selector.selectedKeys().clear();
        }
    }
}

(三) 注释说明

  1. ServerSocketChannel:用于监听来自客户端的连接请求。在 NIO 中,ServerSocketChannel 提供了服务器端接受连接的能力。它需要被配置为非阻塞模式,这样可以避免阻塞等待客户端连接。
  2. Selector:一个多路复用器,允许你在单线程中监听多个通道的 I/O 事件。通过 select() 方法,Selector 会阻塞,直到至少有一个通道准备好进行 I/O 操作。你可以选择性地注册通道的事件,例如 OP_ACCEPT(连接请求)和 OP_READ(数据可读)。
  3. SelectionKey:每个通道注册到 Selector 时,会生成一个 SelectionKey,它包含了与该通道相关联的事件类型(如连接请求或可读数据)。selectedKeys() 方法返回所有已准备就绪的事件集合。
  4. 非阻塞 I/O:设置通道为非阻塞模式后,调用 select() 会让线程阻塞等待事件的发生,避免了为每个通道分配一个线程的开销。
  5. 处理客户端连接与数据读取:当有新的客户端连接时,OP_ACCEPT 事件触发,服务器会接受客户端连接并注册到 Selector 上,监听后续的读取事件(OP_READ)。当客户端有数据发送时,OP_READ 事件触发,服务器读取数据并回应 HTTP 响应。
  6. 缓冲区操作:在 NIO 中,所有的数据都是通过 ByteBuffer 来操作的。flip() 切换缓冲区的读写模式,clear() 清空缓冲区准备下一次读写。
  7. 关闭连接:在读取过程中,如果客户端关闭了连接,read() 方法会返回 -1,服务器需要关闭相应的通道。

该代码演示了一个简单的非阻塞式 HTTP 服务端实现,使用 Selector 来处理多个客户端连接,避免了为每个连接都创建一个独立线程,从而提高了服务器的性能。

(四) NIOServer 测试类(客户端)

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;

import static java.lang.Thread.sleep;

public class NIOClient {

    public static void main(String[] args) {
        SocketChannel socketChannel = null;

        try {
            // 1. 打开客户端 SocketChannel
            socketChannel = SocketChannel.open();

            // 2. 连接到服务器(指定服务器的 IP 和端口)
            socketChannel.connect(new InetSocketAddress("localhost", 8080));

            // 3. 配置为非阻塞模式
            socketChannel.configureBlocking(false);

            // 4. 等待连接完成,直到连接建立
            while (!socketChannel.finishConnect()) {
                // 在非阻塞模式下,finishConnect() 会检查连接是否已完成
                System.out.println("Connecting to the server...");
            }
            System.out.println("Connected to the server.");

            // 5. 向服务器发送请求数据(HTTP 请求示例)
            String request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
            ByteBuffer buffer = ByteBuffer.wrap(request.getBytes());
            socketChannel.write(buffer);
            System.out.println("Request sent to server: " + request);

            // 6. 接收服务器的响应
            buffer.clear(); // 清空缓冲区准备读取数据
            sleep(1000); // 暂停一秒等待服务器响应 , 防止客户端过早的关闭,产生异常
            int bytesRead = socketChannel.read(buffer);

            if (bytesRead != -1) {
                buffer.flip(); // 切换到读取模式
                byte[] responseData = new byte[buffer.remaining()];
                buffer.get(responseData);

                // 打印服务器响应
                System.out.println("Response from server: " + new String(responseData));
            }

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 7. 关闭连接
            try {
                if (socketChannel != null && socketChannel.isOpen()) {
                    socketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

1. 说明

  1. 连接到服务器
  • 客户端通过 SocketChannel.open() 打开一个套接字通道,接着通过 connect() 方法连接到服务器。
  • 服务器的地址为 localhost,端口为 8080,与 NIOServer 中的服务器端口一致。
  1. 非阻塞模式
  • 设置 SocketChannel 为非阻塞模式,使用 finishConnect() 方法检查连接是否完成。由于是非阻塞操作,客户端会立即返回,连接完成后再继续进行下一步操作。
  1. 发送请求
  • 构造一个简单的 HTTP 请求字符串(GET 请求),并将其封装到 ByteBuffer 中。
  • 使用 write() 方法将请求数据写入服务器。
  1. 接收响应
  • 客户端读取服务器的响应数据。服务器通过 SocketChannel.write() 返回简单的 HTTP 响应(如 HTTP/1.1 200 OK)。
  • 使用 read() 方法从服务器读取响应数据,并将其输出。
  1. 关闭连接
  • 客户端操作完成后,关闭 SocketChannel 连接。

2. 测试流程

  1. 启动 NIOServer 类,它将监听 8080 端口等待连接。
  2. 运行 NIOClient 类,客户端将连接到服务器并发送一个简单的 HTTP 请求。
  3. 服务器会接收请求并发送响应,客户端接收到响应后打印输出。

原文地址:https://blog.csdn.net/qq_56435346/article/details/145303545

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!