Java 中的 BIO、NIO和 AIO

Java 中的 BIO、NIO和 AIO

BIO

概述

BIO,同步阻塞,即传统的JAVA IO,相关类和接口在java.io包下。BIO的线程模型呈现出以下特点: 一个连接一个线程,每个连接进来都对应着需要新启一个线程去负责处理该连接,可能出现处理连接的 线程被执行时所需的IO资源未准备好,造成线程被阻塞的情况出现。server端处理连接的请求一旦被阻 塞,client也会因为收不到响应而阻塞,因此client端、server端呈现出同步阻塞的特点。

核心

BIO的核心是SocketSocket是对TCP/IP通信过程的一个抽象,它将TCP/IP协议里面复杂的通信逻辑进行 封装,对用户来说,只要通过一组简单的API就可以实现网络的连接和通信。

特点

BIO最后的实现效果是一个连接一个线程。原因是服务端每accept一个socket后,就算主线程接下 来去处理这个Socket也只是处理了一个Socket,因此要并发的处理多个Socket,还是只能在服务端起多 个线程,每个线程处理一个Socket。因此才说一个Socket对应着一个线程。这时就会存在两个问题:

耗时严重 accept的socket并不知道其数据包是否已经收完,很可能出现因为数据包没有收完,还需要阻塞在原地等待IO继续收数据包的情况

同步阻塞 一个Socket被阻塞期间,对映的client端由于没有收到回信,也只有跟着阻塞,无法继续向下执行,这就造成了client端和server的同步阻塞。

总结

BIO(Blocking IO) 是最传统的IO模型,也称为同步阻塞IO。它实现的是同步阻塞模型,即服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情会造成不必要的线程开销,并且线程在进行IO操作期间是被阻塞的,无法进行其他任务。在高并发环境下,BIO的性能较差,因为它需要为每个连接创建一个线程,而且线程切换开销较大,不过可以通过线程池机制改善。BIO适合一些简单的、低频的、短连接的通信场景,例如HTTP请求。

NIO

概述

计算机IO模型

image-20241204175913879

NIO,non-blocking IO,从JDK1.4版本开始引入,其直观的特点就是非阻塞,深入一点来看的话,NIO推出之前的JAVA BIO只是对TCP进行了简单的封装,用户只能对进行简单的IO,而整个计算机底层,在内存中的IO过程是被封装成了黑盒的。NIO对计算机底层的IO过程做了抽象,开放出来了内存粒度的API,让使用者可以更加细粒度的从计算机内存的角度来控制IO。

核心

NIO有三大核心:

  1. channel

  2. buffer

  3. selector

1.buffer:

可以理解为用户段的内存的抽象。

2.channel:

可以理解为用户段和内核段IO区的连接的抽象,当然直接理解为内核段的IO区的抽象其实也可以。

3.selector:

NIO实现非阻塞式IO的核心,其可以基于事件监听的方式,选择准备好的channel,再去其中将数据读到buffer中,然后去操作buffer中的数据。

buffer

buffer,理解为用户段中一块内存的抽象即可。

既然是一块内存,那么其本质上就是用来进行数据读写的一个容器,由java.nio包定义,顶级接口为Buffer,定义了一套API用来管理缓 冲区中的数据针对存储不同的数据类型,有不同的buffer:

  1. ByteBuffer

  2. CharBuffer

  3. ShortBuffer

  4. IntBuffer

  5. LongBuffer

  6. FloatBuffer

  7. DoubleBuffer

buffer具有以下几个基本属性:

  • 容量(capacity),buffer的大小,buffer创建后,容量不能更改。 限制(limit) ,buffer可用的大小,limit之后的区域无法进行读写。 位置(position) 表示接下来要读写的数据的所在位置。 标记(mark) 标记一个位置的索引,调用reset()方法可以回到该位置上

代码示例 由于buffer其实就是一块内存的抽象,是一个数据容器,所以核心其实就是put、get。

此处以byteBuffer为例,其它相同。

byte[] resources = "hello".getBytes();
//初始化
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println(byteBuffer.capacity());
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
//写
        byteBuffer.put(resources);
System.out.println(byteBuffer.capacity());
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
//读
//不开读模式,读不到任何数据
        System.out.println(byteBuffer.get());
        System.out.println(byteBuffer.get());
//开起读模式才能读到数据
        byteBuffer.flip();
System.out.println(byteBuffer.get());
        System.out.println(byteBuffer.get());
​
//读取全部
        System.out.println(new String(byteBuffer.array()));

channel

Channel,通道,由java.nio.channel包下定义,用来向buffer中读写数据,可以理解为内核段和用户段之间进行数据传输的一条逻辑通道,甚至可以直接理解为内核段中内存的一个抽象。

image-20241205092326130

通道具有以下特性:

全双工,读写可以同时进行,即可以向缓冲区中写,又可以向缓冲区中读 支持异步 Channel是一个顶级父接口,针对需要传输的数据格式的不同分为:

FileChannel 用于读取、写入、映射操作文件 DatagramChannel 用于通过UDP读写网络中的数据 SocketChannel 通过TCP读写网络中的数据,底层封装的Socket ServerSocketChannel SocketChannel的升级版,可以自动监听新的TCP连接,每一条新连接创建一个SocketChannel。可以从以下地方获取不同的通道:

  • FileInputStream

  • FileOutStream

  • RandomAccessFile

  • Socket ServerSocket

       try {
FileOutputStream fos = new FileOutputStream( "nio_channel/data01.txt");
//获取file类型的channel
FileChannel channel=fos.getChannel();
//准备好要写出的内容
ByteBuffer buffer=ByteBuffer.allocate(1024);
            buffer.put("helloWorld!".getBytes());
        //将buffer切换成读模式
        buffer.flip();
//写出
            channel.write(buffer);
            channel.close();
        } catch (Exception e) {
        e.printStackTrace();
        }
    try {
        FileInputStream is=new FileInputStream("data01.txt");
        FileChannel channel = is.getChannel();
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //用channel将数据读到buffer中
        channel.read(buffer);
        System.out.println(new String(buffer.array()));
    } catch (IOException e) {
        e.printStackTrace();
    }

在JAVA NIO中有两种方式可以实现文件的复制:

  1. 非零拷贝,即一个通道向buffer中写,另一个通道去buffer中读,数据要走用户段。

  2. 零拷贝,直接从磁盘的一个地方拷贝到磁盘的另一个地方,数据不用走用户段。

1.非零拷贝:

一个channel向buffer中写,另一个channel去buffer中读。

    try {
        File srcFile=new File("data01.txt");
        File targetFile=new File("data02.txt");
        FileInputStream fis=new FileInputStream(srcFile);
        FileOutputStream fos=new FileOutputStream(targetFile);
 
        FileChannel isChannel=fis.getChannel();
        FileChannel osChannel=fos.getChannel();
 
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        while (true){
            //读数据
            int flag=isChannel.read(buffer);
            if(flag==-1){
                break;
            }
            //读模式
            buffer.flip();
            //写数据
            osChannel.write(buffer);
            //清空buffer
            buffer.clear();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

2.零拷贝:

当使用Java NIO进行文件传输时,提供了两个底层使用零拷贝的API,一个是transferTo、一个是transferFrom。可以通过transferTo方法将数据从一个Channel传输到另一个Channel,也可以使用transferFrom方法将数据从一个Channel传输到另一个Channel。

// 定义源文件和目标文件路径
String sourceFilePath = "path/to/source/file.txt";
String targetFilePath = "path/to/target/file.txt";
​
// 创建源文件和目标文件的RandomAccessFile对象
try (RandomAccessFile sourceFile = new RandomAccessFile(sourceFilePath, "r");
RandomAccessFile targetFile = new RandomAccessFile(targetFilePath, "rw")) {
​
// 获取源文件和目标文件的FileChannel
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel targetChannel = targetFile.getChannel();
​
// 使用transferTo()方法将数据从源文件传输到目标文件
// 从源文件的位置0开始,传输全部文件内容到目标文件
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
System.out.println("文件传输成功,传输了 " + transferredBytes + " 字节数据。");
// 使用transferFrom()方法将数据从目标文件传输回源文件
// 从目标文件的位置0开始,传输全部文件内容回源文件
transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
        System.out.println("数据回传成功,传输了 " + transferredBytes + " 字节数据。");
} catch (IOException e) {
        e.printStackTrace();
}

ServerSocketChannel、SocketChannel支持两种阻塞模式:

  • 阻塞模式,遇到阻塞操作产生阻塞的时候会直接阻塞。

  • 非阻塞模式,遇到阻塞操作产生阻塞的时候会直接返回null。

默认都是阻塞模式,可以手动设置为非阻塞模式。

//准备buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//创建服务器
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//设置为非阻塞模式
//serverSocketChannel.configureBlocking(false);
//绑定监听端口
serverSocketChannel.bind(new InetSocketAddress(8080));
//获取连接,这是一步阻塞操作,阻塞模式下,没读到连接会在这一步阻塞;非阻塞模式下不会阻塞,会直接返回一个null
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞模式
//socketChannel.configureBlocking(false);
//读数据,这是一步阻塞操作
if(socketChannel!=null) {
        //阻塞模式下,没读到连接会在这一步阻塞;非阻塞模式下不会阻塞,会直接返回一个null
        socketChannel.read(buffer);
}

selector

selector,NIO实现非阻塞式IO的核心,它的功能很简单,就是用事件机制来监听channel,挑选出触发事件的channel。我们知道,如果线程中有IO操作,IO没有完成,资源没有准备好之前,线程是会进入阻塞状态的。我们可以用单线程起一个selector去监听channel是否准备好数据,将准备好数据的channel挑选出来交给其它线程去处理,这样就不会因为IO资源没准备好导致线程阻塞。

网络通信的时候如果用BIO的方式通信一进来就给一个线程去处理,那么就会有可能因为数据包还没收完,IO等待、阻塞,造成线程阻塞。而用NIO的话就可以用selector挑选出数据包收完的IO出来处理,不会有线程阻塞

image-20241205094302029

服务端代码

//获取通道
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
//切换为非阻塞模式
serverSocketChannel.configureBlocking(false);
//绑定连接的端口
serverSocketChannel.bind(new InetSocketAddress(9999));
//获取选择器
Selector selector=Selector.open();
//将通道注册到选择器上,并开始指定监听接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询监听
while(selector.select()>0){
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
    while (iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
//判当前socket的事件
//1.接收事件(表示socket接收到了数据)
        if(selectionKey.isAcceptable()){
SocketChannel socketChannel=serverSocketChannel.accept();
//切换为非阻塞模式
            socketChannel.configureBlocking(false);
//将通道以读就绪的事件重新注册到选择器
            socketChannel.register(selector,SelectionKey.OP_READ);
        }
                //2.读就绪事件
                if(selectionKey.isReadable()){
SocketChannel socketChannel=(SocketChannel)selectionKey.channel();
//读取数据
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
int length=0;
            while((length=socketChannel.read(byteBuffer))>0){
        byteBuffer.flip();
                System.out.println(new String(byteBuffer.array(),0,length));
        byteBuffer.clear();
            }
                    }
                    //事件处理完成,移除事件
                    iterator.remove();
    }
            }

客户端代码

//获取通道
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
//切换成非阻塞模式
socketChannel.configureBlocking(false);
//分配缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//发送数据
Scanner scanner=new Scanner(System.in);
while(true){
String msg=scanner.nextLine();
    byteBuffer.put(msg.getBytes());
        byteBuffer.flip();
    socketChannel.write(byteBuffer);
    byteBuffer.clear();
}

总结

NIO是Java 1.4引入的新IO模型,也称为同步非阻塞IO,它提供了一种基于事件驱动的方式来处理I/O操作。

相比于传统的BIO模型,NIO采用了Channel、Buffer和Selector等组件,线程可以对某个IO事件进行监听,并继续执行其他任务,不需要阻塞等待。当IO事件就绪时,线程会得到通知,然后可以进行相应的操作,实现了非阻塞式的高伸缩性网络通信。在NIO模型中,数据总是从Channel读入Buffer,或者从Buffer写入Channel,这种模式提高了IO效率,并且可以充分利用系统资源。

NIO主要由三部分组成:选择器(Selector)缓冲区(Buffer)通道(Channel)Channel是一个可以进行数据读写的对象,所有的数据都通过Buffer来处理,这种方式避免了直接将字节写入通道中,而是将数据写入包含一个或者多个字节的缓冲区。在多线程模式下,一个线程可以处理多个请求,这是通过将客户端的连接请求注册到多路复用器上,然后由多路复用器轮询到连接有I/O请求时进行处理。

对于NIO,如果从特性来看,它是非阻塞式IO,N是Non-Blocking的意思;如果从技术角度,NIO对于BIO来说是一个新技术,N的意思是New的意思。所以NIO也常常被称作Non-Blocking I/ONew I/O

NIO适用于连接数目多且连接比较短(轻操作)的架构,例如聊天服务器、弹幕系统、服务器间通讯等。它通过引入非阻塞通道的概念,提高了系统的伸缩性和并发性能。同时,NIO的使用也简化了程序编写,提高了开发效率。

AIO

概述

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

特点

优点:

  1. 非阻塞:AIO的主要优点是它是非阻塞的。这意味着在读写操作进行时,程序可以继续执行其他任务。这对于需要处理大量并发连接的高性能服务器来说是非常有用的。

  2. 高效:由于AIO可以处理大量并发连接,因此它通常比同步I/O(例如Java的传统I/O和NIO)更高效。

  3. 简化编程模型:AIO使用了回调函数,这使得编程模型相对简单。当一个操作完成时,会自动调用回调函数,无需程序员手动检查和等待操作的完成。

缺点:

  1. 复杂性:虽然AIO的编程模型相对简单,但是由于其非阻塞的特性,编程复杂性可能会增加。例如,需要处理操作完成的通知,以及可能的并发问题。

  2. 资源消耗:AIO可能会消耗更多的系统资源。因为每个操作都需要创建一个回调函数,如果并发连接数非常大,可能会消耗大量的系统资源。

  3. 可移植性:AIO在某些平台上可能不可用或者性能不佳。因此,如果需要跨平台的可移植性,可能需要考虑使用其他I/O模型。

AIO适合一些极端的、超高频的、超长连接的通信场景,例如云计算、大数据等。

需要注意的是,目前AIO模型还没有广泛应用,Netty等网络框架仍然是基于NIO模型。

public class AIOServer {
    public static void main(String[] args) throws Exception {
        // 创建一个新的异步服务器套接字通道,绑定到指定的端口上
        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));
        System.out.println("服务端启动成,等待客户端连接。");
        // 开始接受新的客户端连接
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void att) {
                // 当一个新的连接完成时,再次接受新的客户端连接
                serverChannel.accept(null, this);

                // 创建一个新的缓冲区来读取数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                try {
                    InetSocketAddress clientAddress = (InetSocketAddress) clientChannel.getRemoteAddress();
                    InetAddress clientIP = clientAddress.getAddress();
                    int clientPort = clientAddress.getPort();
                    System.out.println("客户端 "+ clientIP + ":" + clientPort + " 连接成功。");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 从异步套接字通道中读取数据
                clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to accept a connection");
            }
        });
        // 保持服务器开启
        Thread.sleep(Integer.MAX_VALUE);
    }
}
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {

    private AsynchronousSocketChannel channel;

    public ReadCompletionHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 当读取完成时,反转缓冲区并打印出来
        attachment.flip();
        byte[] bytes = new byte[attachment.remaining()];
        attachment.get(bytes);
        System.out.println("收到的消息: " + new String(bytes , StandardCharsets.UTF_8));
        attachment.clear();

        // 从键盘读取输入
        Scanner scanner = new Scanner(System.in);
        System.out.print("输入消息: ");
        String message = scanner.nextLine();
        System.out.println();
        // 写入数据到异步套接字通道
        channel.write(ByteBuffer.wrap(message.getBytes()));

        channel.read(attachment , attachment , new ReadCompletionHandler(channel));
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("Failed to read message");
    }
}
public class AIOClient {
    public static void main(String[] args) throws Exception {
        // 创建一个新的异步套接字通道
        AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();

        // 连接到服务器
        clientChannel.connect(new InetSocketAddress("localhost", 5000), null, new CompletionHandler<Void, Void>() {
            @Override
            public void completed(Void result, Void attachment) {
                System.out.println("连接到服务端成功。");
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to connect server");
            }
        });
        
        // 从键盘读取输入
        Scanner scanner = new Scanner(System.in);
        System.out.print("发送消息: ");
        String message = scanner.nextLine();

        // 写入数据到异步套接字通道
        clientChannel.write(ByteBuffer.wrap(message.getBytes()), null, new CompletionHandler<Integer, Void>() {
            @Override
            public void completed(Integer result, Void attachment) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to write message");
            }
        });

        // 保持客户端开启
        Thread.sleep(Integer.MAX_VALUE);
    }
}

总结

Java AIO(Asynchronous I/O)是Java提供的异步非阻塞IO编程模型,从Java 7版本开始支持,AIO又称NIO 2.0。

相比于NIO模型,AIO模型更进一步地实现了异步非阻塞IO,提高了系统的并发性能和伸缩性。在NIO模型中,虽然可以通过多路复用器处理多个连接请求,但仍需要在每个连接上进行读写操作,这仍然存在一定的阻塞。而在AIO模型中,所有的IO操作都是异步的,不会阻塞任何线程,可以更好地利用系统资源。

AIO模型有以下特性:

  1. 异步能力:AIO模型的最大特性是异步能力,对于socket和I/O操作都有效。读写操作都是异步的,完成后会自动调用回调函数。

  2. 回调函数:在AIO模型中,当一个异步操作完成后,会通知相关线程进行后续处理,这种处理方式称为“回调”。回调函数可以由开发者自行定义,用于处理异步操作的结果。

  3. 非阻塞:AIO模型实现了完全的异步非阻塞IO,不会阻塞任何线程,可以更好地利用系统资源。

  4. 高性能:由于AIO模型的异步能力和非阻塞特性,它可以更好地处理高并发、高伸缩性的网络通信场景,进一步提高系统的性能和效率。

  5. 操作系统支持:AIO模型需要操作系统的支持,因此在不同的操作系统上可能会有不同的表现。在Linux内核2.6版本之后增加了对真正异步IO的实现。

LICENSED UNDER CC BY-NC-SA 4.0