菜单

Administrator
发布于 2026-05-18 / 1 阅读
0
0

NIO 和 BIO

Java IO 模型深度解析:BIO、NIO 与 AIO

前言

Java 中的 IO 模型是每位后端开发者必须掌握的核心知识。从传统的 BIO(Blocking I/O)NIO(Non-blocking I/O),再到 AIO(Asynchronous I/O),Java 的 I/O 模型经历了三次重要演进。本文将深入剖析这三种模型的设计思想、实现原理和适用场景,并通过完整代码示例帮助你真正理解它们的区别。


一、BIO — 阻塞 I/O 模型

1.1 什么是 BIO

BIO(Blocking I/O)即同步阻塞 I/O 模型,是 Java 最早的 I/O 方式。在 java.net 包中,ServerSocketSocket 是核心类。服务端每接受一个客户端连接,就需要一个线程去处理该连接上的读写操作。

核心特点:

  • 一个连接对应一个线程
  • 线程在 accept()read()write() 等操作时会阻塞
  • 数据未就绪时线程挂起,不占用 CPU 但占用系统资源

1.2 BIO 的工作机制

客户端1 ──→ 服务端主线程 ──→ 线程1(处理客户端1)
客户端2 ──→               ──→ 线程2(处理客户端2)
客户端3 ──→               ──→ 线程3(处理客户端3)

主线程负责 accept 新的连接,每 accept 一个连接就启动一个新线程处理该连接的读写。

1.3 传统 BIO 编程示例

服务端代码

package com.example.bio;

import java.io.*;
import java.net.*;

/**
 * 传统 BIO 服务端
 * 每个客户端连接对应一个独立线程
 */
public class BioServer {

    private static final int PORT = 8080;

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("BIO Server 启动,监听端口:" + PORT);

            while (true) {
                // ★ 阻塞点1:accept() 阻塞,直到有客户端连接
                Socket socket = serverSocket.accept();
                System.out.println("接收到客户端连接:" + socket.getRemoteSocketAddress());

                // 每个连接启动一个新线程处理
                new Thread(new ClientHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static class ClientHandler implements Runnable {
        private final Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
                 PrintWriter writer = new PrintWriter(
                     socket.getOutputStream(), true)) {

                String message;
                // ★ 阻塞点2:readLine() 阻塞,直到读取到一行数据
                while ((message = reader.readLine()) != null) {
                    System.out.println("收到消息:" + message);
                    writer.println("服务端回复:" + message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端代码

package com.example.bio;

import java.io.*;
import java.net.*;

/**
 * BIO 客户端
 */
public class BioClient {

    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8080;

    public static void main(String[] args) {
        try (Socket socket = new Socket(HOST, PORT);
             BufferedReader reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             PrintWriter writer = new PrintWriter(
                 socket.getOutputStream(), true);
             BufferedReader consoleReader = new BufferedReader(
                 new InputStreamReader(System.in))) {

            System.out.println("已连接到服务器,请输入消息:");

            String input;
            while ((input = consoleReader.readLine()) != null) {
                writer.println(input);
                String response = reader.readLine();
                System.out.println("服务器响应:" + response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.4 BIO 的痛点

问题 说明
线程开销大 每个连接一个线程,线程创建销毁和上下文切换成本高
连接数受限 系统线程数量有限,当连接数达到数千时性能急剧下降
资源浪费 大量线程在等待 I/O 时阻塞,仅占内存却不做实际工作
伸缩性差 无法有效支撑高并发场景(如 C10K 问题)

伪代码示意 BIO 的阻塞本质:

// 用户态视角
while (true) {
    // 线程在此处阻塞 —— 操作系统将线程标记为 WAITING
    // 移出调度队列,直到有数据到达才唤醒
    byte[] data = socket.read();
    process(data);
}

在操作系统层面,阻塞 I/O 会将线程从运行队列移入等待队列,数据到达后再通过中断或 select/epoll 唤醒线程。这个过程涉及 用户态 ↔ 内核态 的切换。


二、NIO — 非阻塞 I/O 模型

2.1 什么是 NIO

NIO(Non-blocking I/O / New I/O)是 JDK 1.4 引入的全新 I/O 库(java.nio 包),提供了面向缓冲区(Buffer)通道(Channel) 的 I/O 方式,并引入了多路复用器(Selector) 实现单线程管理多个连接。

核心思想: 一个线程可以管理成千上万个连接,通过事件驱动机制只在数据真正就绪时才进行实际读写操作。

2.2 NIO 三大核心组件

┌─────────────────────────────────────────────┐
│                  NIO 架构                     │
│                                              │
│  线程 ──→ Selector                            │
│            │     │     │                      │
│      SelectionKey  SelectionKey  SelectionKey │
│            │     │     │                      │
│       ChannelA  ChannelB  ChannelC            │
│         │         │         │                 │
│      Buffer    Buffer    Buffer               │
│         │         │         │                 │
│      Data      Data      Data                 │
└─────────────────────────────────────────────┘

2.2.1 Channel(通道)

Channel 类似传统 IO 中的流(Stream),但有以下区别:

传统 Stream Channel
单向(InputStream / OutputStream) 双向(可读可写)
基于字节/字符流 基于缓冲区(Buffer)
阻塞式 可配置为非阻塞模式
不支持多路复用 可与 Selector 配合使用

常用 Channel 类型:

// 文件通道(阻塞模式,不支持 Selector)
FileChannel fileChannel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);

// 服务端套接字通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();

// 客户端套接字通道
SocketChannel socketChannel = SocketChannel.open();

// UDP 数据报通道
DatagramChannel datagramChannel = DatagramChannel.open();

2.2.2 Buffer(缓冲区)

Buffer 是 NIO 中数据读写的载体。所有数据都通过 Buffer 进行读写,Channel 从 Buffer 读或向 Buffer 写。

Buffer 的核心属性:

public abstract class Buffer {
    // 容量 —— 缓冲区最大大小,一旦创建不可改变
    private int capacity;
    
    // 读写位置 —— 下一个要读写的位置索引
    private int position;
    
    // 界限 —— 缓冲区中有效数据的末尾位置
    private int limit;
    
    // 标记 —— 用于记录 position 位置,配合 reset() 使用
    private int mark;
}

Buffer 的四个状态:

初始状态 (allocate):
position=0, limit=capacity, capacity=10
[ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ]
  ↑
  pos, limit

写入 5 个字节后:
position=5, limit=capacity=10
[ H | e | l | l | o |   |   |   |   |   ]
                      ↑
                      pos         limit

调用 flip() 切换为读模式:
position=0, limit=5
[ H | e | l | l | o |   |   |   |   |   ]
  ↑                   ↑
  pos                limit

读取 3 个字节后:
position=3, limit=5
[ H | e | l | l | o |   |   |   |   |   ]
              ↑       ↑
              pos    limit

Buffer API 示例:

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class BufferExample {
    public static void main(String[] args) {
        // 1. 分配缓冲区(堆内存)
        ByteBuffer buffer = ByteBuffer.allocate(10);
        printBuffer("初始状态", buffer);    // pos=0, lim=10, cap=10

        // 2. 写入数据
        buffer.put((byte) 'H');
        buffer.put((byte) 'e');
        buffer.put((byte) 'l');
        buffer.put((byte) 'l');
        buffer.put((byte) 'o');
        printBuffer("写入5字节后", buffer); // pos=5, lim=10, cap=10

        // 3. 翻转 —— 从写模式切换到读模式
        buffer.flip();
        printBuffer("flip()后", buffer);    // pos=0, lim=5, cap=10

        // 4. 读取数据
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        System.out.println();
        printBuffer("读取后", buffer);       // pos=5, lim=5, cap=10

        // 5. 重绕 —— 重新从头读
        buffer.rewind();
        printBuffer("rewind()后", buffer);  // pos=0, lim=5, cap=10

        // 6. clear() —— 清空缓冲区(只是重置位置,不清除数据)
        buffer.clear();
        printBuffer("clear()后", buffer);    // pos=0, lim=10, cap=10

        // 7. compact() —— 只清除已读数据,未读数据保留并移动至开头
        buffer.put("Hello World!".getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        buffer.get(); // 读走 'H'
        buffer.get(); // 读走 'e'
        buffer.compact();
        printBuffer("compact()后", buffer);  // "llo World!" 保留
    }

    private static void printBuffer(String msg, ByteBuffer buf) {
        System.out.printf("%s → position=%d, limit=%d, capacity=%d%n",
                msg, buf.position(), buf.limit(), buf.capacity());
    }
}

Buffer 类型一览:

Buffer 类型 描述
ByteBuffer 最常用,可存储字节
CharBuffer 存储字符
ShortBuffer 存储 short 类型
IntBuffer 存储 int 类型
LongBuffer 存储 long 类型
FloatBuffer 存储 float 类型
DoubleBuffer 存储 double 类型
MappedByteBuffer 内存映射文件缓冲区(FileChannel.map())

2.2.3 Selector(选择器 / 多路复用器)

Selector 是 NIO 的核心。它允许一个线程同时监控多个 Channel 的 I/O 事件(连接、读取、写入等),实现单线程管理多连接

Selector 工作原理:

Selector selector = Selector.open();
channel.configureBlocking(false);  // 必须配置为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

可注册的事件类型:

事件 SelectionKey 常量 触发条件
连接就绪 OP_CONNECT 客户端连接成功
接受连接 OP_ACCEPT 服务端接收到新连接
可读 OP_READ 通道中有数据可读
可写 OP_WRITE 通道可以写入数据

SelectionKey 对象:

// 注册时返回的 SelectionKey 包含了通道和选择器的关联信息
SelectionKey key = channel.register(selector, ops);

// 常用方法
key.channel();          // 获取关联的通道
key.selector();         // 获取关联的选择器
key.interestOps();      // 感兴趣的事件集合
key.readyOps();         // 已就绪的事件集合
key.attachment();       // 获取附件对象(可用来存储会话状态)

// 判断事件类型
key.isAcceptable();     // 有新的连接可以接受
key.isConnectable();    // 连接完成
key.isReadable();       // 有数据可读
key.isWritable();       // 可以写入数据

2.3 NIO 完整服务端示例

以下是一个基于 NIO 实现的 Echo 服务器,展示 Selector + Channel + Buffer 的完整工作流程:

package com.example.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * 基于 NIO 的 Echo 服务器
 * 单线程管理多个客户端连接
 */
public class NioEchoServer {

    private static final int PORT = 8080;
    private static final int BUFFER_SIZE = 1024;

    private Selector selector;
    private ServerSocketChannel serverChannel;
    private ByteBuffer readBuffer;

    public void start() throws IOException {
        // 1. 打开 Selector
        selector = Selector.open();

        // 2. 打开 ServerSocketChannel
        serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);  // ★ 必须非阻塞
        serverChannel.bind(new InetSocketAddress(PORT));
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO Server 启动,监听端口:" + PORT);

        // 3. 初始化读缓冲区
        readBuffer = ByteBuffer.allocate(BUFFER_SIZE);

        // 4. 事件循环
        while (true) {
            // ★ 核心方法:select() 阻塞,直到至少一个 Channel 有事件就绪
            int readyCount = selector.select();
            if (readyCount == 0) {
                continue;
            }

            // 获取就绪的 SelectionKey 集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

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

                try {
                    // 处理就绪事件
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    } else if (key.isWritable()) {
                        handleWrite(key);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    key.cancel();
                    key.channel().close();
                }

                // ★ 必须移除已处理的 key,否则下次 select() 会重复返回
                keyIterator.remove();
            }
        }
    }

    /**
     * 处理新连接:OP_ACCEPT
     */
    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = ssc.accept();
        clientChannel.configureBlocking(false);

        // 为新连接注册 OP_READ 事件
        clientChannel.register(selector, SelectionKey.OP_READ);

        System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
    }

    /**
     * 处理可读事件:OP_READ
     */
    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();

        // 清空缓冲区,准备读取新数据
        readBuffer.clear();

        int bytesRead = clientChannel.read(readBuffer);

        if (bytesRead == -1) {
            // 客户端关闭连接
            System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
            key.cancel();
            clientChannel.close();
            return;
        }

        // 切换为读模式,读取数据
        readBuffer.flip();
        byte[] data = new byte[readBuffer.remaining()];
        readBuffer.get(data);
        String message = new String(data, StandardCharsets.UTF_8);
        System.out.println("收到消息 [" + clientChannel.getRemoteAddress() + "]: " + message);

        // ★ 将回复数据附加到 key 上,同时注册 OP_WRITE 事件
        byte[] responseBytes = ("服务端回复: " + message).getBytes(StandardCharsets.UTF_8);
        ByteBuffer writeBuffer = ByteBuffer.wrap(responseBytes);
        key.attach(writeBuffer);

        // 修改感兴趣的事件为 OP_WRITE
        key.interestOps(SelectionKey.OP_WRITE);
    }

    /**
     * 处理可写事件:OP_WRITE
     */
    private void handleWrite(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer writeBuffer = (ByteBuffer) key.attachment();

        if (writeBuffer == null) {
            key.interestOps(SelectionKey.OP_READ);
            return;
        }

        // 将缓冲区数据写入通道
        while (writeBuffer.hasRemaining()) {
            clientChannel.write(writeBuffer);
        }

        // ★ 写入完成后切换回 OP_READ,等待下一条消息
        key.interestOps(SelectionKey.OP_READ);
        key.attach(null); // 清理附件
    }

    public static void main(String[] args) throws IOException {
        new NioEchoServer().start();
    }
}

2.4 NIO 客户端示例

package com.example.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.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

/**
 * 基于 NIO 的客户端
 */
public class NioClient {

    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8080;

    private Selector selector;
    private SocketChannel channel;

    public void start() throws IOException {
        selector = Selector.open();
        channel = SocketChannel.open();
        channel.configureBlocking(false);

        // 连接服务器(非阻塞)
        channel.connect(new InetSocketAddress(HOST, PORT));
        channel.register(selector, SelectionKey.OP_CONNECT);

        System.out.println("连接服务器 " + HOST + ":" + PORT);

        // 读线程
        new Thread(this::readLoop).start();

        // 写 —— 从控制台读取输入并发送
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String input = scanner.nextLine();
            if ("quit".equalsIgnoreCase(input)) {
                break;
            }
            ByteBuffer buffer = ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8));
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
        }

        channel.close();
        selector.close();
    }

    private void readLoop() {
        try {
            while (true) {
                int readyCount = selector.select();
                if (readyCount == 0) continue;

                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();

                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();

                    if (key.isConnectable()) {
                        handleConnect(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void handleConnect(SelectionKey key) throws IOException {
        SocketChannel sc = (SocketChannel) key.channel();
        if (sc.finishConnect()) {
            System.out.println("连接成功!");
            sc.register(selector, SelectionKey.OP_READ);
        }
    }

    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = sc.read(buffer);

        if (bytesRead == -1) {
            sc.close();
            return;
        }

        buffer.flip();
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        System.out.println("服务器响应:" + new String(data, StandardCharsets.UTF_8));
    }

    public static void main(String[] args) throws IOException {
        new NioClient().start();
    }
}

2.5 NIO 关键方法详解

select() 方法

// 阻塞直到至少一个 Channel 就绪
int select();

// 阻塞最多 timeout 毫秒
int select(long timeout);

// 非阻塞,立即返回就绪 Channel 数量
int selectNow();

关于 OP_WRITE 事件

这是一个容易被误解的事件。大多数情况下通道都是可写的,如果注册了 OP_WRITE 事件,select() 几乎会立即返回。正确的用法是:仅在真正需要写入数据时才注册 OP_WRITE,写入完成后立即改回 OP_READ

// ❌ 错误:一开始就注册 WRITE
channel.register(selector, SelectionKey.OP_WRITE);

// ✅ 正确:有数据要发送时才注册,发送完后切回 READ
key.interestOps(SelectionKey.OP_WRITE);
// ... 发送数据 ...
key.interestOps(SelectionKey.OP_READ);

三、BIO 与 NIO 全面对比

3.1 对比表格

维度 BIO NIO
IO 模型 同步阻塞 同步非阻塞
编程模型 一个连接一个线程 一个线程管理多个连接(多路复用)
核心类 ServerSocket, Socket ServerSocketChannel, SocketChannel, Selector
数据载体 流(Stream,单向) 缓冲区(Buffer,双向)
资源消耗 线程数随连接数线性增长,高 线程数恒定(通常 1~CPU核心数),低
并发能力 连接数几百左右达到瓶颈 支持数千到数万连接(C10K 级别)
适用场景 低并发、短连接、固定连接数 高并发、长连接、连接数波动大
编程复杂度 低,直观 较高,需处理粘包/半包、状态管理
JDK 版本 JDK 1.0+ JDK 1.4+
操作系统支持 所有系统 需系统支持非阻塞 IO

3.2 线程模型对比

BIO 线程模型(连接数 = 线程数):
┌──────────┐    ┌──────────┐
│ Thread 1 │◄──►│ Client 1 │
├──────────┤    ├──────────┤
│ Thread 2 │◄──►│ Client 2 │
├──────────┤    ├──────────┤
│ Thread 3 │◄──►│ Client 3 │
├──────────┤    ├──────────┤
│   ...    │    │   ...    │
└──────────┘    └──────────┘

NIO 线程模型(单线程管理 N 连接):
┌──────────┐
│  Thread  │◄── Selector ──► Channel 1 (Client 1)
│          │                ├── Channel 2 (Client 2)
│          │                ├── Channel 3 (Client 3)
│          │                ├── ...
│          │                └── Channel N (Client N)
└──────────┘

3.3 消息读取方式对比

// BIO:面向流,直接读取
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);  // 阻塞直到有数据

// NIO:面向缓冲区,通过 Channel 读取
SocketChannel channel = socket.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);  // 可能立即返回 0
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);

3.4 性能对比参考

连接数 BIO 响应时间 NIO 响应时间 说明
100 5ms 3ms 低负载下差异不大
1,000 120ms 8ms BIO 开始出现明显线程竞争
5,000 超时/拒绝连接 15ms BIO 线程数达到系统瓶颈
10,000 无法运行 25ms NIO 依然稳定

注:以上数据为实验室环境下的参考值,实际表现取决于硬件、操作系统和 JVM 参数。


四、AIO — 异步 I/O 模型

4.1 什么是 AIO

AIO(Asynchronous I/O)即异步非阻塞 I/O,JDK 1.7 引入(NIO.2)。与 NIO 的区别在于:

  • NIO(同步非阻塞): 用户线程通过 Selector 轮询数据是否就绪,就绪后自己读取
  • AIO(异步非阻塞): 用户线程发出 I/O 请求后直接返回,数据就绪后由操作系统回调通知

4.2 AIO 的工作方式

// AIO 核心类
AsynchronousServerSocketChannel  // 服务端异步通道
AsynchronousSocketChannel        // 客户端异步通道
AsynchronousFileChannel          // 文件异步通道
CompletionHandler                // 回调处理器

4.3 AIO 服务端示例

package com.example.aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * AIO 服务端示例
 */
public class AioServer {

    private static final int PORT = 8080;

    public void start() throws IOException {
        AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(PORT));

        System.out.println("AIO Server 启动,监听端口:" + PORT);

        // 方式一:使用 Future(轮询方式)
        // serverChannel.accept();

        // 方式二:使用 CompletionHandler(推荐)
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                // 接受下一个连接(实现链式调用)
                serverChannel.accept(null, this);

                System.out.println("新客户端连接:" + getRemoteAddress(clientChannel));

                // 分配缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);

                // 异步读取
                clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                System.err.println("接受连接失败:" + exc.getMessage());
                serverChannel.accept(null, this);
            }
        });

        System.out.println("服务器已启动,按任意键停止...");
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取操作的 CompletionHandler
     */
    private static class ReadCompletionHandler
            implements CompletionHandler<Integer, ByteBuffer> {

        private final AsynchronousSocketChannel clientChannel;

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

        @Override
        public void completed(Integer bytesRead, ByteBuffer buffer) {
            if (bytesRead == -1) {
                closeChannel();
                return;
            }

            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data, StandardCharsets.UTF_8);
            System.out.println("收到消息: " + message);

            // 准备回复数据
            byte[] response = ("回复: " + message).getBytes(StandardCharsets.UTF_8);
            ByteBuffer writeBuffer = ByteBuffer.wrap(response);

            // 异步写入
            clientChannel.write(writeBuffer, writeBuffer,
                    new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer attachment) {
                            if (attachment.hasRemaining()) {
                                clientChannel.write(attachment, attachment, this);
                            } else {
                                // 写入完成,继续读取下一条
                                ByteBuffer newBuffer = ByteBuffer.allocate(1024);
                                clientChannel.read(newBuffer, newBuffer,
                                        new ReadCompletionHandler(clientChannel));
                            }
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            System.err.println("写入失败:" + exc.getMessage());
                            closeChannel();
                        }
                    });
        }

        @Override
        public void failed(Throwable exc, ByteBuffer buffer) {
            System.err.println("读取失败:" + exc.getMessage());
            closeChannel();
        }

        private void closeChannel() {
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static String getRemoteAddress(AsynchronousSocketChannel channel) {
        try {
            return channel.getRemoteAddress().toString();
        } catch (IOException e) {
            return "unknown";
        }
    }

    public static void main(String[] args) throws IOException {
        new AioServer().start();
    }
}

4.4 AIO 的适用场景

场景 适用性 说明
文件 I/O ✅ 非常适合 操作系统原生支持异步文件读写
高吞吐网络 ⚠️ 谨慎选择 Linux 下 AIO 实现不够成熟,底层仍用 epoll
低延迟要求 ❌ 不如 NIO AIO 的回调处理有一定额外开销
简单编程模型 ✅ 方便 回调方式比 NIO 的轮询更易理解

4.5 三种模型的演进关系

BIO (JDK 1.0)
 │
 ├── 同步阻塞,一个连接一个线程
 ├── 编程简单,适合低并发
 │
 ▼
NIO (JDK 1.4)
 │
 ├── 同步非阻塞,多路复用
 ├── Channel + Buffer + Selector
 ├── 单线程管理数千连接
 │
 ▼
AIO (JDK 1.7 / NIO.2)
 │
 ├── 异步非阻塞,回调驱动
 ├── 真正解放线程
 ├── 编程模型更简洁
 └── Linux 下底层仍依赖 epoll(非原生异步 I/O)

五、实际场景中的最佳实践

5.1 如何选择

连接数 < 500 且 短连接?
    └── BIO(简单够用)

连接数 > 500 或 长连接?
    └── NIO(推荐)
        └── 考虑使用 Netty 等高性能 NIO 框架

大量文件 I/O 且 不频繁?
    └── AIO(异步文件读写)

高吞吐网络服务器?
    └── NIO(Netty)> AIO > BIO

5.2 常见 NIO 陷阱

// 陷阱1:忘记设置非阻塞
ServerSocketChannel channel = ServerSocketChannel.open();
// channel.configureBlocking(false);  // 缺少这行会抛异常
channel.register(selector, SelectionKey.OP_ACCEPT);  // ❌ IllegalBlockingModeException

// 陷阱2:忘记移除已处理的 SelectionKey
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {      // ❌ 没有 iterator.remove()
    handle(key);
}
// 下次 select() 会再次返回同样的 key,导致无限循环

// 陷阱3:OP_WRITE 滥用
channel.register(selector, SelectionKey.OP_WRITE);  // ❌ 几乎立即就绪
// 应仅在需要写数据时临时注册

// 陷阱4:缓冲区大小不当
ByteBuffer buffer = ByteBuffer.allocate(1);  // ❌ 太小,频繁 I/O
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // ❌ 太大,浪费内存
// 通常 1024~8192 字节较为合理

5.3 生产级别的 NIO 框架

虽然直接使用 JDK NIO 能让你深入理解底层,但生产环境中更推荐使用封装好的框架:

框架 特点
Netty 最流行的 NIO 框架,Dubbo、RocketMQ 等都在用
Vert.x 反应式编程,支持多语言
Grizzly GlassFish 的 NIO 框架,成熟稳定
JDK 自带 适合学习,或简单场景

Netty 版本的 Echo 服务端(对比参考):

// Netty 的代码量比原生 NIO 少 60% 以上
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         protected void initChannel(SocketChannel ch) {
             ch.pipeline().addLast(new EchoServerHandler());
         }
     });
    ChannelFuture f = b.bind(8080).sync();
    f.channel().closeFuture().sync();
} finally {
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}

六、总结

模型 类型 线程模型 并发能力 适用场景
BIO 同步阻塞 连接 : 线程 = 1 : 1 低(~500) 连接数少、低并发、简单应用
NIO 同步非阻塞 连接 : 线程 = N : 1 高(数万) 高并发、长连接、通用网络服务
AIO 异步非阻塞 线程 : 回调 : 数据 = 1 : 1 : N 文件 I/O、对延迟不敏感的场景

一句话总结:

BIO 是「一个人端一碗饭」,NIO 是「一个人用传送带管理多碗饭」,AIO 是「饭好了叫你来吃」。

推荐学习路径

  1. 先掌握 BIO:理解传统 Socket 编程、阻塞的本质
  2. 再学习 NIO的三大核心:Buffer → Channel → Selector(按顺序)
  3. 动手实现一个简单的 NIO 服务器(反复调试理解事件驱动)
  4. 学习 Netty 框架:大量生产验证,帮你避开 NIO 的坑
  5. 拓展了解 AIO:知道存在即可,实际项目中用得较少

本文通过 Halo 博客发布,示例代码可在 GitHub 仓库 [链接] 查看完整源码。


评论