当前位置: 首页 > article >正文

从BIO到NIO:Java IO的进化之路

引言

在 Java 编程的世界里,输入输出(I/O)操作是基石般的存在,从文件的读取写入,到网络通信的数据传输,I/O 操作贯穿于各种应用程序的核心。BIO(Blocking I/O,阻塞式 I/O)作为 Java 早期的 I/O 模型,以其简单直观的编程方式,在早期的 Java 应用开发中扮演了重要角色 ,为开发者提供了基础的 I/O 能力。但随着互联网应用的快速发展,尤其是在高并发场景下,BIO 的局限性逐渐暴露,如线程资源消耗大、I/O 操作阻塞导致效率低下等问题,难以满足日益增长的性能需求。

为了突破 BIO 的困境,NIO(New I/O 或 Non - Blocking I/O,新 I/O 或非阻塞 I/O)应运而生。NIO 自 Java 1.4 版本引入后,带来了全新的 I/O 编程理念和方式,它通过 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)等核心组件,构建了一种基于事件驱动的非阻塞 I/O 模型,极大地提升了 I/O 操作的效率和并发处理能力,成为 Java I/O 领域的一次重大变革。

本文将深入探讨 BIO 到 NIO 的演变历程,从背景历史、功能点、业务场景、底层原理等多个维度进行剖析,详细介绍其实现原理,并通过 Java 代码给出至少三个不同的样例,帮助读者全面理解和掌握这两种 I/O 模型的精髓,以及它们在不同场景下的应用。

一、BIO 的诞生与发展

1.1 计算机 IO 的基础概念

在计算机系统中,I/O(Input/Output,输入 / 输出)是指计算机与外部世界进行数据交互的过程。计算机通过输入设备(如键盘、鼠标、摄像头等)接收外部数据,再将处理后的数据通过输出设备(如显示器、打印机、扬声器等)输出到外部 。在这个过程中,数据在内存和外部设备之间流动,而 I/O 操作就是负责管理和控制这种数据流动的机制。

I/O 操作涉及到计算机硬件和软件的多个层面。从硬件角度看,I/O 设备通过设备控制器与计算机的总线相连,设备控制器负责管理设备的具体操作,如数据传输、设备状态监测等。从软件角度看,操作系统提供了设备驱动程序,用于与设备控制器进行通信,使得应用程序能够通过操作系统提供的接口来访问 I/O 设备。

1.2 BIO 的初步实现

BIO(Blocking I/O,同步阻塞 I/O)是 Java 早期的 I/O 模型,它基于流(Stream)的概念来实现数据的读写操作。在 BIO 中,当进行 I/O 操作时,线程会被阻塞,直到操作完成。例如,使用InputStream从文件或网络连接中读取数据时,线程会一直等待,直到有数据可读或读取操作完成;使用OutputStream向文件或网络连接中写入数据时,线程会一直等待,直到数据被完全写入。

以从文件中读取数据为例,Java 代码如下:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class BIOExample {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("example.txt")) {
            int data;
            while ((data = inputStream.read())!= -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,FileInputStream是InputStream的子类,用于从文件中读取数据。read()方法会阻塞线程,直到读取到一个字节的数据或到达文件末尾(返回 -1)。每次读取一个字节,然后将其转换为字符并打印出来。

1.3 BIO 的优化与工具类扩展

为了提升 BIO 的读写效率,Java 引入了缓冲区(Buffer)的概念。通过使用BufferedInputStream和BufferedOutputStream,可以减少系统 I/O 调用的次数,从而提高性能。BufferedInputStream内部维护了一个缓冲区,当读取数据时,它会一次性从底层输入流中读取多个字节到缓冲区中,然后从缓冲区中返回数据给应用程序。当缓冲区中的数据耗尽时,它会再次从底层输入流中读取数据到缓冲区。BufferedOutputStream的工作原理类似,它会将数据先写入缓冲区,当缓冲区满或调用flush()方法时,才将缓冲区中的数据一次性写入到底层输出流中。

以下是使用BufferedInputStream和BufferedOutputStream进行文件复制的示例代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedIOExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("target.txt"))) {
            int data;
            while ((data = bis.read())!= -1) {
                bos.write(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

此外,为了方便字符数据的读写,Java 还提供了转换流InputStreamReader和OutputStreamWriter,它们可以将字节流转换为字符流,并指定字符编码。例如:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class ConvertStreamExample {
    public static void main(String[] args) {
        try (InputStreamReader isr = new InputStreamReader(new FileInputStream("source.txt"), "UTF-8");
             OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("target.txt"), "UTF-8")) {
            int data;
            while ((data = isr.read())!= -1) {
                osw.write(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

进一步地,Java 还提供了更便捷的字符流操作类FileReader和FileWriter,它们默认使用平台的默认字符编码,简化了字符文件的读写操作。例如:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileReaderWriterExample {
    public static void main(String[] args) {
        try (FileReader fr = new FileReader("source.txt");
             FileWriter fw = new FileWriter("target.txt")) {
            int data;
            while ((data = fr.read())!= -1) {
                fw.write(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.4 BIO 的特点与局限性

BIO 在处理少量连接请求时,具有响应速度高、编程简单等优势。因为每个连接由一个独立的线程处理,代码逻辑清晰,开发人员容易理解和实现。例如,在一些简单的客户端 - 服务器应用中,BIO 可以快速搭建起基本的通信框架。

然而,当面对大量连接请求时,BIO 的局限性就会凸显出来。首先,为了处理每个连接,服务器需要创建大量的监听线程,而线程的创建和销毁会消耗大量的系统资源,包括内存、CPU 时间等。其次,由于 I/O 操作是阻塞的,当一个线程在进行 I/O 操作时,它会被阻塞,无法执行其他任务,这就导致了线程资源的浪费。例如,当一个客户端连接到服务器后,如果长时间没有数据发送,那么处理该客户端的线程就会一直阻塞在读取数据的操作上,无法为其他客户端提供服务。此外,大量线程的存在还会导致线程上下文切换频繁,进一步降低系统的性能。

二、NIO 的应运而生

2.1 时代需求催生 NIO

随着互联网的飞速发展,软件系统面临的并发访问压力与日俱增。在传统的 BIO 模型下,服务器为每个客户端连接创建一个独立的线程进行处理。当并发连接数达到一定规模时,大量线程的创建、管理和销毁会消耗大量的系统资源,包括内存、CPU 时间等 。同时,由于 I/O 操作的阻塞特性,当线程在进行 I/O 操作时,会被阻塞,无法执行其他任务,导致线程资源的浪费,系统的整体性能和并发处理能力受到严重制约。

为了应对这些挑战,Java 在 1.4 版本引入了 NIO(New I/O 或 Non - Blocking I/O)。NIO 的出现旨在提供一种更高效、更灵活的 I/O 处理方式,以满足高并发场景下的性能需求。NIO 基于通道(Channel)和缓冲区(Buffer)进行操作,采用非阻塞 I/O 和多路复用技术,允许一个线程管理多个 I/O 通道,大大减少了线程的数量和上下文切换开销,提高了系统的并发处理能力和 I/O 操作效率。

2.2 NIO 的核心组件

NIO 包含了三个核心组件:Selector(多路复用器)、Channel(通道)和 Buffer(缓冲区)。这三个组件相互协作,共同实现了 NIO 的高效非阻塞 I/O 操作。

2.2.1 Selector(多路复用器)

Selector 是 NIO 的核心组件之一,它允许一个线程同时监听多个 Channel 的事件,如连接建立、数据可读、数据可写等。通过 Selector,线程可以在多个 Channel 之间进行高效的切换,避免了线程的阻塞和资源的浪费 。

在使用 Selector 时,首先需要将 Channel 注册到 Selector 上,并指定需要监听的事件类型。Selector 会不断地轮询注册在其上的 Channel,当某个 Channel 上有感兴趣的事件发生时,Selector 会返回对应的 SelectionKey 集合,通过这些 SelectionKey 可以获取到发生事件的 Channel,并进行相应的处理。

例如,在一个服务器应用中,可以使用 Selector 来监听多个客户端的连接请求和数据传输。当有新的客户端连接请求到达时,Selector 会通知服务器线程,服务器线程可以创建新的 Channel 来处理该连接;当有客户端发送数据时,Selector 也会通知服务器线程,服务器线程可以从对应的 Channel 中读取数据并进行处理。这样,一个服务器线程就可以同时处理多个客户端的请求,大大提高了服务器的并发处理能力。

2.2.2 Channel(通道)

Channel 是对操作系统底层 I/O 通道的抽象,它提供了一种与 I/O 设备进行交互的方式。与传统的 BIO 中的流(Stream)不同,Channel 是双向的,可以同时进行读和写操作 。

在 NIO 中,有多种类型的 Channel,如 FileChannel 用于文件 I/O 操作,SocketChannel 和 ServerSocketChannel 用于 TCP 网络通信,DatagramChannel 用于 UDP 网络通信等。每个 Channel 都可以注册到 Selector 上,以便 Selector 能够监听其事件。

例如,使用 SocketChannel 进行网络通信时,可以通过调用SocketChannel.open()方法创建一个 SocketChannel 实例,然后通过configureBlocking(false)方法将其设置为非阻塞模式,再将其注册到 Selector 上,监听连接建立和数据可读事件。当有数据可读时,就可以从 SocketChannel 中读取数据。

2.2.3 Buffer(缓冲区)

Buffer 是一个用于存储数据的缓冲区,它在 NIO 中扮演着数据读写的中间角色。所有的数据操作都需要通过 Buffer 来进行,Channel 从数据源读取数据到 Buffer 中,然后程序从 Buffer 中读取数据进行处理;程序将处理后的数据写入 Buffer,再由 Channel 将 Buffer 中的数据写入到目标数据源 。

Java NIO 提供了多种类型的 Buffer,如 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分别用于存储不同类型的数据。其中,ByteBuffer 是最常用的 Buffer 类型,它可以用于存储字节数据。

ByteBuffer 又分为 HeapByteBuffer 和 DirectByteBuffer。HeapByteBuffer 是基于 Java 堆内存的缓冲区,它的创建和销毁由 Java 虚拟机的垃圾回收机制管理,优点是使用方便,与 Java 对象交互简单;缺点是在进行 I/O 操作时,可能需要在堆内存和直接内存之间进行数据拷贝,影响性能。DirectByteBuffer 是基于直接内存(堆外内存)的缓冲区,它直接在操作系统的物理内存中分配空间,不需要经过 Java 堆,因此在进行 I/O 操作时可以减少数据拷贝,提高性能,但它的创建和销毁需要手动管理,使用不当可能会导致内存泄漏。

例如,在使用 FileChannel 读取文件时,可以创建一个 ByteBuffer,然后调用 FileChannel 的read(ByteBuffer buffer)方法将文件中的数据读取到 ByteBuffer 中,再从 ByteBuffer 中读取数据进行处理。在写入数据时,先将数据写入 ByteBuffer,然后调用 FileChannel 的write(ByteBuffer buffer)方法将 ByteBuffer 中的数据写入文件。

三、BIO 与 NIO 的全面对比

3.1 功能点差异

BIO 和 NIO 在功能实现上存在显著差异,这些差异决定了它们在不同场景下的适用性。

从数据处理方式来看,BIO 基于流(Stream)进行操作,数据是顺序、连续地从流中读取或写入 ,就像水流一样,数据的读取和写入是线性的。而 NIO 引入了缓冲区(Buffer)的概念,数据先被读取到缓冲区中,然后再从缓冲区进行处理 。缓冲区提供了更灵活的数据处理方式,例如可以对缓冲区中的数据进行随机访问、标记和重置等操作。

在阻塞特性方面,BIO 是阻塞式的 I/O 模型。当一个线程执行 I/O 操作时,如从输入流中读取数据或向输出流中写入数据,线程会被阻塞,直到操作完成 。这意味着在 I/O 操作执行期间,线程无法执行其他任务,只能等待。例如,在一个简单的网络通信程序中,当服务器线程调用InputStream的read()方法读取客户端发送的数据时,如果没有数据可读,线程就会一直阻塞在这个read()操作上。而 NIO 是非阻塞式的,当线程执行 I/O 操作时,如果数据还没有准备好,线程不会被阻塞,而是立即返回 。线程可以继续执行其他任务,然后在适当的时候再次检查 I/O 操作的状态。例如,在 NIO 的网络编程中,SocketChannel在设置为非阻塞模式后,调用read()方法时,如果没有数据可读,会立即返回一个状态值,告知调用者当前没有数据可读,而不会阻塞线程。

从操作基础来看,BIO 主要基于字节流(InputStream和OutputStream)和字符流(Reader和Writer)进行操作,字节流用于处理二进制数据,字符流用于处理文本数据 。而 NIO 则基于通道(Channel)和缓冲区(Buffer)进行操作。通道是对 I/O 设备的抽象,它提供了一种与 I/O 设备进行交互的方式,并且是双向的,可以同时进行读和写操作 。缓冲区则是用于存储数据的内存块,所有的数据操作都需要通过缓冲区来进行。

在数据传输方向上,BIO 中的流通常是单向的,要么是输入流(InputStream或Reader)用于读取数据,要么是输出流(OutputStream或Writer)用于写入数据 。而 NIO 中的通道是双向的,可以同时进行读和写操作,这使得 NIO 在数据传输上更加灵活 。例如,使用SocketChannel进行网络通信时,可以通过同一个通道既读取来自客户端的数据,又向客户端发送数据。

3.2 底层原理剖析

3.2.1 BIO 的底层原理

BIO 的底层原理基于操作系统的阻塞 I/O 机制。在 BIO 中,当一个客户端与服务器建立连接时,服务器会为每个客户端连接创建一个独立的线程 。这个线程负责处理该客户端的所有 I/O 操作,包括读取客户端发送的数据和向客户端发送响应数据。

当服务器线程调用ServerSocket的accept()方法监听客户端连接时,该方法会阻塞线程,直到有新的客户端连接请求到达 。一旦有新的连接请求,accept()方法会返回一个新的Socket对象,代表与客户端的连接。然后,服务器会为这个Socket创建一个新的线程,用于处理该客户端的 I/O 操作。

在处理客户端的 I/O 操作时,例如读取客户端发送的数据,服务器线程会调用Socket的InputStream的read()方法 。这个方法会阻塞线程,直到有数据可读。如果没有数据可读,线程会一直等待,直到有数据到达或者连接关闭。当有数据可读时,read()方法会将数据读取到缓冲区中,然后返回读取的字节数。同样,在向客户端发送数据时,调用Socket的OutputStream的write()方法也会阻塞线程,直到数据被完全写入。

这种一个客户端对应一个线程的处理方式,虽然简单直观,但在高并发场景下存在严重的性能问题。因为每个线程都需要占用一定的系统资源,包括栈空间、CPU 时间等。当并发连接数较多时,大量线程的创建和管理会消耗大量的系统资源,导致系统性能下降。此外,由于 I/O 操作的阻塞特性,当线程在进行 I/O 操作时,会被阻塞,无法执行其他任务,这就导致了线程资源的浪费。

3.2.2 NIO 的底层原理

NIO 的底层原理基于操作系统的多路复用(Multiplexing)和非阻塞 I/O 机制。在 NIO 中,核心组件包括 Selector(多路复用器)、Channel(通道)和 Buffer(缓冲区) 。

Selector 是 NIO 的关键组件之一,它允许一个线程同时监听多个 Channel 的事件 。Selector 通过内部的轮询机制,不断地检查注册在其上的 Channel 是否有感兴趣的事件发生,如连接建立、数据可读、数据可写等。当某个 Channel 上有事件发生时,Selector 会将该 Channel 对应的 SelectionKey 加入到已选择键集合中,程序可以通过遍历这个集合来获取发生事件的 Channel,并进行相应的处理。

Channel 是对操作系统底层 I/O 通道的抽象,它提供了一种与 I/O 设备进行交互的方式 。Channel 可以注册到 Selector 上,以便 Selector 能够监听其事件。与 BIO 中的流不同,Channel 是双向的,可以同时进行读和写操作。例如,SocketChannel用于 TCP 网络通信,它可以通过configureBlocking(false)方法设置为非阻塞模式,在这种模式下,调用read()和write()方法时,如果数据没有准备好,不会阻塞线程,而是立即返回。

Buffer 是一个用于存储数据的缓冲区,它在 NIO 中扮演着数据读写的中间角色 。所有的数据操作都需要通过 Buffer 来进行,Channel 从数据源读取数据到 Buffer 中,然后程序从 Buffer 中读取数据进行处理;程序将处理后的数据写入 Buffer,再由 Channel 将 Buffer 中的数据写入到目标数据源。例如,在使用SocketChannel读取数据时,首先创建一个ByteBuffer,然后调用SocketChannel的read(ByteBuffer buffer)方法将数据读取到ByteBuffer中,再从ByteBuffer中读取数据进行处理。

NIO 的多路复用机制通过 Selector 实现,它使得一个线程可以同时管理多个 Channel,大大减少了线程的数量和上下文切换开销,提高了系统的并发处理能力和 I/O 操作效率。在一个聊天服务器中,可以使用一个 Selector 来监听多个客户端的SocketChannel,当有新的客户端连接请求到达时,Selector 会通知服务器线程,服务器线程可以创建新的SocketChannel来处理该连接;当有客户端发送数据时,Selector 也会通知服务器线程,服务器线程可以从对应的SocketChannel中读取数据并进行处理。这样,一个服务器线程就可以同时处理多个客户端的请求,提高了服务器的并发处理能力。

3.3 适用业务场景

BIO 适用于连接数目少且固定的场景,因为它为每个连接创建一个线程,在连接数较少时,线程资源的消耗相对较小,且编程简单,易于理解和维护 。例如,一些小型的企业内部应用,可能只需要与少量的客户端进行通信,这种情况下使用 BIO 可以快速搭建起通信框架,并且由于连接数固定,不会出现线程资源耗尽的问题。另外,对于一些对服务器资源要求不高,但对程序的简单性和可读性要求较高的场景,BIO 也是一个不错的选择。比如,一些简单的命令行工具,需要与用户进行交互,读取用户输入并返回结果,使用 BIO 可以方便地实现这种简单的 I/O 操作。

NIO 适用于连接数目多且连接短(轻操作)的高并发场景 。由于 NIO 采用非阻塞 I/O 和多路复用技术,一个线程可以管理多个连接,大大减少了线程的数量和上下文切换开销,能够高效地处理大量并发连接 。例如,在聊天服务器中,会有大量的客户端同时连接到服务器,并且每个客户端的消息发送和接收操作通常都是轻量级的,使用 NIO 可以有效地处理这些并发连接,提高服务器的性能和响应速度。同样,弹幕系统也是一个典型的高并发场景,大量的用户同时发送弹幕消息,NIO 可以快速地处理这些消息,保证弹幕的实时性。此外,在一些服务器间的通信场景中,也经常会涉及到大量的连接,NIO 的高性能和高并发处理能力使其成为理想的选择。

四、Java 实现 BIO 与 NIO 的样例展示

4.1 BIO 示例代码

4.1.1 简单 BIO 实现

下面是一个简单的 BIO(Blocking I/O)实现示例,包括服务端和客户端代码。该示例展示了如何使用 Java 的ServerSocket和Socket进行基本的网络通信,并进行数据的读写操作。

BIO 服务端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("BIO Server is listening on port 8080");
            while (true) {
                // 监听客户端连接,accept()方法会阻塞,直到有新的客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("New client connected: " + socket.getInetAddress());
                // 为每个客户端连接创建一个新的线程来处理通信
                new Thread(() -> {
                    try (
                        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
                    ) {
                        String inputLine;
                        while ((inputLine = in.readLine())!= null) {
                            System.out.println("Received from client: " + inputLine);
                            // 向客户端发送响应
                            out.println("Server response: " + inputLine);
                            if ("exit".equalsIgnoreCase(inputLine)) {
                                break;
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

BIO 客户端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class BIOClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 8080);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            String userInput;
            while ((userInput = stdIn.readLine())!= null) {
                out.println(userInput);
                System.out.println("Sent to server: " + userInput);
                String response = in.readLine();
                System.out.println("Received from server: " + response);
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
            }
        } catch (UnknownHostException e) {
            System.out.println("Don't know about host: localhost");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("Couldn't get I/O for the connection to: localhost");
            e.printStackTrace();
        }
    }
}

在上述代码中,BIO 服务端通过ServerSocket监听 8080 端口,当有客户端连接时,创建一个新的线程来处理与该客户端的通信。在处理线程中,使用BufferedReader从客户端读取数据,使用PrintWriter向客户端发送数据。BIO 客户端通过Socket连接到服务端,同样使用BufferedReader和PrintWriter进行数据的读写操作。

4.1.2 线程池优化的 BIO

为了减少线程创建和销毁的开销,提高性能,可以使用线程池来优化 BIO。下面是一个使用线程池优化后的 BIO 服务端代码示例:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolOptimizedBIOServer {
    private static final int THREAD_POOL_SIZE = 10;
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("ThreadPoolOptimizedBIO Server is listening on port 8080");
            while (true) {
                // 监听客户端连接,accept()方法会阻塞,直到有新的客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("New client connected: " + socket.getInetAddress());
                // 将客户端连接的处理任务提交到线程池
                executorService.submit(() -> {
                    try (
                        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
                    ) {
                        String inputLine;
                        while ((inputLine = in.readLine())!= null) {
                            System.out.println("Received from client: " + inputLine);
                            // 向客户端发送响应
                            out.println("Server response: " + inputLine);
                            if ("exit".equalsIgnoreCase(inputLine)) {
                                break;
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

在这个示例中,使用Executors.newFixedThreadPool(THREAD_POOL_SIZE)创建了一个固定大小为 10 的线程池。当有新的客户端连接时,不再为每个连接创建一个新的线程,而是将处理任务提交到线程池中,由线程池中的线程来处理。这样可以减少线程的创建和销毁开销,提高系统的性能和资源利用率 。同时,在程序结束时,调用executorService.shutdown()方法来关闭线程池,释放资源。

4.2 NIO 示例代码

4.2.1 NIO 基础示例

下面是一个 NIO(New I/O)的基础示例,展示了 NIO 中Selector、Channel和Buffer的协同工作。该示例包括 NIO 服务端和客户端代码,演示了如何使用 NIO 进行网络通信。

NIO 服务端代码

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 NIOServer {
    public static void main(String[] args) {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("NIO Server is listening on port 8080");
            while (true) {
                // 阻塞,直到有感兴趣的事件发生
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();
                    if (key.isAcceptable()) {
                        // 处理新的客户端连接
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = server.accept();
                        clientChannel.configureBlocking(false);
                        // 注册读事件
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("Accepted new connection from client: " + clientChannel.getRemoteAddress());
                    } else if (key.isReadable()) {
                        // 处理客户端发送的数据
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = clientChannel.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            String message = new String(data);
                            System.out.println("Received from client: " + message);
                            // 向客户端发送响应
                            ByteBuffer responseBuffer = ByteBuffer.wrap(("Server response: " + message).getBytes());
                            clientChannel.write(responseBuffer);
                        } else if (bytesRead == -1) {
                            // 客户端关闭连接
                            clientChannel.close();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO 客户端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            while (!socketChannel.finishConnect()) {
                // 可以在此处执行其他任务
            }
            System.out.println("Connected to server");
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("Hello, Server".getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
            int bytesRead = socketChannel.read(buffer);
            if (bytesRead > 0) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                String response = new String(data);
                System.out.println("Received from server: " + response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述 NIO 服务端代码中,首先创建了一个Selector和一个非阻塞的ServerSocketChannel,并将ServerSocketChannel注册到Selector上,监听OP_ACCEPT事件。当有新的客户端连接时,接受连接并将新的SocketChannel注册到Selector上,监听OP_READ事件。当有可读事件发生时,从SocketChannel中读取数据,并向客户端发送响应。NIO 客户端代码中,创建一个非阻塞的SocketChannel,连接到服务端,发送数据并接收服务端的响应。

4.2.2 复杂 NIO 场景示例

下面是一个更复杂的 NIO 应用场景示例,实现了一个简单的文件服务器。该示例展示了 NIO 在实际应用中的强大功能,包括文件的读取和传输。

NIO 文件服务器服务端代码

import java.io.File;
import java.io.FileInputStream;
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 NIOFileServer {
    private static final int PORT = 8081;
    private static final String FILE_DIRECTORY = "files";
    public static void main(String[] args) {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("NIO File Server is listening on port " + PORT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = server.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("Accepted new connection from client: " + clientChannel.getRemoteAddress());
                    } else if (key.isReadable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = clientChannel.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            String fileName = new String(data).trim();
                            System.out.println("Received file request for: " + fileName);
                            File file = new File(FILE_DIRECTORY + File.separator + fileName);
                            if (file.exists() && file.isFile()) {
                                try (FileInputStream fileInputStream = new FileInputStream(file)) {
                                    buffer = ByteBuffer.allocate(1024);
                                    while ((bytesRead = fileInputStream.read(buffer.array()))!= -1) {
                                        buffer.flip();
                                        clientChannel.write(buffer);
                                        buffer.clear();
                                    }
                                }
                            } else {
                                System.out.println("File not found: " + fileName);
                                clientChannel.write(ByteBuffer.wrap("File not found".getBytes()));
                            }
                        } else if (bytesRead == -1) {
                            clientChannel.close();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO 文件服务器客户端代码

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOFileClient {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 8081;
    private static final String DOWNLOAD_DIRECTORY = "downloads";
    public static void main(String[] args) {
        if (args.length!= 1) {
            System.out.println("Usage: java NIOFileClient <fileName>");
            return;
        }
        String fileName = args[0];
        try (SocketChannel socketChannel = SocketChannel.open()) {
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(SERVER_ADDRESS, SERVER_PORT));
            while (!socketChannel.finishConnect()) {
                // 可以在此处执行其他任务
            }
            System.out.println("Connected to file server");
            ByteBuffer buffer = ByteBuffer.wrap(fileName.getBytes());
            socketChannel.write(buffer);
            buffer = ByteBuffer.allocate(1024);
            FileOutputStream fileOutputStream = new FileOutputStream(DOWNLOAD_DIRECTORY + "/" + fileName);
            int bytesRead;
            while ((bytesRead = socketChannel.read(buffer))!= -1) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                if (new String(data).equals("File not found")) {
                    System.out.println("File not found on server");
                    break;
                }
                fileOutputStream.write(data);
                buffer.clear();
            }
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个 NIO 文件服务器示例中,服务端监听指定端口,当有客户端连接并发送文件名请求时,服务端检查文件是否存在,如果存在则将文件内容读取并发送给客户端;如果文件不存在,则向客户端发送 “File not found”。客户端连接到服务端,发送文件名请求,接收文件内容并保存到本地指定目录。如果接收到 “File not found”,则提示用户文件在服务器上不存在。

五、总结与展望

从 BIO 到 NIO 的演变,是 Java I/O 领域顺应时代发展的重大变革。BIO 作为 Java 早期的 I/O 模型,以其简单直观的编程方式,在早期的应用开发中发挥了重要作用 ,但在高并发场景下,其阻塞式的 I/O 操作和线程资源的高消耗,限制了系统的性能和扩展性。

NIO 的出现,为解决 BIO 的困境提供了全新的思路和方法。通过引入 Selector、Channel 和 Buffer 等核心组件,NIO 构建了基于事件驱动的非阻塞 I/O 模型,实现了一个线程管理多个 I/O 通道,大大提高了系统的并发处理能力和 I/O 操作效率 。在高并发场景下,NIO 的优势尤为显著,如聊天服务器、弹幕系统、文件服务器等应用场景中,NIO 能够高效地处理大量并发连接,提升系统的性能和响应速度 。

展望 Java I/O 的未来发展趋势,随着硬件技术的不断进步和应用场景的日益复杂,对 I/O 性能的要求也将越来越高。一方面,NIO 有望在现有基础上进一步优化和完善,提升其在不同场景下的性能表现和稳定性 。例如,在缓冲区管理、通道操作等方面进行更高效的实现,减少资源消耗和性能开销。另一方面,随着异步编程、分布式系统等技术的发展,Java I/O 可能会与这些技术更紧密地结合,以满足分布式、高并发环境下的复杂 I/O 需求 。如在分布式文件系统中,NIO 可以为数据的高效传输和处理提供支持;在异步通信框架中,NIO 的非阻塞特性能够更好地实现异步操作,提高系统的响应能力。此外,随着人工智能、大数据等新兴领域的快速发展,Java I/O 也需要不断演进,以适应这些领域对大规模数据处理和高速数据传输的需求 。

对于 Java 开发者来说,深入理解 BIO 和 NIO 的原理、特性及适用场景,能够根据不同的业务需求选择合适的 I/O 模型,是提升 Java 应用性能和开发效率的关键 。同时,关注 Java I/O 的发展动态,不断学习和掌握新的 I/O 技术和应用方法,将有助于在未来的 Java 开发中,更好地应对各种挑战,开发出更高效、更可靠的应用程序。


http://www.kler.cn/a/533775.html

相关文章:

  • 【Elasticsearch】date range聚合
  • Java进阶(JVM调优)——阿里云的Arthas的使用 安装和使用 死锁查找案例,重新加载案例,慢调用分析
  • CH340G上传程序到ESP8266-01(S)模块
  • 游戏引擎 Unity - Unity 下载与安装
  • kubernetes 核心技术-集群安全机制 RBAC
  • 一次线程数超限导致的hive写入hbase作业失败分析
  • deepseekLLM发展历程
  • ElasticSearch学习笔记-解析JSON格式的内容
  • 硬件工程师笔试基础题目
  • 数字化转型:概念性名词浅谈(第四讲)
  • DS图(下)(19)
  • 【算法】经典博弈论问题③——斐波那契博弈 + Zeckendorf 定理 python
  • 基于YUV的色相调节(一)
  • Leetcode 78. 子集(全排列的变形)
  • Gauss高斯:分布键
  • DeepSeek各版本说明与优缺点分析
  • 留学生编程辅导Haskell/OCaml/Prolog/Rust/Python
  • Linux du 命令详解:查看磁盘使用情况与高级用法 (中英双语)查看某个用户磁盘占用
  • 38. RTC实验
  • 大模型RAG优化方案_融合bm25和语义检索
  • 【Kubernetes Pod间通信-第1篇】在单个子网中使用underlay网络实现Pod到Pod的通信
  • RK3588——解决Linux系统触摸屏坐标方向相反问题
  • Java 网络原理 ④-路由选择 || 网段划分
  • UE求职Demo开发日志#22 显示人物信息,完善装备的穿脱
  • 限流策略实战指南:从算法选择到阈值设置,打造高可用系统
  • 算法 贪心算法