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

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

😄生命不息,写作不止

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

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

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

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

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

前言

在分布式系统与高并发场景成为主流的今天,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模型,服务器端用多线程处理多个客户端连接(入门级多线程优化,避免单线程只能处理一个客户端)。

服务端:通过线程来处理

xml 复制代码
/**
 * 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();
                }
            }
        }
    }
}

客户端

xml 复制代码
/**
 * @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 代码对应如下

java 复制代码
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 仓库,这里不会放太多代码。)

① 服务端代码

java 复制代码
// 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 绑定服务器的端口号,待会客户端通过这个来访问,并且注册到选择器中,监听连接就绪的事件。

java 复制代码
// 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 的多路复用。

处理连接事件

java 复制代码
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);
    }
}

处理读就绪事件

java 复制代码
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 中。

java 复制代码
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回调方式),服务器端异步接收连接、异步读取数据,客户端异步连接、异步发送数据,线程无需阻塞和轮询。

① 服务端代码

java 复制代码
/**
 * 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();
            }
        }
    }
}

② 客户端代码

java 复制代码
/**
 * 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、Selector AsynchronousChannel、CompletionHandler
编程难度 简单(入门首选) 中等(需掌握三大组件) 中等(回调模型,逻辑清晰)
适用场景 连接数少、并发低(如本地测试) 连接数多、并发高(如聊天、秒杀) 连接数多、IO耗时久(如文件、视频)
实际应用 简单工具、测试程序 Netty、Mina框架(主流) 少数高性能场景(Windows环境更优)

总结

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


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

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
Java后端的Ai之路3 小时前
【JDK】-JDK 21 新特性内容
java·开发语言·后端·jdk·jdk21
普通网友5 小时前
PHP语言的正则表达式
开发语言·后端·golang
葵续浅笑6 小时前
从Spring拦截器到Filter过滤器:一次报文修改加解密的填坑经验
java·后端·spring
snakeshe10107 小时前
Java集合框架深度解析:核心类库与实战应用
后端
大鹏19887 小时前
告别 XML 与字符串拼接:dbVisitor 如何以“多范式融合”重塑 Java DAL 层
后端
你有医保你先上7 小时前
go-es:一个优雅的 Elasticsearch Go 客户端
后端·elasticsearch
柠檬味的Cat8 小时前
零基础搭建WordPress网站完整流程
后端·php
代龙涛8 小时前
wordpress块主题
开发语言·后端·php
禾味8 小时前
过程即奖励|前端转后端经验分享
前端·后端·面试