BIO、NIO、AIO


一、概念

1、IO

IO 全程 Input/Output,即数据的读取(接收)或写入(发送)操作,针对不同的数据存储媒介,大致可以分为网络 IO 和磁盘 IO 两种。

而在 Linux 系统中,为了保证系统安全,操作系统将虚拟内存划分为内核空间和用户空间两部分。因此用户进程无法直接操作IO设备资源,需要通过系统调用完成对应的IO操作。

即此时一个完整的 IO 操作将经历一下两个阶段:用户空间 <-> 内核空间 <-> 设备空间。

img

2、同步与异步(返回结果)

同步和异步针对的是返回结果,是否立即返回

  • 同步:是发起一个调用后,被调用者未处理完请求之前,调用不会返回
  • 异步:是发起一个调用之后,立刻得到被调用者回应表示已经接收到请求。但返回的并不是结果,这是调用者可以去处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。
  • 区别:同步需要调用者等待结果,而异步不需要调用者等待结果,被调用者会通过回调等机制来通知调用者其返回结果。

3、阻塞和非阻塞(调用方)

阻塞和非阻塞针对的是调用方的动作,发起请求后是否会等待

  • 阻塞:发起一个请求,调用者会一直等待请求结果的返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞:发起一个请求,不用一直等待返回结果,可以先去处理其他事情。

4、缓冲区

应用层的 IO 操作基本都是依赖操作系统提供的 read 和 write 两大系统调用实现。但由于计算机外部设备(磁盘、网络)与内存、CPU 的读写速度相差过大,若直接读写涉及操作系统中断,因此为了减少 OS 频繁中断导致的性能损耗和提高吞吐量,引入了缓冲区的概念。

根据内存空间的不同,又可分为内核缓冲区和进程缓冲区。

操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行 IO 设备的中断处理,集中执行物理设备的实际 IO 操作,通过这种机制来提升系统的性能。至于具体什么时候执行系统中断(包括读中断、写中断)则由操作系统的内核来决定,应用程序不需要关心。

二、BIO(Blocking I/O)

同步阻塞I/O模式,数据读取写入必须阻塞在一个线程内等待其完成。

1、BIO模型

BIO全称是Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:

image-20230529172402051

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。

2、传统BIO(一请求一应答通信模型)

只能处理一个连接,因为采用的是同步阻塞,如果请求长时间占用,程序将不会继续执行,此时如果有请求,则无法获取。

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接

public class BioServer {
    ServerSocket serverSocket;
    private String msg;

    public BioServer(int port) {
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("服务器已经启动,监听端口是:" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() throws IOException {
        while (true){
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int len = inputStream.read(buffer);
            if (len > 0) {
                String msg = new String(buffer, 0, len);
                System.out.println("收到:" + msg);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new BioServer(8080).listen();
    }
}
public class BioClient {
    public static void main(String[] args) throws IOException {
        Socket client = new Socket("localhost", 8080);
        OutputStream outputStream = client.getOutputStream();
        String name = UUID.randomUUID().toString();
        System.out.println("客户端发送数据:" + name);
        outputStream.write(name.getBytes());
        outputStream.close();
        client.close();
    }
}

案例2

服务端

// 创建一个 BIO Socket服务端
ServerSocket serverSocket = new ServerSocket(9001);

while (true){
    //阻塞监听服务端来自客户端的连接
    System.out.println("等待客户端连接...");
    Socket clientSocket = serverSocket.accept();
    System.out.println("客户端已连接:" + clientSocket);

    // 缓冲区
    int bytesSize = 5;
    byte[] bytes = new byte[bytesSize];

    // 获取输入流
    InputStream inputStream = clientSocket.getInputStream();

    // 读取数据到缓冲区,返回值为读取的总字节数,-1代表全部读取完毕
    int read = 0;
    StringBuilder msg = new StringBuilder();
    while(true) {
        // 阻塞读取
        read = inputStream.read(bytes);
        // 客户端断开连接
        if (read == -1){
            clientSocket.close();
            break;
        }
        String s = new String(bytes, 0, read);
        msg.append(s);
        System.out.println("读取到:" + s);

        // 判断结束标记
        String endFlag = "endFlag";
        int endFlagStart = msg.length() - endFlag.length();

        if (msg.length() > endFlag.length() &&
            msg.substring(endFlagStart, msg.length()).equals(endFlag)){
            System.out.println("读取结束:" + msg.substring(0, endFlagStart));

            // 回写数据
            OutputStream outputStream = clientSocket.getOutputStream();
            outputStream.write("收到".getBytes());
            outputStream.flush();
            break;
        }
    }

}

客户端

Socket client = new Socket("localhost", 9001);

// 发送消息
OutputStream outputStream = client.getOutputStream();
String name = UUID.randomUUID().toString() + "endFlag";
System.out.println("客户端发送数据:" + name);
outputStream.write(name.getBytes());
outputStream.flush();

// 缓冲区
byte[] bytes = new byte[1024];
int read = 0;

// 读取消息
while (true){
    // 阻塞读取
    read = client.getInputStream().read(bytes);
    if (read == -1){
        break;
    }
    String msg = new String(bytes);
    System.out.println("client读取到字节数:" + read + "-" + msg);
}

// 关闭Socket 会自动关闭其中的流,同样关闭流也会导致Socket关闭
client.close();
System.out.println("客户端退出");

3、 伪异步IO(加入线程池)

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

总结:
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

三、NIO(New I/O)

1、NIO简介

一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO基于Channel和Buffer进行数据的传输,其中Channel是一条双向传输的数据通道,Buffer是一个内存缓冲区,用于暂存写入通道或从通道读取的数据。

它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。1)对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;2)对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

2、NIO模型

Java NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

image-20230529173559347

  1. Selector 对应一个线程,一个线程对应多个 Channel。
  2. 每个 Channel 对应一个 Buffer。
  3. 程序切换到那个 Channel 是由事件决定的(Event)。
  4. Selector 会根据不同的事件,在各个通道上切换。
  5. Buffer 就是一个内存块,底层是有一个数组。
  6. 数据的读取和写入是通过 Buffer,但是需要flip()切换读写模式,而 BIO 是单向的,要么输入流要么输出流。

3、NIO的特性和组件

如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。

特性

(1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

Java IO的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了

(2)Buffer(缓冲区)

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

  • NIO类库加入Buffer对象,即所有数据都是用缓冲区处理的,在读取数据时,它是直接读到缓冲区中,在写入数据时,写入缓冲区中。任何时间访问NIO中的数据,都是通过缓冲区进行操作。
    最常用的缓冲区是ByteBuffer,一个ByteBuffer用于操作byte数组。每一种Java基本类型都对应一种缓冲区(除了boolean类型)
  • 在面向流IO中,可以将数据直接写入或者将数据直接读到Stream对象中,虽然Stream中也有Buffer开头的扩展类,但是只是流的包装类,还是从流读到缓冲区。

(3)Channel (通道)

NIO通过Channel(通道)进行读写。
通道是双向的,可读可写,而流是单向。因为通道只能和Buffer交互,因为Buffer,通道可以异步的读写。

(4)Selectors(选择器)

NIO有选择器,而IO没有。
选择器是用于单个线程处理多个通道。

4、NIO 读数据和写数据方式

NIo中所有的IO都是从Channel(通道)开始的。

  • 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据。
  • 从通道进行数据写入:创建一个缓冲区,填充数据,并要求铜带写入数据。
public class NioServer {
    //准备两个东西  轮询器(叫号系统)+缓冲区(等候区)
    private Selector selector;
    private ByteBuffer buffer = ByteBuffer.allocate(1024);
    private int port = 8080;

    public NioServer(int port) {
        try {
            this.port = port;
            ServerSocketChannel socketChannel = ServerSocketChannel.open();
            socketChannel.bind(new InetSocketAddress(this.port));
            //为了兼容BIO NIO默认使用阻塞
            socketChannel.configureBlocking(false);

            //叫号系统工作
            selector = Selector.open();

            //开门营业
            socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        System.out.println("listen on " + this.port + ".");
        try {
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    process(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void process(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel channel = serverSocketChannel.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            int len = channel.read(buffer);
            if (len > 0) {
                buffer.flip();
                String content = new String(buffer.array(), 0, len);
                channel.register(selector, SelectionKey.OP_WRITE);
                //在KEY上携带一个附件,一会儿写出去
                key.attach(content);
                System.out.println("读取内容:" + content);
            }
        } else if (key.isWritable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            String attachment = (String) key.attachment();
            channel.write(ByteBuffer.wrap(("输出:" + attachment).getBytes()));
            channel.close();
        }
    }

    public static void main(String[] args) {
        new NioServer(8080).listen();
    }
}

四、AIO(Asynchronous I/O)

AIO是NIO2,即在java7中引入NIO的改进版NIO2,踏实异步非阻塞的IO模型。异步IO是基于事件和回调记住实现。也就是引用操作之后会直接返回,不会阻塞等待,当后台处理完,操作系统会通知相应的线程进行后续的操作。
除了AIO其他的IO类型都是同步的。

区别

1.BIO:同步阻塞,服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。当然,这种情况可以通过线程池机制改善,但并不能从本质上消除这个弊端。

2.NIO:在JDK1.4以前,Java的IO模型一直是BIO,但从JDK1.4开始,JDK引入的新的IO模型NIO,它是同步非阻塞的。而服务器的实现模式是多个请求一个线程,即请求会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理。

3.AIO:JDK1.7发布了NIO2.0,这就是真正意义上的异步非阻塞,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)。

应用场景:并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty。

BIO是一个连接一个线程。 NIO是多个请求一个线程。 AIO是多个有效请求一个线程。

https://www.cnblogs.com/mikechenshare/p/16587635.html

https://blog.csdn.net/adminpd/article/details/124546529


  目录