BIO、NIO、AIO、Netty从简单理解到使用

Java编程中BIO、NIO、AIO是三种不同的I/O(输入/输出)模型,它们代表了不同的I/O处理方式。

Netty就是基于Java的NIO(New Input/Output)类库编写的一个高性能、异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

先来了解一下基本的三种基本的io模型:

BIO(Blocking I/O,阻塞I/O)

定义 :BIO是Java最传统的I/O模型,基于流的同步阻塞I/O操作。每个连接都会占用一个线程,当进行I/O操作时,线程会被阻塞,直到操作完成。
特点

同步阻塞:每个I/O操作都会阻塞当前线程,直到操作完成。

线程占用:每个连接需要一个独立的线程,线程资源消耗较大。

实现简单:代码实现相对简单,易于理解和编写。
作用:适用于连接数较少且固定的架构,如传统的C/S架构。由于其实现简单、编程直观,因此在一些简单的网络编程场景中仍然被使用。

创建客户端和服务端演示数据接收与传输。

复制代码
//服务端
public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(9999)) {
            while (true) {
                Socket socket = serverSocket.accept(); // 持续监听新连接
                new Thread(() -> { // 为每个客户端创建独立线程
                    try (BufferedReader reader = new BufferedReader(
                            new InputStreamReader(socket.getInputStream()))) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            System.out.println("客户端[" + socket.getPort() + "] 消息: " + line);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//客户端
public static void main(String[] args) {
        System.out.println("客户端启动...");
        try {
            //创建套接字
            Socket socket = new Socket("127.0.0.1", 9999);
            //获取输出流发送消息
            PrintStream ps = new PrintStream(socket.getOutputStream());
            Scanner sc = new Scanner(System.in);
            while (true){
                System.out.println("请输入信息:");
                ps.println(sc.nextLine());
                ps.flush();
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

NIO(Non-blocking I/O,非阻塞I/O)

定义 :NIO是Java 1.4引入的新I/O API,基于通道(Channel)和缓冲区(Buffer)的非阻塞I/O操作。
特点

非阻塞:I/O操作不会阻塞线程,线程可以在等待数据时执行其他任务。

多路复用:通过Selector可以管理多个Channel,提高了资源利用率。

高性能:适用于高并发场景,减少线程开销和上下文切换。
核心组件

缓冲区(Buffer):用于存储数据的固定大小的内存区域,提供了多种类型的缓冲区,如ByteBuffer、CharBuffer等。

通道(Channel):用于数据读写的通道,支持非阻塞模式,与缓冲区配合使用。

选择器(Selector):允许单个线程同时处理多个通道的I/O事件。
作用:适用于连接数较多且连接较短的架构,如高并发的服务器端应用。NIO提供了更高效、更灵活的I/O操作方式,显著提高了系统的并发性能和吞吐量。

复制代码
//服务端
    public static void main(String[] args) throws Exception {
        //创建连接通道,ServerSocketChannel 绑定端口,监听新的客户端连接请求。它本身不处理任何数据传输。
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置通道为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //创建 Selector 并注册 ACCEPT 事件
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO 服务端启动...");

        //阻塞等待事件发生
        while (selector.select()>0){
            //获取到所有事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            //遍历所有事件
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //检查是否有新的连接请求
                if(key.isAcceptable()){
                    //为每一个创建新的通道
                    SocketChannel clientChannel = serverSocketChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector,SelectionKey.OP_READ);
                }else if(key.isReadable()){
                    //处理读数据
                    SocketChannel channel = (SocketChannel)key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int bytesRead = 0;
                    while ((bytesRead = channel.read(byteBuffer)) > 0){
                        byteBuffer.flip();
                        String message = new String(byteBuffer.array(),0,bytesRead);
                        System.out.println("接收客户端信息:"+message);
                        byteBuffer.clear();

                        //回写数据给客户端
                        ByteBuffer response = ByteBuffer.wrap(("ECHO: " + message).getBytes());
                        channel.write(response);
                    }
                }
                iterator.remove();
            }
        }
    }

//客户端
public static void main(String[] args) throws Exception {
        // 1. 创建 SocketChannel 并连接服务器
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
        socketChannel.configureBlocking(false); // 非阻塞模式

        System.out.println("NIO 客户端已连接");

        //接收线程
        new Thread(() -> {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (true) {
                try {
                    int bytesRead = socketChannel.read(buffer);
                    if (bytesRead > 0) {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("收到服务端响应: " + new String(data));
                        buffer.clear();
                    }
                } catch (IOException e) {
                    System.out.println("连接已断开");
                    break;
                }
            }
        }).start();

        //发送消息
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.println("请输入信息:");
            String msg = sc.nextLine();
            byteBuffer.put(("AA:"+msg).getBytes(StandardCharsets.UTF_8));
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
    }

AIO(Asynchronous I/O,异步I/O)

定义 :AIO是Java 7引入的异步I/O API,基于异步通道(AsynchronousChannel)和异步回调机制。
特点

异步非阻塞:I/O操作是异步的,通过回调处理结果,不阻塞线程。

回调机制:通过回调函数处理I/O操作结果,提高了程序的灵活性。

高性能:适用于高并发、高吞吐量的场景。
工作方式

程序发起一个异步I/O请求,并提供一个回调函数。

程序无需等待I/O操作完成,而是立即返回,继续执行其他任务。

当I/O操作完成后,系统会调用之前提供的回调函数,传递结果或状态信息。
作用:适用于连接数较多且连接时间较长的架构,如高并发的服务器端应用。AIO提供了真正的异步I/O操作方式,进一步提高了系统的并发性能和吞吐量。

Netty

先了解一下netty出现的原因,是干嘛用的?

  • Netty出现的原因:

Java NIO的复杂性:

Java NIO虽然提供了非阻塞IO的能力,但其API设计较为底层,使用起来比较复杂。开发者需要处理大量的细节,如选择器的管理、缓冲区的操作等,这增加了开发的难度和出错的风险。
性能瓶颈:

在高并发场景下,直接使用Java NIO进行网络编程可能会遇到性能瓶颈。例如,选择器的实现和多线程管理的复杂性可能导致性能下降。
缺乏高级功能:

Java NIO仅提供了基础的非阻塞IO机制,缺乏一些高级功能,如协议编解码、连接管理等。这些功能在开发网络应用时非常重要,但实现起来却相对复杂。

  • Netty的具体用途是什么?

简化网络编程:

Netty封装了Java NIO的复杂性,提供了一套简洁易用的API。开发者可以使用这些API快速构建网络应用,而无需关注底层的细节。
提高性能:

Netty采用了异步非阻塞IO模型,并支持零拷贝等技术,可以在保证高性能的同时,减少CPU和内存资源的消耗。这使得Netty在高并发场景下表现尤为出色。
支持多种协议:

Netty内置了对多种协议的支持,如TCP、UDP、HTTP、WebSocket等。开发者可以轻松地使用这些协议构建网络应用,而无需自己实现协议编解码等复杂功能。
灵活的扩展性:

Netty提供了丰富的扩展点,如ChannelHandler、Codec等。开发者可以通过实现这些接口来扩展Netty的功能,以满足特定的业务需求。
广泛的应用场景:

Netty经过广泛的使用和验证,具有高稳定性和可靠性,适用于各种网络应用场景,如分布式系统、微服务架构中的通信组件、实时通讯系统、游戏服务器等。

它提供了丰富的错误处理和恢复机制,能够有效地处理网络通信中的各种异常情况。
netty核心优势:

异步事件驱动、零拷贝、内存池、高度可定制。

  • 核心组件

先大概了解一下 Reactor 模型(单线程、多线程、主从多线程)。

因为Reactor 模式是 Netty 高性能和高并发能力的核心设计基础。Netty 的线程模型、事件驱动机制和异步非阻塞 I/O 都深度依赖 Reactor 模式。

Reactor 模式是一种 事件驱动的设计模式,用于处理高并发的 I/O 请求。其核心思想是 用一个或多个线程监听事件(如连接、读写请求),并将事件分发给对应的处理器异步处理,避免线程阻塞和资源浪费。

Reactor模型的核心组件

事件源(Event Source):指任何可以产生I/O事件的对象,例如网络连接、文件、设备等。

事件循环(Event Loop):Reactor模型的核心,负责监控所有事件源的状态,并在有事件发生时将其分发给相应的处理程序(即回调函数)。

事件处理器(Event Handler):处理特定事件的代码模块,通常实现为回调函数或方法。针对特定的事件源,每一个事件源通常都有一个相应的处理器,用于处理该事件源的I/O事件。

多路复用器(Demultiplexer):用于监视多个事件源并将发生的事件通知给Reactor。常见的实现包括Java的Selector、Linux的epoll等。

Reactor模型的实现方式

单Reactor单线程模型:所有操作(连接、读写)由一个线程完成。设计简单,但在高并发场景下容易成为性能瓶颈。

单Reactor多线程模型:Reactor线程负责监听和分发事件,而事件的处理则交给线程池中的工作线程完成。这种方式能够充分利用多核CPU的处理能力,但在高并发场景下,Reactor线程可能成为性能瓶颈。

主从Reactor多线程模型(Netty 默认模型-高并发):也称为多Reactor多线程模型。主Reactor线程负责监听和分发连接事件,当有新的连接到来时,将其分发给从Reactor线程处理。从Reactor线程负责监听和分发读写事件,并交给线程池中的工作线程处理。这种方式能够进一步提高系统的并发处理能力和可扩展性。

写个netty服务端示例看一下:

复制代码
// 1. 创建主 Reactor(处理连接)管理一组 EventLoop,每个线程绑定一个 EventLoop(事件循环线程)
EventLoopGroup bossGroup = new NioEventLoopGroup(1);  // 主线程组(通常 1 个线程)
// 2. 创建子 Reactor(处理 I/O)
EventLoopGroup workerGroup = new NioEventLoopGroup();  // 子线程组(默认 CPU 核数 × 2)

//Reactor 模式配置入口	绑定主/子线程组、设置 Channel 类型
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)  // 绑定主从线程组
       .channel(NioServerSocketChannel.class)  // 设置 Channel 类型(NIO)代表一个 Socket 连接或监听端口
       .childHandler(new ChannelInitializer<SocketChannel>() {
           @Override
           protected void initChannel(SocketChannel ch) {
               ch.pipeline().addLast(new ServerHandler());  // 添加业务处理器,这里需要自己实现
           }
       });

// 3. 绑定端口并启动服务
ChannelFuture future = server.bind(8080).sync();
//等待服务器通道关闭
future.channel().closeFuture().sync();
//关闭通道
finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

//业务处理器
public class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端连接成功...");
    }
   }
  • 组件说明:

Channel:

在Netty中,Channel是一个核心概念,Channel提供了数据的读取和写入操作,以及与远程端点进行通信的能力。

通过Channel可以获取到很多信息,比如:本地地址、远程地址、Channel的EventLoop、Channel的Pipeline等。

Channel 生命周期:

channelRegistered (初始化操作)→ channelActive (连接)→ channelRead(数据接收) → channelInactive(断开)→ channelUnregistered(注销)
Selector:

Selector是Java NIO(New Input/Output)中的一个核心组件,用于监控多个Channel(通道)的状态,例如连接、读、写等事件。在netty中Selector被封装在NioEventLoop中,当Channel注册到Selector并指定感兴趣的事件类型(如连接、读、写等)后,会返回一个SelectionKey,用于表示Selector与Channel之间的关联关系。Selector会不断地轮询其注册的Channel,如果有事件发生,Selector会将相应的SelectionKey放入就绪事件集合中。事件循环线程会从就绪事件集合中取出SelectionKey,并根据事件类型调用相应的ChannelHandler进行处理。处理完成后,事件循环线程会将Channel放回到Selector中,继续等待下一次事件的发生。

EventLoopGroup:

管理一组 EventLoop,通常分为 bossGroup(处理连接)和 workerGroup(处理 I/O)。

配置线程数:new NioEventLoopGroup(4) 表示 4 个线程。
ServerBootstrap:

用于启动服务器端的引导类,配置服务器参数和处理器链配置、启动服务器、关闭服务器。

  1. 配置服务器参数:

线程模型:ServerBootstrap允许您配置两个主要的线程组:bossGroup和workerGroup。

bossGroup用于处理客户端的连接请求,而workerGroup用于处理已连接的客户端的I/O操作。

通道类型:通过channel方法,您可以指定服务器使用的通道类型。对于NIO传输,通常使用NioServerSocketChannel。

通道选项:使用option方法可以设置服务器通道的选项,如SO_BACKLOG(TCP连接请求的最大队列长度)和SO_REUSEADDR(允许地址重用)。

子通道选项:childOption方法用于设置已连接客户端的通道选项,如SO_KEEPALIVE(保持连接活动状态)。

Option:可以应用于Channel或Bootstrap上。

ChannelOption:专门用于配置Channel的参数。

  1. 处理器链配置:

父处理器:通过handler方法,您可以设置处理服务器通道I/O事件的处理器。这通常用于处理连接请求。

子处理器:childHandler方法用于设置处理已连接客户端I/O事件的处理器。这是开发者编写业务逻辑处理代码的地方,通常通过添加一系列的ChannelHandler来实现。

ChannelPipeline用于处理网络事件和数据的流动。负责将一系列的处理逻辑(称为ChannelHandler)串联起来,形成一个处理链,对网络事件进行拦截和处理。

事件处理:ChannelPipeline负责处理入站(Inbound)和出站(Outbound)事件,如数据读取、数据写入、连接建立、连接断开等。

拦截器链:ChannelPipeline内部维护了一个拦截器链(或称为处理器链),每个拦截器(即ChannelHandler)都可以对事件进行处理或拦截。

动态性:ChannelPipeline允许在运行时动态地添加、删除或替换拦截器,从而灵活地扩展和定制网络处理逻辑。

ChannelHandler是ChannelPipeline中的基本处理单元,负责处理或拦截入站和出站事件。每个ChannelHandler都有一个关联的ChannelHandlerContext,通过它可以方便地与其他组件进行交互。ChannelPipeline则是ChannelHandler的容器,负责ChannelHandler的管理和事件拦截。

  1. 启动服务器:

    配置完成后,通过调用bind方法并传入服务器的端口号,ServerBootstrap将启动服务器并绑定到指定的端口。

    bind方法返回一个ChannelFuture对象,您可以使用sync方法等待服务器启动完成。

    ChannelFuture是Netty框架中特有的接口,继承自Java的Future接口。在Netty中,所有的I/O操作都是异步的,ChannelFuture用于在操作完成时通知应用程序,以便应用程序可以执行某些操作或检索操作的结果。

  2. 关闭:

当服务器需要关闭时,可以调用ChannelFuture对象的channel().closeFuture().sync()方法,等待服务器通道关闭。

最后,调用bossGroup和workerGroup的shutdownGracefully方法,以优雅地断开连接并关闭线程组。

ChannelHandlerContext:

ChannelHandlerContext封装了Channel和ChannelPipeline,使得ChannelHandler可以方便地访问和操作ChannelPipeline和Channel。

ChannelHandlerContext提供了许多方法,用于操作和传播事件,如写入数据、刷新数据、触发读事件等。

ChannelHandlerContext允许ChannelHandler将事件传递给ChannelPipeline中的下一个处理器。例如,当一个入站事件(如数据读取)发生时,ChannelHandlerContext可以调用fireChannelRead方法,将事件传递给下一个入站处理器。

相关推荐
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 分钟前
配置springdoc swagger开关
java
Echo flower6 分钟前
Spring Boot WebFlux 实现流式数据传输与断点续传
java·spring boot·后端
没有bug.的程序员12 分钟前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
鸽鸽程序猿17 分钟前
【Redis】Java客户端使用Redis
java·redis·github
悦悦子a啊17 分钟前
使用 Java 集合类中的 LinkedList 模拟栈以此判断字符串是否是回文
java·开发语言
Lucky小小吴19 分钟前
java代码审计入门篇——Hello-Java-Sec(完结)
java·开发语言
一个想打拳的程序员21 分钟前
无需复杂配置!用%20docker-webtop%20打造跨设备通用%20Linux%20桌面,加载cpolar远程访问就这么简单
java·人工智能·docker·容器
一起养小猫23 分钟前
LeetCode100天Day2-验证回文串与接雨水
java·leetcode
清晓粼溪27 分钟前
Java登录认证解决方案
java·开发语言
液态不合群33 分钟前
查找算法详解
java·数据结构·算法