Java 网络编程核心:BIO、NIO、AIO IO 模型深度解析与实战

0 阅读32分钟

Java 网络编程核心:BIO、NIO、AIO IO 模型深度解析与实战

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页   @怒放吧德德  To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息  @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

Netty.jpg

前言

在分布式系统与高并发场景成为主流的今天,Java 网络编程作为后端开发的核心基础,其 IO 模型的选择直接决定了系统的性能上限。从早期的 BIO(同步阻塞 IO)到为解决高并发而生的 NIO(同步非阻塞 IO),再到更贴合异步编程理念的 AIO(异步非阻塞 IO),三种 IO 模型贯穿了 Java 网络编程的发展历程,也对应着不同的业务场景需求。

对于初学者而言,IO 模型的核心差异(阻塞 / 非阻塞、同步 / 异步)常常难以理解;而对于资深开发者,如何根据业务场景(如连接数、数据传输量、实时性要求)选择合适的 IO 模型,更是优化系统性能的关键。本文将从网络通信的基础概念(Socket)入手,层层拆解 BIO、NIO、AIO 的核心原理,结合可直接运行的实战案例,清晰对比三者的底层逻辑、适用场景与性能优劣,帮助读者不仅理解 “是什么”,更能掌握 “怎么用”,最终实现从理论到实践的落地,为构建高可用、高性能的 Java 网络应用打下坚实基础。

1 网络通信基本常识:Socket是什么?

1.1 Socket核心概念

Socket(套接字)是Java实现网络通信的核心组件,本质是一种“通信端点”,用于在不同设备(或同一设备的不同进程)之间通过网络传输数据。它就像两个设备之间的“通信管道接口”,一端是客户端Socket,一端是服务器端Socket,双方通过这个接口建立连接、发送和接收数据。

Java中Socket编程基于TCP/IP协议(主流),分为客户端Socket服务器端ServerSocket,核心流程遵循“三次握手建立连接→数据传输→四次挥手关闭连接”的TCP通信规范,无需开发者手动处理底层协议细节,Java已封装好相关API(位于java.net包、java.nio包下)。

1.2 Socket通信核心要素

  • IP地址:标识网络中具体的设备(如192.168.1.1),相当于设备的“网络地址”。
  • 端口号:标识设备上的具体进程(范围0-65535,0-1024为系统端口,建议使用1024以上端口),相当于“进程的门牌号”,确保数据能准确送达目标程序。
  • 通信协议:约定数据传输的格式和规则,Java网络编程主要用TCP(面向连接、可靠传输,适用于文件传输、登录验证等)和UDP(无连接、不可靠,适用于视频直播、聊天消息等实时场景),本文重点讲解TCP协议下的BIO、NIO、AIO。

1.3 Socket通信基本流程(TCP)

  1. 服务器端:创建ServerSocket,绑定指定端口,监听客户端的连接请求(阻塞/非阻塞,取决于IO模型)。
  2. 客户端:创建Socket,指定服务器的IP地址和端口,发起连接请求。
  3. 连接建立:服务器端接收客户端连接,生成对应的Socket(用于与该客户端通信),此时双方建立“双向通信通道”。
  4. 数据传输:客户端和服务器端通过Socket获取输入流(InputStream)和输出流(OutputStream),实现数据的读写。
  5. 关闭连接:通信结束后,关闭输入流、输出流和Socket,释放资源。

客户端和服务端在通信编程中,只关心三件事:连接、读数据、写数据。

2 Java IO模型概述

Java中网络编程的IO模型,本质是“服务器端处理客户端连接和数据读写的方式”,主要分为三类:BIO(同步阻塞IO)、NIO(同步非阻塞IO)、AIO(异步非阻塞IO)。三者的核心区别在于“是否阻塞线程”和“是否主动等待IO操作完成”,下面依次详解,每部分均搭配入门案例,确保可直接运行、易于理解。

2.1 BIO:同步阻塞 IO

2.1.1 BIO 核心原理

BIO(Blocking IO)即同步阻塞IO,是Java最早的IO模型,也是最易理解的模型。其核心特点是:一个客户端连接对应一个服务器端线程,线程在执行IO操作(等待连接、读取数据、写入数据)时会被阻塞,直到IO操作完成后,线程才能继续执行其他任务。

通俗来说,BIO就像“一个服务员对应一个顾客”,服务员(线程)接待顾客(客户端)后,必须全程等待顾客点餐、用餐(IO操作),期间不能接待其他顾客,效率较低,但编程逻辑简单,适合入门学习。

BIO的核心缺陷:当客户端连接数量较多(如1000个)时,服务器端需要创建1000个线程,线程的创建和上下文切换会消耗大量系统资源,导致服务器性能急剧下降,甚至崩溃,因此BIO仅适用于连接数少、并发量低的场景(如本地测试、简单工具)。

2.1.2 BIO案例代码

案例说明:实现“客户端发送消息,服务器端接收消息并回复”的简单TCP通信,采用BIO模型,服务器端用多线程处理多个客户端连接(入门级多线程优化,避免单线程只能处理一个客户端)。

服务端:通过线程来处理

/**
 * Bio服务端 - 通过线程
 * <p>
 * 每次连接都创建一个线程,虽然能够解决,但是当链接特别多,就会导致创建很多的连接
 * @author: lyd
 * @date: 2026/1/29 22:46
 */
public class BioThreadServer {
    public static void main(String[] args) {
        // 1. 定义服务器端口(1024以上,避免占用系统端口)
        int port = 8888;
        ServerSocket serverSocket = null;

        try {
            // 2. 创建ServerSocket,绑定端口,开始监听客户端连接
            serverSocket = new ServerSocket(port);
            System.out.println("BIO服务器已启动,监听端口:" + port + ",等待客户端连接...");
            // 3. 循环监听客户端连接(阻塞点1:serverSocket.accept(),等待连接时线程阻塞)
            while (true) {
                // 接收客户端连接,accept()方法会阻塞,直到有客户端发起连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接成功:" + clientSocket.getInetAddress().getHostAddress());
                // 4. 为每个客户端创建一个独立线程,处理数据读写(避免单线程阻塞)
                new Thread(() -> {
                    // 定义输入流(读取客户端消息)、输出流(回复客户端消息)
                    try (
                        InputStream is = clientSocket.getInputStream();
                        OutputStream os = clientSocket.getOutputStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));
                        PrintWriter pw = new PrintWriter(os, true)) { // autoFlush=true,自动刷新缓冲区
                        String clientMsg;
                        // 5. 读取客户端消息(阻塞点2:br.readLine(),无数据时线程阻塞)
                        while ((clientMsg = br.readLine()) != null) {
                            System.out.println("收到客户端[" + clientSocket.getInetAddress().getHostAddress() + "]消息:" + clientMsg);
                            // 6. 回复客户端消息 (底层也是调用了pw.write())
                            pw.println("服务器已收到消息:" + clientMsg);
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        try {
                            // 客户端断开连接时,关闭Socket
                            clientSocket.close();
                            System.out.println("客户端[" + clientSocket.getInetAddress().getHostAddress() + "]断开连接");
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                }).start();
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭ServerSocket,释放资源
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端

/**
 * @author: lyd
 * @date: 2026/1/29 22:46
 */
public class BioClient {
    public static void main(String[] args) {
        // 1. 服务器IP(本地测试用127.0.0.1)和端口(与服务器一致)
        String serverIp = "127.0.0.1";
        int serverPort = 8888;
        Socket socket = null;
        try {
            // 2. 创建Socket,连接服务器(阻塞点:连接未建立时,线程阻塞)
            // *立即连接:在构造函数内部就完成了连接
            socket = new Socket(serverIp, serverPort);
//            SocketAddress add = new InetSocketAddress(serverIp, serverPort);
            // 显示连接
//            socket.connect(add);
            System.out.println("连接服务器成功,可发送消息(输入exit退出):");
            // 3. 获取输出流(发送消息)、输入流(接收服务器回复)
            try (OutputStream os = socket.getOutputStream();
                 PrintWriter pw = new PrintWriter(os, true); // 自动刷新
                 InputStream is = socket.getInputStream();
                 BufferedReader br = new BufferedReader(new InputStreamReader(is));
                 Scanner scanner = new Scanner(System.in)) {
                String msg;
                // 4. 循环输入消息,发送给服务器
                while (true) {
                    msg = scanner.nextLine();
                    // 输入exit,退出客户端
                    if ("exit".equals(msg)) {
                        pw.println(msg); // 告知服务器客户端退出
                        break;
                    }
                    // 发送消息到服务器
                    pw.println(msg);
                    // 接收服务器回复(阻塞点:无回复时,线程阻塞)
                    String serverReply = br.readLine();
                    System.out.println("服务器回复:" + serverReply);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭Socket,释放资源
            if (socket != null) {
                try {
                    socket.close();
                    System.out.println("客户端已退出");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行说明

  1. 先运行BioServer类,控制台输出“BIO服务器已启动,监听端口:8888,等待客户端连接...”,表示服务器启动成功。
  2. 再运行多个BioClient类(可同时运行2-3个),每个客户端控制台输出“连接服务器成功,可发送消息(输入exit退出)”。
  3. 在客户端输入消息(如“Hello BIO”),服务器端会打印客户端IP和消息,同时客户端会收到服务器的回复。
  4. 客户端输入“exit”,即可退出,服务器端会打印“客户端断开连接”。

核心阻塞点:serverSocket.accept()(等待连接)、br.readLine()(读取数据),这两个方法会让线程阻塞,直到有连接或数据到来。

2.1.3 为什么说是 bio 是阻塞 io 呢?

首先我们先将上诉 demo 的服务端的线程去掉,不建立新线程。启动服务端,里面会通过一个 while 去不断地循环监听客户端连接,accept()方法会阻塞,直到有客户端发起连接。

第二创建两个客户端实例,在发送服务器消息的时候打个断点

debug 模式启动客户端 1

客户端提示成功

输入数据后卡在 debug 中

服务端也有显示连接

然后在启动另一个客户端实例(不用 debug)

对比一下,客户端 2 显示连接成功,但是服务端却没有响应。

当客户端 2 发起消息

服务端依旧不知道

直到 debug 放开就能够看到消息,但是客户端 2 的消息已经无法接受了,也就是连接丢失了。

那么我们可以将服务端改成在接收到客户端连接之后创建一个线程,这样是不是就解决了这个丢失的问题?那么,这么做又会有新的问题出来,那就是,当客户端有很多个,那就会创建十分多的线程,那又是一场灾难,所以聪明的你又想到了加上线程池来实现。(具体代码可以看 gitee-Trial2-Netty/Trial2-Netty-Base 模块下的代码)

*2.2 NIO:非阻塞 IO(重要)

2.2.1 NIO核心原理

NIO(New IO/Non-Blocking IO)即同步非阻塞IO,是JDK1.4引入的IO模型,用于解决BIO的高并发缺陷。其核心特点是:一个线程可处理多个客户端连接,线程在执行IO操作时不会被阻塞,若当前无IO事件(无连接、无数据),线程可去处理其他任务,无需等待。

通俗来说,NIO就像“一个服务员对应多个顾客”,服务员(线程)不需要全程等待每个顾客,而是每隔一段时间巡视(轮询),查看哪个顾客有需求(IO事件),再去处理该顾客的需求,效率大幅提升。

NIO的核心组件(必须掌握),三者协同工作实现非阻塞通信:

  • Channel(通道):替代BIO中的Socket(本质是 Socket 的包装),是双向的(可读可写),可配置为非阻塞模式,核心实现类有ServerSocketChannel(服务器端,负责连接,【 关注OP_ACCEPT 】)、SocketChannel(客户端,实际读写【关注OP_READ/OP_WRITE 】)。
  • Buffer(缓冲区):NIO中所有数据的读写都必须通过缓冲区,本质是一块内存区域,用于临时存储数据(避免频繁IO调用,提升效率),核心实现类有ByteBuffer(最常用)、CharBuffer等。
  • Selector(选择器):NIO的“灵魂”,用于监听多个Channel的IO事件(连接就绪、读就绪、写就绪),一个Selector可绑定多个Channel,线程通过Selector轮询所有Channel,仅处理有IO事件的Channel,实现“单线程多连接”。

SelectionKey事件类型(四种事件),如下:

OP_READ:读事件

OP_WRITE:写事件

OP_CONNECT:连接事件(客户端专用)

OP_ACCEPT:接收连接事件(服务端专用)

于 NIO 代码对应如下

isAcceptable() → 是否有新连接可接受(OP_ACCEPT)
isReadable() → 是否有数据可读(OP_READ)
isWritable() → 是否可写(OP_WRITE)
isConnectable() → 连接是否完成(OP_CONNECT)
cancel() → 取消注册,让 Selector 不再监听该 Channel

关键规则:

ServerSocketChannel:仅关注OP_ACCEPT

客户端SocketChannel:连接→读写(动态调整关注事件)

服务端SocketChannel:仅关注OP_READ/OP_WRITE

反应堆模式(Reactor 模式):

这是 NIO 底层真正的灵魂,简单的说,反应堆模式 = 一个服务员 + 一个菜单 + 一堆顾客。

  • 服务员 = Selector(选择器)
  • 菜单 = 监听的事件(连接、读、写)
  • 顾客 = 多个客户端连接(SocketChannel)

*用一个线程,同时处理成千上万的连接。

NIO的适用场景:

连接数多、并发量高但每个连接的数据传输量小的场景(如聊天服务器、电商秒杀系统),主流框架(Netty、Mina)均基于NIO封装。

1、应用程序写入数据

应用程序层的业务代码,把要发送的**业务数据写入 Buffer 缓冲区**。( 遵循规则:数据先入 Buffer,不直接写 Channel。)

2、Channel.write () 写数据到通道

应用程序调用<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">SocketChannel.write(Buffer)</font>,将 Buffer 中的数据**写入通道**。(Channel 是双向传输通道,此时数据从应用层交给网络传输层.)

3、触发写事件 / 通道就绪

数据写入 Channel 后,Channel 触发**OP_WRITE 写就绪事件**,并把事件通知给 Selector。(Selector 会感知到这个 Channel 有写事件就绪。)

4、Selector.select () 返回

Selector 的select()是阻塞轮询方法,当任意一个注册的 Channel 有就绪事件(读 / 写 / 连接)时,select()立即返回,不再阻塞。(这是多路复用的核心:一个 select () 监听所有 Channel,不用为每个连接开线程。)

5、Channel 发送数据到网络

SocketChannel 将 Buffer 中的数据发送给网络层的客户端,完成网络数据输出。

6、触发 OP_READ 读就绪事件

当客户端向服务端发送回传数据,网络数据到达服务端的 SocketChannel 时,通道触发OP_READ 读就绪事件,并通知 Selector。

7、Selector 通知应用程序 “可读”

Selector 检测到 OP_READ 就绪后,唤醒线程,通知应用程序:当前通道有数据可以读取

8、Channel.read () 读取网络数据

应用程序调用 SocketChannel.read(Buffer),将 Channel 中的网络数据读取到 Buffer 缓冲区

9、应用程序处理数据

应用程序从 Buffer 中读取数据,执行业务逻辑处理(解析、计算、响应等)。

10、客户端发起新连接请求

网络层的新客户端,向服务端的监听端口发起 TCP 连接请求。

11、触发 OP_ACCEPT 连接就绪事件

服务端的ServerSocketChannel(监听端口)感知到新连接,触发OP_ACCEPT 连接就绪事件,通知 Selector。

12、Selector 通知应用程序 “有新连接”

Selector 检测到 OP_ACCEPT就绪后,通知应用程序:有新客户端请求连接,服务端可以接受这个连接。(服务端接受连接后,会生成新的 SocketChannel,将其非阻塞模式下注册到 Selector,并绑定 OP_READ/OP_WRITE 事件,加入监听列表,实现新连接的管理。)

2.2.2 NIO 案例代码

案例说明:实现与BIO案例相同的功能(客户端发消息、服务器端收消息并回复),采用NIO模型,服务器端用单线程+Selector处理多个客户端连接,体现非阻塞优势。(在看代码之前,要先理解 NIO 的逻辑,其次,代码繁多,可以看我的 gitee 仓库,这里不会放太多代码。)

① 服务端代码

// 1. 定义端口
int port = 8889;
Selector selector = null;
ServerSocketChannel serverSocketChannel = null;

// 2. 创建Selector(选择器)
selector = Selector.open();
// 3. 创建ServerSocketChannel,绑定端口
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(port));
// 4. 配置为非阻塞模式(核心:NIO必须设置为非阻塞)
serverSocketChannel.configureBlocking(false);
// 5. 将ServerSocketChannel注册到Selector,监听"连接就绪"事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

这里主要的时候要创建选择器-Selector,并且给 ServerSocketChannel 绑定服务器的端口号,待会客户端通过这个来访问,并且注册到选择器中,监听连接就绪的事件。

// 6. 循环轮询Selector,处理IO事件(非阻塞)
while (true) {
    // 轮询Selector,获取有IO事件的通道数量(阻塞1秒,无事件则继续循环,避免空轮询消耗CPU)
    int selectCount = selector.select(1000);
    if (selectCount == 0) {
        continue; // 无IO事件,继续轮询
    }

    // 7. 获取所有有IO事件的SelectionKey(事件令牌)
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();

    // 8. 遍历处理每个IO事件
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        // 移除当前key,避免重复处理
        // 为什么需要移除?
        // Selector不会自动清空selectedKeys集合, 需要程序员显式移除已处理的key
        iterator.remove();

        try {
            // 处理"连接就绪"事件(客户端发起连接)
            if (key.isAcceptable()) {
                handleAccept(key, selector);
            }
            // 处理"读就绪"事件(客户端发送消息)
            if (key.isReadable()) {
                handleRead(key);
            }
        } catch (IOException e) {
            // 处理单个客户端异常,不影响服务器继续运行
            System.err.println("处理客户端IO异常:" + e.getMessage());
            // 取消key并关闭channel
            key.cancel();
            if (key.channel() != null) {
                try {
                    key.channel().close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

这段代码是 NIO 事件循环核心部分,实现了非阻塞 IO 的多路复用。

处理连接事件

private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
    // 获取ServerSocketChannel
    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
    // 接收客户端连接(非阻塞,因为ServerSocketChannel已设为非阻塞)
    SocketChannel clientChannel = serverSocketChannel.accept();
    if (clientChannel != null) {
        System.out.println("客户端连接成功:" + clientChannel.getRemoteAddress());
        // 配置客户端Channel为非阻塞模式
        clientChannel.configureBlocking(false);
        // 分配缓冲区(大小1024字节,可根据需求调整)
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 将客户端Channel注册到Selector,监听"读就绪"事件,并绑定缓冲区(附加对象)
        clientChannel.register(selector, SelectionKey.OP_READ, buffer);
    }
}

处理读就绪事件

private static void handleRead(SelectionKey key) throws IOException {
    // 获取客户端SocketChannel
    SocketChannel clientChannel = (SocketChannel) key.channel();
    // 获取绑定的缓冲区(附加对象)
    ByteBuffer buffer = (ByteBuffer) key.attachment();

    // 读取客户端数据(非阻塞,无数据时返回-1)
    int readLen = clientChannel.read(buffer);
    if (readLen > 0) {
        // 切换缓冲区为"读模式"(flip():将position重置为0,limit设为当前position)
        buffer.flip();
        // 将缓冲区数据转为字符串(去除首尾空白字符,包括换行符)
        String clientMsg = new String(buffer.array(), 0, buffer.limit()).trim();
        System.out.println("收到客户端[" + clientChannel.getRemoteAddress() + "]消息:" + clientMsg);

        if ("exit".equals(clientMsg)) {
            System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]主动断开连接");
            // 取消注册,关闭Channel
            key.cancel();
            clientChannel.close();
            return;
        }

        // 回复客户端消息
        String replyMsg = "服务器已收到消息:" + clientMsg;
        // 清空缓冲区,准备写入回复数据
        buffer.clear();
        // 将回复消息写入缓冲区
        buffer.put(replyMsg.getBytes());
        // 切换缓冲区为"写模式"
        buffer.flip();
        // 写入客户端Channel(发送回复)
        clientChannel.write(buffer);

        // 清空缓冲区,准备下次读取
        buffer.clear();
    } else if (readLen == -1) {
        // 读取到-1,表示客户端断开连接
        System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]断开连接");
        // 取消注册,关闭Channel
        key.cancel();
        clientChannel.close();
    }
}

② 客户端代码

客户端代码比较简单,需要设置缓冲区,写入数据的时候,实际上是写到缓冲区里面,在写到客户端SocketChannel 中。

public class NioClient {
    public static void main(String[] args) {
        String serverIp = "127.0.0.1";
        int serverPort = 8889;
        SocketChannel clientChannel = null;
        ByteBuffer buffer = ByteBuffer.allocate(1024); // 缓冲区

        try {
            // 1. 创建SocketChannel
            clientChannel = SocketChannel.open();
            // 2. 配置为非阻塞模式
            clientChannel.configureBlocking(false);
            // 3. 连接服务器(非阻塞,连接未建立时不会阻塞线程)
            clientChannel.connect(new InetSocketAddress(serverIp, serverPort));

            // 循环等待连接建立(非阻塞连接,需判断连接状态)
            while (!clientChannel.finishConnect()) {
                // 连接未建立时,可做其他任务(此处简化,仅打印提示)
                System.out.println("正在连接服务器...");
                Thread.sleep(500); // 模拟其他任务
            }

            System.out.println("连接服务器成功,可发送消息(输入exit退出):");

            // 4. 读取用户输入,发送消息
            try (Scanner scanner = new Scanner(System.in)) {
                String msg;
                while (true) {
                    msg = scanner.nextLine();
                    if ("exit".equals(msg)) {
                        // 发送退出消息,关闭客户端
                        buffer.put(msg.getBytes());
                        buffer.flip();
                        clientChannel.write(buffer);
                        buffer.clear();
                        break;
                    }

                    // 发送消息到服务器
                    buffer.put(msg.getBytes());
                    buffer.flip(); // 切换为写模式
                    clientChannel.write(buffer);
                    buffer.clear(); // 清空缓冲区

                    // 接收服务器回复(非阻塞模式,需要等待数据到达)
                    // 等待服务器回复(最多等待3秒)
                    int retryCount = 0;
                    int maxRetry = 30; // 最多重试30次,每次100ms,共3秒
                    boolean receivedReply = false;
                    
                    while (retryCount < maxRetry && !receivedReply) {
                        int readLen = clientChannel.read(buffer);
                        if (readLen > 0) {
                            buffer.flip();
                            String serverReply = new String(buffer.array(), 0, buffer.limit());
                            System.out.println("服务器回复:" + serverReply);
                            buffer.clear();
                            receivedReply = true;
                        } else if (readLen == 0) {
                            // 数据还未到达,等待一段时间后重试
                            Thread.sleep(100);
                            retryCount++;
                        } else {
                            // readLen == -1,服务器断开连接
                            System.out.println("服务器断开连接");
                            break;
                        }
                    }
                    
                    if (!receivedReply && retryCount >= maxRetry) {
                        System.out.println("等待服务器回复超时");
                    }
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (clientChannel != null) {
                try {
                    clientChannel.close();
                    System.out.println("客户端已退出");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行说明

  1. 先运行NioServer,控制台输出“NIO服务器已启动,监听端口:8889,等待客户端连接...”。
  2. 运行多个NioClient(可同时运行5-10个),每个客户端会打印“正在连接服务器...”,连接成功后提示输入消息。
  3. 客户端输入消息,服务器端会接收并回复,多个客户端可同时发送消息,服务器端单线程即可处理(无卡顿)。
  4. 核心亮点:服务器端仅用一个主线程,通过Selector轮询所有客户端连接,无需创建大量线程,解决了BIO的高并发缺陷;所有IO操作均为非阻塞,线程无需等待。

启动一个服务端,两个客户端

此时会打印客户端连接成功

当我们在客户端 2 输出数据,服务端将会得到响应,客户端也能够得到服务端的回复。

当客户端 1 输出 exit 退出的时候,服务端不会挂掉,这里是做了处理(详细见 git 提交)

demo 做了异常捕获,当客户端程序断开的时候,会将异常捕获。

2.3 AIO:异步非阻塞IO

2.3.1 AIO核心原理

AIO(Asynchronous IO)即异步非阻塞IO,是JDK1.7引入的IO模型(也称为NIO.2),是真正意义上的“异步IO”。其核心特点是:线程发起IO操作后,立即返回,无需轮询等待,IO操作的整个过程(等待数据、读取数据)由操作系统在后台完成,当IO操作完成后,操作系统会主动通知线程,线程再处理结果。

通俗来说,AIO就像“顾客(客户端)通过手机下单,商家(服务器端)收到订单后处理,处理完成后主动通知顾客”,商家(线程)无需主动询问顾客,全程无需等待,效率最高。

AIO与NIO的核心区别:NIO是“同步非阻塞”,线程需要主动轮询Selector查看IO事件;AIO是“异步非阻塞”,线程无需轮询,由操作系统主动通知IO事件完成,编程模型更简洁(无需手动管理Selector和Buffer的状态)。

AIO的核心实现:Java提供了两种AIO编程方式(CompletionHandler回调方式、Future方式),其中回调方式更常用,本文案例采用回调方式实现。

AIO的适用场景:连接数多、并发量高且IO操作耗时较长的场景(如文件服务器、视频点播系统),但由于Java AIO的底层实现依赖操作系统(Windows下性能较好,Linux下性能一般),实际开发中不如NIO(搭配Netty)常用,但作为IO模型的重要组成部分,需了解其核心思想。

2.3.2 AIO 案例代码

案例说明:实现与前面一致的功能,采用AIO模型(CompletionHandler回调方式),服务器端异步接收连接、异步读取数据,客户端异步连接、异步发送数据,线程无需阻塞和轮询。

① 服务端代码

/**
 * AIO服务器端:异步非阻塞,基于CompletionHandler回调处理IO事件
 * @author: lyd
 * @date: 2026/2/10 23:52
 */
public class AioServer {
    public static void main(String[] args) {
        int port = 8890;
        try {
            // 1. 创建异步服务器通道
            AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
            // 2. 绑定端口
            serverChannel.bind(new InetSocketAddress(port));
            System.out.println("AIO服务器已启动,监听端口:" + port + ",等待客户端连接...");

            // 3. 异步接收客户端连接(无阻塞,发起后立即返回,连接完成后回调handle方法)
            serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
                /**
                 * 连接成功时,回调此方法
                 * @param clientChannel 客户端通道(连接成功后生成)
                 * @param attachment 附加对象(此处为null)
                 */
                @Override
                public void completed(AsynchronousSocketChannel clientChannel, Object attachment) {
                    // 继续接收下一个客户端连接(否则只能接收一个客户端)
                    serverChannel.accept(null, this);

                    try {
                        System.out.println("客户端连接成功:" + clientChannel.getRemoteAddress());
                        // 分配缓冲区,异步读取客户端消息(读取完成后回调ReadHandler)
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        clientChannel.read(buffer, buffer, new ReadHandler(clientChannel));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                /**
                 * 连接失败时,回调此方法
                 * @param throwable 异常信息
                 * @param attachment 附加对象
                 */
                @Override
                public void failed(Throwable throwable, Object attachment) {
                    System.out.println("客户端连接失败:" + throwable.getMessage());
                }
            });

            // 主线程不能退出(AIO的IO操作由操作系统后台处理,主线程退出则程序终止)
            Thread.sleep(Integer.MAX_VALUE);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 自定义回调处理器:处理“读取客户端消息”的异步事件
     */
    static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel clientChannel;

        // 构造方法,传入客户端通道
        public ReadHandler(AsynchronousSocketChannel clientChannel) {
            this.clientChannel = clientChannel;
        }

        /**
         * 读取数据成功时,回调此方法
         * @param readLen 读取到的字节数(-1表示客户端断开连接)
         * @param buffer 缓冲区(附加对象,存储读取到的数据)
         */
        @Override
        public void completed(Integer readLen, ByteBuffer buffer) {
            if (readLen > 0) {
                // 切换缓冲区为读模式,读取客户端消息
                buffer.flip();
                String clientMsg = new String(buffer.array(), 0, readLen);
                try {
                    System.out.println("收到客户端[" + clientChannel.getRemoteAddress() + "]消息:" + clientMsg);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 回复客户端消息(异步写入,写入完成后回调WriteHandler)
                String replyMsg = "服务器已收到消息:" + clientMsg;
                buffer.clear();
                buffer.put(replyMsg.getBytes());
                buffer.flip();
                clientChannel.write(buffer, buffer, new WriteHandler(clientChannel));
            } else if (readLen == -1) {
                // 客户端断开连接
                try {
                    System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]断开连接");
                    clientChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 读取数据失败时,回调此方法
         */
        @Override
        public void failed(Throwable throwable, ByteBuffer buffer) {
            System.out.println("读取客户端消息失败:" + throwable.getMessage());
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 自定义回调处理器:处理“回复客户端消息”的异步事件
     */
    static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel clientChannel;

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

        /**
         * 写入数据成功时,回调此方法
         */
        @Override
        public void completed(Integer writeLen, ByteBuffer buffer) {
            // 写入成功后,继续异步读取客户端下一条消息
            buffer.clear();
            clientChannel.read(buffer, buffer, new ReadHandler(clientChannel));
        }

        /**
         * 写入数据失败时,回调此方法
         */
        @Override
        public void failed(Throwable throwable, ByteBuffer buffer) {
            System.out.println("回复客户端消息失败:" + throwable.getMessage());
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

② 客户端代码

/**
 * AIO客户端:异步非阻塞,基于CompletionHandler回调处理IO事件
 * @author: lyd
 * @date: 2026/2/10 23:53
 */
public class AioClient {
    public static void main(String[] args) {
        String serverIp = "127.0.0.1";
        int serverPort = 8890;

        try {
            // 1. 创建异步客户端通道
            AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();
            // 2. 异步连接服务器(连接完成后回调ConnectHandler)
            clientChannel.connect(new InetSocketAddress(serverIp, serverPort), null, new CompletionHandler<Void, Object>() {
                /**
                 * 连接成功回调
                 */
                @Override
                public void completed(Void result, Object attachment) {
                    System.out.println("连接服务器成功,可发送消息(输入exit退出):");
                    // 读取用户输入,发送消息
                    try (Scanner scanner = new Scanner(System.in)) {
                        String msg;
                        while (true) {
                            msg = scanner.nextLine();
                            if ("exit".equals(msg)) {
                                // 发送退出消息,关闭客户端
                                ByteBuffer buffer = ByteBuffer.allocate(1024);
                                buffer.put(msg.getBytes());
                                buffer.flip();
                                clientChannel.write(buffer, buffer, new ClientWriteHandler(clientChannel));
                                break;
                            }

                            // 异步发送消息到服务器(写入完成后回调ClientWriteHandler)
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            buffer.put(msg.getBytes());
                            buffer.flip();
                            clientChannel.write(buffer, buffer, new ClientWriteHandler(clientChannel));
                        }
                    }
                }

                /**
                 * 连接失败回调
                 */
                @Override
                public void failed(Throwable throwable, Object attachment) {
                    System.out.println("连接服务器失败:" + throwable.getMessage());
                    try {
                        clientChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });

            // 主线程不能退出
            Thread.sleep(Integer.MAX_VALUE);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端写入消息回调处理器
     */
    static class ClientWriteHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel clientChannel;

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

        /**
         * 写入成功回调:写入成功后,异步读取服务器回复
         */
        @Override
        public void completed(Integer writeLen, ByteBuffer buffer) {
            buffer.clear();
            // 异步读取服务器回复(读取完成后回调ClientReadHandler)
            clientChannel.read(buffer, buffer, new ClientReadHandler(clientChannel));
        }

        /**
         * 写入失败回调
         */
        @Override
        public void failed(Throwable throwable, ByteBuffer buffer) {
            System.out.println("发送消息失败:" + throwable.getMessage());
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 客户端读取服务器回复回调处理器
     */
    static class ClientReadHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel clientChannel;

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

        /**
         * 读取成功回调:打印服务器回复
         */
        @Override
        public void completed(Integer readLen, ByteBuffer buffer) {
            if (readLen > 0) {
                buffer.flip();
                String serverReply = new String(buffer.array(), 0, readLen);
                System.out.println("服务器回复:" + serverReply);
            } else if (readLen == -1) {
                // 服务器断开连接
                System.out.println("服务器已断开连接");
                try {
                    clientChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 读取失败回调
         */
        @Override
        public void failed(Throwable throwable, ByteBuffer buffer) {
            System.out.println("读取服务器回复失败:" + throwable.getMessage());
            try {
                clientChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行说明

  1. 先运行AioServer,控制台输出“AIO服务器已启动,监听端口:8890,等待客户端连接...”,主线程进入休眠(不占用CPU)。
  2. 运行多个AioClient,每个客户端连接成功后提示输入消息,输入消息后,服务器端会异步接收并回复,客户端异步接收回复。
  3. 核心亮点:所有IO操作(连接、读、写)均为异步,服务器端和客户端线程无需阻塞、无需轮询,IO操作由操作系统后台完成,完成后通过回调函数通知线程处理结果,编程模型更简洁,资源消耗更低。

3 BIO、NIO、AIO核心对比

对比维度BIO(同步阻塞)NIO(同步非阻塞)AIO(异步非阻塞)
核心模型一个连接一个线程单线程处理多个连接(Selector轮询)异步回调,操作系统通知IO完成
线程状态IO操作时线程阻塞线程非阻塞,需轮询Selector线程完全不阻塞,无需轮询
核心组件Socket、ServerSocket、流Channel、Buffer、SelectorAsynchronousChannel、CompletionHandler
编程难度简单(入门首选)中等(需掌握三大组件)中等(回调模型,逻辑清晰)
适用场景连接数少、并发低(如本地测试)连接数多、并发高(如聊天、秒杀)连接数多、IO耗时久(如文件、视频)
实际应用简单工具、测试程序Netty、Mina框架(主流)少数高性能场景(Windows环境更优)

总结

Socket是Java网络通信的基础,核心是“通信端点”,分为客户端和服务器端,基于TCP/IP协议实现数据传输,掌握“连接→传输→关闭”的基本流程即可入门。BIO、NIO、AIO是Java网络编程的三种IO模型,演进方向是“从阻塞到非阻塞、从同步到异步”,核心目标是提升并发处理能力、降低资源消耗。入门建议:先掌握BIO(理解Socket通信流程),再学习NIO(掌握三大组件,重点是Selector的使用),最后了解AIO(理解异步回调思想),无需急于掌握高级特性,先跑通案例,再逐步深入。实际开发中,NIO是主流(搭配Netty框架,封装了复杂的NIO细节),AIO由于操作系统兼容性问题,使用较少,但了解其异步思想,对后续学习高性能编程有很大帮助。


转发请携带作者信息  @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!