BIO (Blocking I/O) - 同步阻塞I/O

模型: 一个连接对应一个线程。比如:去餐厅点餐,点完后就站在柜台前一直等着,直到厨师做好饭递给你。期间你不能做任何事

工作方式:

  • 应用程序线程发起 read 系统调用。
  • 内核开始准备数据(比如从网卡读取)。
  • 应用程序线程被阻塞,什么都做不了,直到内核将数据准备好并拷贝到用户空间。
  • 数据准备好后,线程被唤醒,继续执行。

特点: 编程简单直观。但在高并发场景下,需要创建大量线程来处理大量连接,线程上下文切换开销巨大,消耗系统资源,性能瓶颈明显。

/**
 * BIO模型下的Echo服务器
 * 每个客户端连接都会创建一个独立的线程进行处理
 */
public class BioEchoServer {

    public static void main(String[] args) throws IOException {
		
		int port = 8888;
		ServerSocket serverSocket = new ServerSocket(port);
		System.out.println("BIO Echo服务器启动,监听端口: " + port);

		//无限循环,接受客户端连接
		while(true){
			//1.阻塞等待客户端连接(Acceptor线程)
			Socket clientSocket = serverSocket.accept();
			System.out.println("客户端连接来自: " + clientSocket.getRemoteSocketAddress());

			// 2. 为每个新的连接创建一个新线程进行处理 (Handler线程)
            new Thread(new EchoClientHandler(clientSocket)).start();
		}
	}

	/**
		处理客户端连接的线程任务
	*/
	static class EchoClientHandler implements Runnable{
		
		private final Socket clientSocket;
		public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }
		
		@Override
		public void run(){
			
			try(
				BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
				PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true)
			){
				String inputLine;

				//3.阻塞读取客户端发送的数据
				while((inputLine = in.readLine())!=null){
					System.out.println("收到消息: " + inputLine);
                    // 4. 将数据原样写回给客户端 (Echo)
                    out.println("Echo: " + inputLine);
				}
			}catch(IOException e){
				System.out.println("处理客户端连接时发生异常: " + e.getMessage());
			}finally{
				try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("客户端连接关闭: " + clientSocket.getRemoteSocketAddress());
			}
		}
	}
}

NIO (Non-blocking I/O / New I/O) - 同步非阻塞I/O

模型: 一个线程处理多个连接。比如:点完餐后,拿到一个号码牌。你可以离开柜台去玩手机。你时不时地抬头看屏幕(轮询),看看你的号码是否被叫到。叫到后,你去取餐。

工作方式 (核心:Selector + Channel + Buffer):

  • 应用程序线程发起 read 系统调用。
  • 如果内核数据还没准备好,立即返回一个错误(EWOULDBLOCK),而不是阻塞线程。
  • 应用程序线程可以继续处理其他Channel的I/O请求。
  • 应用程序通过一个叫 Selector 的组件不断轮询注册在其上的所有 Channel。
  • 当某个Channel的数据准备好后,Selector 会通知应用程序线程。
  • 线程再对这个Channel进行实际的读写操作。

特点: 大大减少了线程数量,适用于高并发、短连接(如聊天服务器、网关)。编程模型比BIO复杂。

/**
 * NIO模型下的Echo服务器
 * 使用单个Selector线程处理所有连接(Accept、Read、Write)
 */
public class NioEchoServer {

    public static void main(String[] args) throws IOException {
        // 1. 创建Selector(调度中心)
        Selector selector = Selector.open();

        // 2. 创建ServerSocketChannel并设置为非阻塞模式
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 必须设置为非阻塞
        serverSocketChannel.bind(new InetSocketAddress(8888));

        // 3. 将ServerSocketChannel注册到Selector,关注ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO Echo服务器启动,监听端口: 8888");

        // 事件循环
        while (true) {
            // 4. 阻塞等待就绪的Channel,超时时间为1秒
            if (selector.select(1000) == 0) {
                // System.out.println("等待1秒,无事件发生...");
                continue;
            }

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

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                // 6. 处理事件,并移除已处理的key
                keyIterator.remove();

                try {
                    if (key.isAcceptable()) {
                        handleAccept(key, selector);
                    }
                    if (key.isReadable()) {
                        handleRead(key);
                    }
                    // 可写事件通常不需要专门监听,只在需要时才注册
                } catch (IOException e) {
                    // 客户端异常断开连接
                    key.cancel();
                    if (key.channel() != null) {
                        key.channel().close();
                    }
                    System.out.println("客户端连接异常关闭");
                }
            }
        }
    }

    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        // 接受连接,生成SocketChannel
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        System.out.println("客户端连接来自: " + clientChannel.getRemoteAddress());

        // 将新连接注册到Selector,关注READ事件,并附加一个Buffer
        clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        // 读取数据
        int bytesRead = channel.read(buffer);
        if (bytesRead == -1) {
            // 客户端正常关闭连接
            System.out.println("客户端断开连接: " + channel.getRemoteAddress());
            channel.close();
            return;
        }

        if (bytesRead > 0) {
            // 切换Buffer为读模式
            buffer.flip();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            String message = new String(bytes);
            System.out.println("收到消息: " + message);

            // 准备Echo回复
            String echoMessage = "Echo: " + message;
            ByteBuffer echoBuffer = ByteBuffer.wrap(echoMessage.getBytes());
            // 将数据写回客户端
            channel.write(echoBuffer);

            // 清空Buffer,为下一次读做准备
            buffer.clear();
        }
    }
}
// 可以使用 `nc localhost 8888` 或写一个NIO客户端来测试

AIO (Asynchronous I/O) - 异步非阻塞I/O

模型: 基于事件和回调。比如:你点完餐后,就可以完全离开去做自己的事。厨师做好饭后,会主动打电话通知你(回调)饭做好了,让你来取。

工作方式:

  • 应用程序线程发起一个 read 异步操作,并提供一个回调函数(或返回一个 Future)。
  • 这个调用立即返回,应用程序线程可以去干别的事,完全不会被这个I/O操作阻塞。
  • 内核自己完成所有的数据准备和从内核空间到用户空间的拷贝工作。
  • 内核完成所有工作后,主动通知应用程序(通过调用之前提供的回调函数,或设置 Future 的状态)。

特点: 理论上是最高效的模型,真正的异步。适用于连接数多且连接时间长的应用(如文件服务器、大型资源下载)。在Linux上的实现底层仍使用epoll,并非真正的原生AIO,因此在实际项目中不如NIO普及。

/**
 * AIO模型下的文件服务器(简化版,发送指定文件)
 */
public class AioFileServer {

    // 定义要发送的文件路径
    private static final String FILE_PATH = "./large_file.dat";

    public static void main(String[] args) throws Exception {
        // 1. 创建一个异步通道组,使用线程池
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(
                Runtime.getRuntime().availableProcessors(),
                Executors.defaultThreadFactory()
        );

        // 2. 创建异步服务器通道并绑定端口
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(group);
        serverChannel.bind(new InetSocketAddress(8888));
        System.out.println("AIO文件服务器启动,监听端口: 8888,提供文件: " + FILE_PATH);

        // 3. 开始异步接受连接
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                // 接受下一个连接(重要!形成链式调用)
                serverChannel.accept(null, this);

                // 处理当前连接
                System.out.println("客户端连接: " + clientChannel);
                try {
                    handleClient(clientChannel);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

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

        // 主线程阻塞,防止服务器退出
        group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }

    private static void handleClient(AsynchronousSocketChannel clientChannel) throws IOException {
        Path path = Paths.get(FILE_PATH);
        // 4. 异步打开文件(返回一个Future)
        java.nio.channels.AsynchronousFileChannel fileChannel = java.nio.channels.AsynchronousFileChannel.open(
                path, StandardOpenOption.READ);

        // 创建一个Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 64); // 64KB Buffer

        // 5. 定义一个内部类,用于链式读取和发送文件
        FileReadSendHandler handler = new FileReadSendHandler(fileChannel, clientChannel, buffer, 0L);

        // 6. 开始第一轮读取->发送
        handler.readAndSend();
    }

    // 处理读取和发送的回调类
    static class FileReadSendHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final java.nio.channels.AsynchronousFileChannel fileChannel;
        private final AsynchronousSocketChannel clientChannel;
        private final ByteBuffer buffer;
        private long filePosition;

        public FileReadSendHandler(java.nio.channels.AsynchronousFileChannel fileChannel,
                                  AsynchronousSocketChannel clientChannel,
                                  ByteBuffer buffer, long filePosition) {
            this.fileChannel = fileChannel;
            this.clientChannel = clientChannel;
            this.buffer = buffer;
            this.filePosition = filePosition;
        }

        public void readAndSend() {
            // 7. 发起异步文件读取
            fileChannel.read(buffer, filePosition, buffer, this);
        }

        @Override
        public void completed(Integer bytesRead, ByteBuffer attachment) {
            if (bytesRead == -1) {
                // 文件读取完毕
                System.out.println("文件发送完成给客户端: " + clientChannel);
                try {
                    fileChannel.close();
                    clientChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return;
            }

            if (bytesRead > 0) {
                // 准备Buffer用于写入网络通道
                attachment.flip();
                filePosition += bytesRead;

                // 8. 发起异步网络写入,将刚读到的数据发送给客户端
                clientChannel.write(attachment, attachment, new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer bytesWritten, ByteBuffer innerBuffer) {
                        if (innerBuffer.hasRemaining()) {
                            // 如果没写完,继续写
                            clientChannel.write(innerBuffer, innerBuffer, this);
                        } else {
                            // 当前Buffer的数据已全部发送完毕,清空Buffer,准备下一次读取
                            innerBuffer.clear();
                            readAndSend(); // 链式调用,继续读下一块文件内容
                        }
                    }

                    @Override
                    public void failed(Throwable exc, ByteBuffer innerBuffer) {
                        System.err.println("发送数据失败: " + exc.getMessage());
                        closeChannels();
                    }
                });
            }
        }

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

        private void closeChannels() {
            try {
                fileChannel.close();
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
// 这个示例展示了AIO最复杂的回调地狱(Callback Hell)问题,逻辑比NIO复杂很多。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐