BIO、NIO、AIO

同步和异步

同步和异步是指应用程序与内核交互的方式:

  • 同步 (Synchronize):用户进程触发IO操作后,必须等待IO操作完成或轮询检查IO操作是否就绪。例如:传统的BIO模式中,当调用read()方法时,必须等到数据真正从内核空间拷贝到用户空间后才能返回。

  • 异步 (Asychronize):当异步调用发出后,调用者不会立即得到结果。被调用者通过状态改变通知(如NIO的Selector机制)或回调函数处理(如AIO的CompletionHandler)通知调用者。

阻塞与非阻塞

阻塞和非阻塞是针对进程访问数据时的行为方式:

  • 阻塞:读写操作会一直等待,直到IO操作完成。在数据未准备好时,调用线程会被挂起。优点是编程模型简单,缺点是资源利用率低。

  • 非阻塞:读写操作立即返回状态值,不会等待。优点是提高系统吞吐量,缺点是需要轮询检查状态,增加CPU负担。

组合应用场景

  1. 同步阻塞(BIO):典型实现是Java传统Socket,适用场景是连接数少且固定的架构。
  2. 同步非阻塞(NIO):典型实现是Java NIO的Selector机制,适用场景是高并发连接但连接时间短的架构(如聊天服务器)。
  3. 异步非阻塞(AIO):典型实现是Java AIO,适用场景是高并发连接且连接时间长的架构(如文件服务器)。

注意:不存在”异步阻塞”的组合,因为异步本身就意味着不阻塞调用线程。


BIO (Blocking I/O)

基本介绍

BIO(Blocking I/O)即同步阻塞I/O模型,其中的”B”代表Blocking(阻塞)。这是Java1.4版本之前唯一的I/O模型选择,也是传统网络编程中最基础的I/O处理方式。

工作原理

服务器实现采用”一个连接一个线程”的模式。当客户端发起连接请求时:

  1. 服务端必须为每个新连接分配一个独立的线程进行处理
  2. 该线程会全程负责该连接的所有I/O操作
  3. 在I/O操作(如read/write)期间,线程会一直阻塞等待,直到操作完成

性能特点

  • 资源开销大:每个连接都需要独立的线程,线程创建和上下文切换成本高
  • 简单易用:编程模型直观,易于理解和实现
  • 效率低下:当连接数增加时,线程数量线性增长,系统资源很快耗尽

适用场景

  • 客户端连接数量较少且固定的应用
  • 对延迟不敏感的服务
  • 需要快速开发的原型系统
  • 教学示例和简单演示程序

服务端代码

package icu.wzk.io;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class WzkIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 8889));
        while (true) {
            Socket socket = serverSocket.accept();
            new Thread(() -> {
                try {
                    byte[] bytes = new byte[1024];
                    int len = socket.getInputStream().read(bytes);
                    System.out.println(new String(bytes, 0, len));

                    socket.getOutputStream().write(bytes, 0, len);
                    socket.getOutputStream().flush();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}

客户端代码

package icu.wzk.io;

import java.net.Socket;

public class WzkIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8889);
        socket.getOutputStream().write("hello".getBytes());
        socket.getOutputStream().flush();
        System.out.println("============ send ===========");

        byte[] bytes = new byte[1024];
        int len = socket.getInputStream().read(bytes);
        System.out.println("server: " + new String(bytes, 0, len));
        socket.close();
    }

}

NIO (New I/O)

基本介绍

NIO(Non-blocking I/O 或 New I/O)是 Java 从 JDK1.4 版本开始提供的一套全新 I/O API,用于替代传统的阻塞式 I/O。与传统的 I/O 相比,NIO 提供了更高效、更灵活的 I/O 处理方式。

NIO 的核心特点:

  • 同步非阻塞:线程不会因为等待 I/O 操作完成而被阻塞
  • 基于通道和缓冲区的 I/O 方式
  • 使用单线程管理多个连接的能力

服务器实现模式:NIO 采用”一个请求一个通道”的模式,所有客户端连接请求都会注册到多路复用器(Selector)上。多路复用器不断轮询这些通道,当某个通道有 I/O 请求时,才会启动一个线程进行处理。

通道 (Channels)

通道是 NIO 引入的最重要的抽象概念,它代表了一个开放的数据连接,可以用于读取和写入数据。与流不同,通道是双向的,可以同时用于读写操作。

主要特点:

  • 数据可以从 Channel 读取到 Buffer 中
  • 数据可以从 Buffer 写入到 Channel 中
  • 支持异步非阻塞操作
  • 提供多种实现类型(如 FileChannel、SocketChannel 等)

缓冲区 (Buffers)

缓冲区是 NIO 用于存储数据的容器,所有数据都是通过缓冲区进行传输的。

缓冲区关键操作:

  1. 写入数据到 Buffer
  2. 调用 flip() 切换为读模式
  3. 从 Buffer 中读取数据
  4. 调用 clear() 或 compact() 清空缓冲区

选择器 (Selectors)

选择器是 NIO 实现多路复用的关键组件,允许单线程处理多个通道。

工作原理:

  1. 将通道注册到选择器上
  2. 选择器不断轮询已注册的通道
  3. 当通道上有感兴趣的事件发生时,选择器会通知应用程序
  4. 应用程序处理这些事件

主要事件类型:

  • SelectionKey.OP_CONNECT(连接就绪)
  • SelectionKey.OP_ACCEPT(接受就绪)
  • SelectionKey.OP_READ(读就绪)
  • SelectionKey.OP_WRITE(写就绪)

服务端代码

package icu.wzk.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;

public class WzkNIOServer extends Thread {

    private Selector selector;

    private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);

    private final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    public WzkNIOServer(int port) {
        init(port);
    }

    private void init(int port) {
        try {
            System.out.println("Server 启动");
            this.selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
            System.out.println("Server 启动完成");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                this.selector.select();
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();
                    keys.remove();
                    if (key.isValid()) {
                        try {
                            if (key.isAcceptable()) {
                                accept(key);
                            }
                            if (key.isReadable()) {
                                read(key);
                            }
                            if (key.isWritable()) {
                                write(key);
                            }
                        } catch (CancelledKeyException e) {
                            key.cancel();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    private void accept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(this.selector, SelectionKey.OP_READ);
            System.out.println("客户端连接成功: " + socketChannel.getRemoteAddress());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void read(SelectionKey key) {
        try {
            this.readBuffer.clear();
            SocketChannel socketChannel = (SocketChannel) key.channel();
            int readLen = socketChannel.read(this.readBuffer);
            if (readLen == -1) {
                key.channel().close();
                return;
            }

            this.readBuffer.flip();
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            System.out.println("从客户端接收到了: " + socketChannel.getRemoteAddress() + " " + new String(bytes, StandardCharsets.UTF_8));

            socketChannel.register(this.selector, SelectionKey.OP_WRITE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void write(SelectionKey key) {
        try {
            this.writeBuffer.clear();
            SocketChannel socketChannel = (SocketChannel) key.channel();
            Scanner scanner = new Scanner(System.in);
            System.out.println("发送数据到客户端, 输入后回车");
            String line = scanner.nextLine();
            this.writeBuffer.put(line.getBytes(StandardCharsets.UTF_8));
            writeBuffer.flip();
            socketChannel.write(writeBuffer);

            socketChannel.register(this.selector, SelectionKey.OP_READ);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(new WzkNIOServer(8889)).start();
    }

}

客户端代码

package icu.wzk.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;


public class WzkNIOClient {
    public static void main(String[] args) {
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8889);
        SocketChannel socketChannel = null;
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(inetSocketAddress);
            Scanner sc = new Scanner(System.in);
            while (true) {
                System.out.println("客户端给服务器发消息");
                String line = sc.nextLine();
                if ("exit".equals(line)) {
                    break;
                }
                byteBuffer.put(line.getBytes(StandardCharsets.UTF_8));
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();

                int len = socketChannel.read(byteBuffer);
                if (len == -1) {
                    socketChannel.close();
                    break;
                }
                byteBuffer.flip();

                byte[] bytes = new byte[byteBuffer.remaining()];
                byteBuffer.get(bytes);
                System.out.println("服务器数据: " + new String(bytes, StandardCharsets.UTF_8));
                byteBuffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != socketChannel) {
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}