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 |
| 网络通信 | 使用 SocketServerSocket 类 | 使用 SocketChannelServerSocketChannel |

(一) 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 读取文件内容

java 复制代码
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 响应。在高并发场景下,你希望只使用少量线程来同时处理大量的请求。

java 复制代码
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 测试类(客户端)

java 复制代码
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. 服务器会接收请求并发送响应,客户端接收到响应后打印输出。
相关推荐
蜗牛、Z5 天前
Java NIO之FileChannel 详解
java·nio
嘉友8 天前
NIO ByteBuffer 总结
java·后端·nio
佩奇的技术笔记9 天前
初级:I/O与NIO面试题深度剖析
java·nio
小汤猿人类10 天前
NIO入门
java·开发语言·nio
A227411 天前
Netty——BIO、NIO 与 Netty
java·netty·nio
A227412 天前
Netty——NIO 空轮询 bug
java·netty·nio
脑子慢且灵13 天前
JavaIO流的使用和修饰器模式(直击心灵版)
java·开发语言·windows·eclipse·intellij-idea·nio
爱的叹息15 天前
java NIO中的FileSystems工具类可以读取本地文件系统,ZIP/JAR等,无需解压处理,还可以复制文件
java·jar·nio
码农的天塌了19 天前
基于Netty的即时通讯服务器
java·netty·nio·即时通信
用针戳左手中指指头1 个月前
Netty笔记3:NIO编程
java·nio