BIO

BIO(Blocking IO) 是最传统的 I/O 模型,也称为同步阻塞 I/O。它实现的是同步阻塞模型,即服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情会造成不必要的线程开销,并且线程在进行 I/O 操作期间是被阻塞的,无法进行其他任务。在高并发环境下,BIO 的性能较差,因为它需要为每个连接创建一个线程,而且线程切换开销较大,不过可以通过线程池机制改善。BIO 适合一些简单的、低频的、短连接的通信场景,例如 HTTP 请求。

优缺点

  • 优点

    • 简单易用: BIO 模型的编程方式相对简单,易于理解和使用。

    • 可靠性高: 由于阻塞特性,I/O 操作的结果是可靠的。

  • 缺点

    • 阻塞等待: 当一个 I/O 操作被阻塞时,线程会一直等待,无法执行其他任务,导致资源浪费。

    • 并发能力有限: 每个连接都需要一个独立的线程,当连接数增加时,线程数量也会增加,造成资源消耗和性能下降。

    • 由于 I/O 操作是同步的,客户端的连接需要等待服务器响应,会降低系统的整体性能。

示例代码

服务端

public class BIOServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket clientSocket = null;
        try {
            serverSocket = new ServerSocket(8888);
            System.out.println("服务端已启动,等待客户端连接。。。");

            while (true) {
                clientSocket = serverSocket.accept();
                int port = clientSocket.getPort();
                InetAddress inetAddress = clientSocket.getInetAddress();
                System.out.println("客户端 " + inetAddress + ":" + port + " 连接成功!");
                //处理客户端消息
                new Thread(new ServerThread(clientSocket)).start();
            }
        } catch (IOException e) {
            System.err.println(e.getMessage());
        } finally {
            try {
                if (clientSocket != null) {
                    clientSocket.close();
                }
                if (serverSocket != null) {
                    serverSocket.close();
                }
            } catch (IOException e) {
                System.out.println("关闭资源失败:" + e.getMessage());
            }
        }
    }

    /**
     * 服务端线程处理类
     */
    static class ServerThread implements Runnable {

        private final Socket clientSocket;

        public ServerThread(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            // 获取客户端输入流以便接收客户端数据
            try {
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // 获取客户端输出流以便向客户端发送数据
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream());

                int port = clientSocket.getPort();
                InetAddress inetAddress = clientSocket.getInetAddress();
                String address = inetAddress + ":" + port;

                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    // 接收客户端消息
                    System.out.println("客户端 " + address + " 发来消息:" + inputLine);
                    // 给客户端发送消息
                    out.println("服务端已接收到消息并回复:" + inputLine);
                    out.flush();
                }
            } catch (IOException e) {
                System.err.println(e.getMessage());
            }
        }
    }
}

客户端

public class BIOClient {
    public static void main(String[] args) {
        Socket clientSocket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            // 绑定服务端 ip 和端口号
            clientSocket = new Socket("localhost", 8888);
            System.out.println("连接服务端成功!");
            // 获取输入流,接收服务端消息
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 获取输出流,给服务端发送消息
            out = new PrintWriter(clientSocket.getOutputStream(), true);

            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("给服务端发送消息:");
                String msg = scanner.nextLine();
                out.println(msg);

                String response;
                if ((response = in.readLine()) != null) {
                    // 接收服务端响应
                    System.out.println("服务端响应:" + response);
                }
            }
        } catch (IOException e) {
            System.out.println("连接服务端失败:" + e.getMessage());
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
                if (clientSocket != null) {
                    clientSocket.close();
                }
            } catch (IOException e) {
                System.out.println("关闭资源失败:" + e.getMessage());
            }
        }
    }
}

NIO

NIO 是 Java 1.4 引入的新 I/O 模型,也称为同步非阻塞 I/O,它提供了一种基于事件驱动的方式来处理 I/O 操作。

相比于传统的 BIO 模型,NIO 采用了 Channel、Buffer 和 Selector 等组件,线程可以对某个 I/O 事件进行监听,并继续执行其他任务,不需要阻塞等待。当 I/O 事件就绪时,线程会得到通知,然后可以进行相应的操作,实现了非阻塞式的高伸缩性网络通信。在 NIO 模型中,数据总是从 Channel 读入 Buffer,或者从 Buffer 写入 Channel,这种模式提高了 I/O 效率,并且可以充分利用系统资源。

NIO 主要由三部分组成:选择器(Selector)缓冲区(Buffer)通道(Channel)Channel 是一个可以进行数据读写的对象,所有的数据都通过 Buffer 来处理,这种方式避免了直接将字节写入通道中,而是将数据写入包含一个或者多个字节的缓冲区。在多线程模式下,一个线程可以处理多个请求,这是通过将客户端的连接请求注册到多路复用器上,然后由多路复用器轮询到连接有 I/O 请求时进行处理。

对于 NIO,如果从特性来看,它是非阻塞式 I/O,N 是 Non-Blocking 的意思;如果从技术角度,NIO 对于 BIO 来说是一个新技术,N 的意思是 New 的意思。所以 NIO 也常常被称作 Non-Blocking I/O New I/O

NIO 适用于连接数目多且连接比较短(轻操作)的架构,例如聊天服务器、弹幕系统、服务器间通讯等。它通过引入非阻塞通道的概念,提高了系统的伸缩性和并发性能。同时,NIO 的使用也简化了程序编写,提高了开发效率。

优缺点

  • 优点

    • 高并发性:使用选择器(Selector)和通道(Channel)的 NIO 模型可以在单个线程上处理多个连接,提供更高的并发性能。

    • 节省资源:相对于 BIO,NIO 需要更少的线程来处理相同数量的连接,节省了系统资源。

    • 灵活性:NIO 提供了多种类型的 Channel 和 Buffer,可以根据需要选择适合的类型。NIO 允许开发人员自定义协议、编解码器等组件,从而提高系统的灵活性和可扩展性。

    • 高性能:NIO 采用了基于通道和缓冲区的方式来读写数据,这种方式比传统的流模式更高效。可以减少数据拷贝次数,提高数据处理效率。

    • 内存管理:NIO 允许用户手动管理缓冲区的内存分配和回收,避免了传统 I/O 模型中的内存泄漏问题。

  • 缺点

    • 编程复杂: 相对于 BIO,NIO 的编程方式更加复杂,需要理解选择器和缓冲区等概念,也需要考虑多线程处理和同步问题。

    • 可靠性较低: NIO 模型中,一个连接的读写操作是非阻塞的,无法保证 I/O 操作的结果是可靠的,可能会出现部分读写或者错误的数据。

NIO 适合一些复杂的、高频的、长连接的通信场景,例如聊天室、网络游戏等。

示例代码

在看代码之前先了解 NIO 中 3 个非常重要的组件,选择器(Selector)缓冲区(Buffer)通道(Channel)

  1. 通道(Channel):Channel 是 NIO 中用于数据读写的双向通道,可以从通道中读取数据,也可以将数据写入通道。与传统的 I/O 不同, Channel 是双向的,可以同时进行读写操作,而传统的 I/O 只能通过 InputStream OutputStream 进行单向读写。Java NIO 中常见的 Channel 有:FileChannel(文件读写)、DatagramChannelUDP 协议)、SocketChannelTCP 协议)和ServerSocketChannel(监听 TCP 连接请求)等。

  2. 缓冲区(Buffer): Buffer 是 NIO 中用于存储数据的缓冲区,可以理解为一个容器,可以从中读取数据,也可以将数据写入其中。Buffer 具有一组指针来跟踪当前位置、限制和容量等属性。Java NIO 中提供了多种类型的 Buffer,例如 ByteBufferCharBufferShortBufferIntBuffer 等。每种类型的Buffer都有自己特定的读写方法,可以使用 get()put() 等方法来读写缓冲区中的数据。

  3. 选择器(Selector): Selector 是 NIO 中用于监控多个 Channel 的选择器,可以实现单线程管理多个 Channel。Selector 可以检测多个 Channel 是否有事件发生,包括连接、接收、读取和写入等事件,并根据不同的事件类型进行相应处理。Selector 可以有效地减少单线程管理多个 Channel 时的资源占用,提高程序的运行效率。

NIO 的操作流程如下:

  1. 打开通道并设置为非阻塞模式。

  2. 将通道注册到选择器上,并指定感兴趣的事件类型(如连接打开、可读等)。

  3. 线程通过调用选择器的 select() 方法等待事件发生。

  4. 当有一个或多个事件发生时,线程可以从选择器中获取已经准备好的通道,并进行相应的 I/O 操作。

  5. I/O 操作完成后,关闭通道和选择器。

下面通过两段代码展示一下 NIO 的操作流程和使用方式。

服务端

public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        // 创建一个ServerSocketChannel并绑定到指定的端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 将ServerSocketChannel注册到Selector上,并监听OP_ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已启动,等待客户端连接...");

        while (true) {
            // 阻塞,等待事件发生
            selector.select();

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                // 处理连接请求事件
                if (key.isAcceptable()) {
                    SocketChannel client = serverSocketChannel.accept();
                    client.configureBlocking(false);
                    // 监听OP_ACCEPT事件
                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    client.getRemoteAddress();
                    // 分配缓存区容量
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();

                    Socket socket = client.socket();
                    InetAddress inetAddress = socket.getInetAddress();
                    int port = socket.getPort();
                    String clientInfo = inetAddress + ":" + port;
                    String message = String.format("来自客户端 %s , 消息:%s", clientInfo , output);
                    System.out.println(message);

                    System.out.print("回复消息: ");
                    writeMessage(selector, client, buffer);
                }

                keyIterator.remove();
            }
        }
    }

    private static void writeMessage(Selector selector, SocketChannel client, ByteBuffer buffer) throws IOException {
        Scanner scanner = new Scanner(System.in);
        String message = scanner.nextLine();
        buffer.clear();
        buffer.put(message.getBytes());
        // 从写模式切换到读模式
        buffer.flip();
        while (buffer.hasRemaining()) {
            client.write(buffer);
        }

        // 重新监听OP_ACCEPT事件
        client.register(selector, SelectionKey.OP_READ);
    }
}

客户端

public class NIOClient {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost", 9999));
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isConnectable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                    }

                    System.out.print("Enter message to server: ");
                    Scanner scanner = new Scanner(System.in);
                    String message = scanner.nextLine();
                    ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                    client.write(buffer);

                    client.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();
                    System.out.println("来自客户端的消息: " + output);

                    System.out.print("输入消息: ");
                    writeMessage(selector, client, buffer);
                }
                keyIterator.remove();
            }
        }
    }

    private static void writeMessage(Selector selector, SocketChannel client, ByteBuffer buffer) throws IOException {
        Scanner scanner = new Scanner(System.in);
        String message = scanner.nextLine();
        buffer.clear();
        buffer.put(message.getBytes());
        // 从写模式切换到读模式
        buffer.flip();
        while (buffer.hasRemaining()) {
            client.write(buffer);
        }

        // 重新监听OP_ACCEPT事件
        client.register(selector, SelectionKey.OP_READ);
    }
}

上面代码新建了两个类:服务端(NIOServer)和客户端(NIOClient), 通过上面代码和运行结果可以发现,在服务端和客户端进行通信时,我们并没有新建线程类进行通信,这也是 NIO 和 BIO 最大的区别之一。

需要注意的是,虽然 NIO 提高了系统的并发性能和伸缩性,但也带来了更高的编程复杂度和更难的调试问题。因此,在使用 Java NIO 时,需要仔细考虑其适用场景和编程模型。

AIO

Java AIO(Asynchronous I/O)是Java提供的异步非阻塞 I/O 编程模型,从 Java 7 版本开始支持,AIO 又称 NIO 2.0。

相比于 NIO 模型,AIO 模型更进一步地实现了异步非阻塞 I/O,提高了系统的并发性能和伸缩性。在 NIO 模型中,虽然可以通过多路复用器处理多个连接请求,但仍需要在每个连接上进行读写操作,这仍然存在一定的阻塞。而在 AIO 模型中,所有的 I/O 操作都是异步的,不会阻塞任何线程,可以更好地利用系统资源。

AIO模型有以下特性:

  1. 异步能力:AIO 模型的最大特性是异步能力,对于 socket 和 I/O 操作都有效。读写操作都是异步的,完成后会自动调用回调函数。

  2. 回调函数:在 AIO 模型中,当一个异步操作完成后,会通知相关线程进行后续处理,这种处理方式称为“回调”。回调函数可以由开发者自行定义,用于处理异步操作的结果。

  3. 非阻塞:AIO 模型实现了完全的异步非阻塞 I/O,不会阻塞任何线程,可以更好地利用系统资源。

  4. 高性能:由于 AIO 模型的异步能力和非阻塞特性,它可以更好地处理高并发、高伸缩性的网络通信场景,进一步提高系统的性能和效率。

  5. 操作系统支持:AIO 模型需要操作系统的支持,因此在不同的操作系统上可能会有不同的表现。在 Linux 内核 2.6 版本之后增加了对真正异步 I/O 的实现。

优缺点

  • 优点:

    • 非阻塞:AIO 的主要优点是它是非阻塞的。这意味着在读写操作进行时,程序可以继续执行其他任务。这对于需要处理大量并发连接的高性能服务器来说是非常有用的。

    • 高效:由于 AIO 可以处理大量并发连接,因此它通常比同步 I/O(例如Java的传统 I/O 和 NIO)更高效。

    • 简化编程模型:AIO 使用了回调函数,这使得编程模型相对简单。当一个操作完成时,会自动调用回调函数,无需程序员手动检查和等待操作的完成。

  • 缺点:

    • 复杂性:虽然 AIO 的编程模型相对简单,但是由于其非阻塞的特性,编程复杂性可能会增加。例如,需要处理操作完成的通知,以及可能的并发问题。

    • 资源消耗:AIO 可能会消耗更多的系统资源。因为每个操作都需要创建一个回调函数,如果并发连接数非常大,可能会消耗大量的系统资源。

    • 可移植性:AIO 在某些平台上可能不可用或者性能不佳。因此,如果需要跨平台的可移植性,可能需要考虑使用其他 I/O 模型。

AIO 适合一些极端的、超高频的、超长连接的通信场景,例如云计算、大数据等。

需要注意的是,目前 AIO 模型还没有广泛应用,Netty 等网络框架仍然是基于 NIO 模型。

示例代码

服务端

public class AIOServer {
    public static void main(String[] args) throws Exception {
        // 创建一个新的异步服务器套接字通道,绑定到指定的端口上
        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));
        System.out.println("服务端启动成,等待客户端连接。");
        // 开始接受新的客户端连接
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void att) {
                // 当一个新的连接完成时,再次接受新的客户端连接
                serverChannel.accept(null, this);

                // 创建一个新的缓冲区来读取数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                try {
                    InetSocketAddress clientAddress = (InetSocketAddress) clientChannel.getRemoteAddress();
                    InetAddress clientIP = clientAddress.getAddress();
                    int clientPort = clientAddress.getPort();
                    System.out.println("客户端 "+ clientIP + ":" + clientPort + " 连接成功。");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 从异步套接字通道中读取数据
                clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to accept a connection");
            }
        });
        // 保持服务器开启
        Thread.sleep(Integer.MAX_VALUE);
    }
}

读处理程序

public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {

    private AsynchronousSocketChannel channel;

    public ReadCompletionHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 当读取完成时,反转缓冲区并打印出来
        attachment.flip();
        byte[] bytes = new byte[attachment.remaining()];
        attachment.get(bytes);
        System.out.println("收到的消息: " + new String(bytes , StandardCharsets.UTF_8));
        attachment.clear();

        // 从键盘读取输入
        Scanner scanner = new Scanner(System.in);
        System.out.print("输入消息: ");
        String message = scanner.nextLine();
        System.out.println();
        // 写入数据到异步套接字通道
        channel.write(ByteBuffer.wrap(message.getBytes()));

        channel.read(attachment , attachment , new ReadCompletionHandler(channel));
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("Failed to read message");
    }
}

客户端

public class AIOClient {
    public static void main(String[] args) throws Exception {
        // 创建一个新的异步套接字通道
        AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();

        // 连接到服务器
        clientChannel.connect(new InetSocketAddress("localhost", 5000), null, new CompletionHandler<Void, Void>() {
            @Override
            public void completed(Void result, Void attachment) {
                System.out.println("连接到服务端成功。");
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to connect server");
            }
        });
        
        // 从键盘读取输入
        Scanner scanner = new Scanner(System.in);
        System.out.print("发送消息: ");
        String message = scanner.nextLine();

        // 写入数据到异步套接字通道
        clientChannel.write(ByteBuffer.wrap(message.getBytes()), null, new CompletionHandler<Integer, Void>() {
            @Override
            public void completed(Integer result, Void attachment) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to write message");
            }
        });

        // 保持客户端开启
        Thread.sleep(Integer.MAX_VALUE);
    }
}

上述示例代码中,通过一个服务端(AIO Server)和 3 个客户端(AIO Client)的通信,简单演示了 AIO 的使用。可以发现,AIO 和 NIO 的使用方式基本一致,数据都是从 Channel 读入 Buffer,或者从 Buffer 写入 Channel 中,不同的是 AIO 是实现了异步非阻塞。

总结

Java 中的 BIO、NIO 和 AIO 都是处理输入/输出(I/O)操作的模型,但它们在处理方式和效率上有所不同。

  1. BIO(Blocking I/O):BIO 是最传统的 I/O 模型,它的操作都是阻塞的。这意味着,当一个线程发起一个 I/O 操作后,必须等待操作完成才能进行其他任务。因此,BIO 在处理大量并发连接时效率较低,但其编程模型简单。

  2. NIO(Non-blocking I/O):NIO 是非阻塞的 I/O 模型,它允许线程在等待 I/O 操作完成时进行其他任务。NIO 引入了 Channel 和 Buffer 的概念,以及 Selector 用于多路复用。NIO 适合处理大量并发连接,但其编程模型相对复杂。

  3. AIO(Asynchronous I/O):AIO 是真正的异步 I/O 模型,应用程序无需等待 I/O 操作的完成,当操作完成时,操作系统会通知应用程序。AIO 使用回调函数或Future对象来获取操作结果,适合处理大量并发连接,其编程模型相对简单。

总之,BIO、NIO 和 AIO 各有优缺点,适用的场景也不同。BIO 适合连接数目较少且固定的架构,NIO 适合连接数目多,但是并发读写操作相对较少的场景,AIO 则适合连接数目多,且并发读写操作也多的场景。在选择使用哪种 I/O 模型时,需要根据具体的应用场景和需求进行权衡。