一文了解Java的BIO, NIO和AIO模型

举报
神的孩子在歌唱 发表于 2024/10/30 09:35:38 2024/10/30
【摘要】 一文了解Java的BIO, NIO和AIO模型前言这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。作者:神的孩子都在歌唱在 Java 中,BIO、NIO 和 AIO 是三种用于处理网络通信的 I/O 模型,它们的主要区别在于数据处理方式和线程模型的不同。一. Java的 I/O模型Java的I/O模型主要包括以下几种:阻塞I/O(Blocking I/O):程序等...

一文了解Java的BIO, NIO和AIO模型

前言

这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。

作者:神的孩子都在歌唱


在 Java 中,BIO、NIO 和 AIO 是三种用于处理网络通信的 I/O 模型,它们的主要区别在于数据处理方式和线程模型的不同。


一. Java的 I/O模型

Java的I/O模型主要包括以下几种:

  1. 阻塞I/O(Blocking I/O):程序等待I/O操作完成,期间无法做其他事情。

  2. 非阻塞I/O(Non-blocking I/O):程序发出I/O请求后不等待,可以做其他事情,需要主动检查操作是否完成。

  3. I/O多路复用(IO Multiplexing):通过一个线程监视多个I/O通道,当某个通道准备好时才进行操作(如Selector)。

  4. 异步I/O(Asynchronous I/O):程序发出I/O请求,操作完成后系统会通知程序,完全异步处理。


二. BIO(Blocking I/O)

BIO 全称是阻塞式 I/O(Blocking I/O),它是 Java 最传统、最简单的 I/O 模型,采用同步阻塞方式来处理数据。BIO 在处理每个 I/O 请求时会创建一个新的线程,每个线程负责处理一个客户端的连接请求,线程会阻塞等待 I/O 操作完成。

image-20240926173837792

工作原理:

  • 服务器端启动一个 ServerSocket

  • 服务器在调用 accept() 方法时,会阻塞等待客户端连接。

  • 客户端连接后,服务器为每个客户端创建一个独立的线程,用于处理 I/O 操作。

  • 如果没有数据可读取,线程会阻塞直到有数据可读。

  • 线程阻塞会造成系统资源浪费,且无法有效处理大量并发请求。

特点:

  • 简单直接。

  • 每个连接都需要一个线程来处理。

  • 线程数目会随着并发连接数增加,造成较大的资源消耗。

  • 适用于连接数量较少且资源充足的场景。

示例代码:

ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();  // 阻塞等待客户端连接
InputStream in = socket.getInputStream();  // 阻塞等待数据输入
byte[] buffer = new byte[1024];
int len = in.read(buffer);  // 阻塞等待数据读取

三. NIO(Non-blocking I/O)


3.1 介绍

NIO 全称为非阻塞 I/O(Non-blocking I/O),从 Java 1.4 开始引入。它采用了多路复用(Selector)机制,允许通过单个线程处理多个客户端连接,使用事件驱动的方式处理 I/O 操作。

image-20240926180652002

工作原理:

  • NIO 的核心组件包括 Channel(通道)Selector(选择器)Buffer(缓冲区)

  • Selector 监听多个 Channel 事件(如连接、读取、写入等),通过单个线程轮询并处理就绪的 I/O 事件。

  • Channel 和传统的 Socket 类似,但可以设置为非阻塞模式,当 I/O 操作未完成时不会阻塞线程。

  • 数据通过 Buffer 读写,ChannelBuffer 中读取或写入数据。

组件关系:

  1. 每个 Channel 都会对应一个 Buffer

  2. Selector 对应一个线程,一个线程对应多个 Channel(连接)。

  3. 程序切换到哪个 Channel 是由事件(Event )决定的。

  4. Selector 会根据不同的事件,在各个通道上切换。

  5. Buffer 就是一个内存块,底层是有一个数组。

  6. Buffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的

特点:

  • 通过少量线程处理大量连接,提升了系统的并发能力。

  • 避免了线程阻塞,大幅减少线程数目,降低了资源消耗。

  • 更加适用于处理大量并发连接的场景(如聊天服务器、游戏服务器等)。


3.2 缓冲区(Buffer)

Buffer 是Java NIO中用于存储数据的容器。Buffer不仅用于存储数据,还跟踪当前读写位置等信息,简化了数据的操作流程。

  • 工作方式Buffer实际上是一个数组,并且通过以下关键属性来帮助管理缓冲区:

    • capacity:缓冲区的总容量(以字节为单位),表示可以存储多少数据。

    • position:当前读/写操作的位置。

    • limit:限制操作的边界,表示可以读/写的最大范围。

    • mark: 标记

    image-20241008153234992

  • 常见类型:Buffer是一个顶级的抽象父类类,以下是其子类

    • ByteBuffer:存储字节数据(常用)。

    • CharBuffer:存储字符数据。

    • IntBufferLongBufferFloatBufferDoubleBuffer:存储相应的基本类型数据。

  • 使用流程:一般的读写操作流程如下:

    1. 写入数据到Buffer:先通过put()方法将数据写入缓冲区。

    2. 切换到读模式:调用flip()将Buffer从写模式切换到读模式。

    3. 读取数据:通过get()方法读取数据。

    4. 清除或重用Buffer:调用clear()compact()方法清空缓冲区或压缩已读取的数据以重用缓冲区。

ByteBuffer buffer = ByteBuffer.allocate(1024);  // 分配一个容量为1024字节的缓冲区
buffer.put(data);  // 向缓冲区写入数据
buffer.flip();  // 切换到读模式
channel.write(buffer);  // 将缓冲区的数据写入通道


3.3 通道(Channel)

Channel 是Java NIO中的一个接口,它代表了I/O源(如文件或网络套接字)与目标的连接通道。Channel与传统I/O中的Stream类似,但它支持异步、非阻塞操作,双向(可以同时进行读写)。

image-20241008154222568

  • 常见类型

    • FileChannel:用于文件的读写操作。

    • SocketChannel:用于TCP网络套接字的读写操作。

    • ServerSocketChannel:用于监听TCP连接的服务器端通道。

    • DatagramChannel:用于UDP数据包的读写操作。

  • 特点Channel可以异步读写数据,并且可以是非阻塞的。数据的读写通过Buffer进行。


3.4 Selector(选择器)

Selector 是Java NIO中用于管理多个Channel的组件,允许一个单一的线程处理多个通道的I/O事件。它的核心功能是监听注册在其上的多个通道的状态,并且可以同时处理这些通道的事件。

  • 主要功能:允许线程监控多个通道的读写事件,减少线程数量,提高资源利用率。

  • 使用场景:适用于有大量I/O操作的场景,例如高并发的网络服务器。

  • 工作机制:通道(Channel)注册到选择器上,并通过选择器监听通道的就绪状态(如读、写、连接等事件)。当有通道就绪时,选择器会返回相应的通道,可以通过处理这些事件来实现非阻塞I/O。

image-20241009092305955

Selector selector = Selector.open();  // 打开一个选择器
channel.register(selector, SelectionKey.OP_READ);  // 注册通道到选择器,并监听READ事件

SelectionKey表示 Selector 和网络通道Channel的注册关系,共四种:

  • int OP_ACCEPT:有新的网络连接可以 accept,值为 16

  • int OP_CONNECT:代表连接已经建立,值为 8

  • int OP_READ:代表读操作,值为 1

  • int OP_WRITE:代表写操作,值为 4

3.5 案例

需求:

  1. 编写一个 NIO ,实现服务器端能够接收客户端发送的消息(非阻塞)

  2. 服务端需要监测客户端上线,离线

3.5.1 服务端

public class GroupChatServer {
​
    // 选择器
    private Selector selector;
    // 通道
    private ServerSocketChannel serverSocketChannel;
    // 启动端口
    private static final int PORT = 8888;
​
    /**
     * 启动
     */
    public void start() {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 注册监听的通道,有新的网络连接就接受
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
​
    /**
     * 监听要注册的通道
     */
    public void listen() {
        try {
            while (true) {
                // 查询处于就绪状态的通道数
                int count = selector.select();
                if (count > 0) {
                    // 获取 selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        // 测试此键的通道是否已准备好接受新的 socket 连接
                        if (selectionKey.isAcceptable()) {
                            // 接受与此通道的套接字建立的连接
                            SocketChannel accept = serverSocketChannel.accept();
                            accept.configureBlocking(false);
                            // 将通道注册到选择器,读取客户端信息
                            accept.register(selector, SelectionKey.OP_READ);
                            System.out.println(accept.getRemoteAddress() + "启动了");
                        }
                        // 测试此键的通道是否已准备好读取
                        if (selectionKey.isReadable()) {
                            readData(selectionKey);
                        }
                        // 删除key,防止重复处理
                        iterator.remove();
                    }
​
                } else {
                    System.out.println("没有就绪的通道数");
                }
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            System.out.println("结束");
        }
    }
​
    /**
     * 读取客户端信息
     */
    public void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            // 创建buffer,分配新的字节缓冲区
            ByteBuffer allocate = ByteBuffer.allocate(1024);
            // 将此 channel 中的字节序列读取到给定的 buffer 中,返回读取的字节数
            int read = channel.read(allocate);
            if (read > 0) {
                String msg = new String(allocate.array());
                System.out.println(channel.getRemoteAddress() + "客户端消息: " + msg.trim());
            }
        } catch (Exception e) {
            try {
                // 客户端关闭后要进行处理
                if (channel != null) {
                    System.out.println(channel.getRemoteAddress() + "离线了");
                }
                // 取消注册和关闭通道
                key.cancel();
                if (channel != null) {
                    channel.close();
                }
            } catch (IOException exception) {
                System.out.println(exception.getMessage());
            }
        }
    }
    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.start();
        groupChatServer.listen();
    }
}

listen方法代码解析:

  1. 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel

  2. Selector 进行监听 select 方法,返回有事件发生的通道的个数。

  3. socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel

  4. 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。

  5. 进一步得到各个 SelectionKey(有事件发生)。

  6. 在通过 SelectionKey 反向获取 SocketChannel,方法 channel()

  7. 可以通过得到的 channel,完成业务处理,读取客户端发送的消息。

3.5.2 客户端代码

public class GroupChatClient {
​
​
    //定义相关的属性
    private final String HOST = "127.0.0.1";
    private final int PORT = 8888;
    private final Selector selector;
    private final SocketChannel socketChannel;
​
    //构造器,完成初始化工作
    public  GroupChatClient() throws IOException {
        // 开启新的选择器
        selector = Selector.open();
        //打开套接字通道并将其连接到远程地址
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将 channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
​
    /**
     * 向服务器发送消息
     */
    public void sendInfo(String info) {
        try {
            // write: 将字节序列从给定的 buffer 写入此 channel   wrap:将字节数组包装到缓冲区中
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
​
    public static void main(String[] args) throws IOException {
        GroupChatClient groupChatClient = new GroupChatClient();
        // 发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String str = scanner.nextLine();
            groupChatClient.sendInfo(str);
        }
    }
}

3.5.3 测试

我们分别启动服务端和客户端,然后客户端发送消息给服务端

image-20241009142622304

服务端能够正常接收消息和监听客户端状态

image-20241009142721730


3.6 总结

  • Selector:管理多个通道,监听它们的就绪状态,可以有效地处理大规模并发连接。

  • Channel:代表了I/O的连接,支持非阻塞I/O操作。

  • Buffer:用于存储从通道中读取的数据或准备写入通道的数据,通过灵活的读写机制优化数据处理。


四. AIO(Asynchronous I/O)

AIO 全称为异步 I/O(Asynchronous I/O),从 Java 7 开始引入。AIO 提供了真正的异步非阻塞 I/O 操作,适合处理大量连接数且对 I/O 操作要求高的场景。

工作原理:

  • AIO 基于异步事件通知机制,允许在 I/O 操作完成时通知线程进行后续处理。

  • 开发者提交 I/O 请求后,不必等待操作完成,操作系统会在 I/O 操作完成后通知应用程序,应用程序可以通过回调函数处理完成后的逻辑。

  • 这使得系统可以在处理其他任务时继续执行,不需要等待 I/O 任务的完成。

特点:

  • 更加高效地利用系统资源,完全依赖于操作系统的底层支持。

  • 更适合高并发和高负载场景,尤其是处理大量连接但每个连接的 I/O 读写较少的场景。

  • 复杂性更高,支持度取决于操作系统。

AIO 还没有广泛应用,Netty 也是基于 NIO,而不是 AIO


五. BIO、NIO 和 AIO 的比较

特性 BIO NIO AIO
I/O 模型 同步阻塞 同步非阻塞 异步非阻塞
线程模型 每个连接一个线程 单线程处理多连接 异步回调,线程池处理
复杂度 简单 中等
适用场景 少量连接,数据处理简单的场景 大量并发连接,且 I/O 操作频繁 大量并发连接,但 I/O 操作较少
性能

总结

  • BIO 适合小规模、简单的应用,因为其每个连接都需要一个线程来处理,线程资源消耗较大。

  • NIO 适合大规模、高并发的网络应用,能够通过少量线程处理大量连接,但编程复杂度有所增加。

  • AIO 则适合高负载的网络应用,异步非阻塞使得其在高并发下表现更优越,但依赖于操作系统的支持且编程复杂度较高。


参考文档:https://github.com/dongzl/netty-handbook/tree/master/docs

作者:神的孩子都在歌唱

本人博客:https://blog.csdn.net/weixin_46654114

转载说明:务必注明来源,附带本人博客连接。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。