Java的BIO、NIO、AIO详解

1. 阻塞式I/O(BIO)
① 一请求一应答

在这里插入图片描述

  • BIO的描述:
  1. 采用BIO通信模型的服务端,通常由一个独立的Accepor线程负责监听客户的连接请求。一般会在while(true)循环中,不断调用accpet()函数接收客户的连接请求。
  2. 如果有多个客户端连接请求,可以将每个socket设置为一个线程,通过多线程实现多个socket的支持
  3. 由于BIO中的accpet()read()write()函数都是阻塞型的,必须使用多线程。即一个socket连接对应一个线程
  4. 在Java中Acceptor线程对应的ServerSocket对象,ServerSocket对象通过accpet()获取已经就绪的socket连接,并返回socket对象。之后,服务器便可以通过该socket对象与客户进行socket通信。
② BIO的通信实例
  • 服务器端:
  1. 创建ServerSocket对象,在构造函数中绑定port。
  2. 在while(true)循环中不断获取就绪的socket连接,然后为每个socket连接创建一个单独的线程,用于与客户通信。
  3. 输入输出就是普通的字节流输入输出,通过socket对象的getInputStreamgetOutputStream方法可以获取输入输出流。
package socket;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        while (true) {
            Socket socket = serverSocket.accept();
            // 接收到客户端连接后,为每个socket创建一个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 读数据缓冲区
                    byte[] data = new byte[1024];
                    InputStream sin = null;
                    try {
                        sin = socket.getInputStream();
                        int len = 0;
                        // 读取数据直到数据结尾
                        while ((len = sin.read(data)) != -1) {
                            System.out.println(new String(data, 0, len));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    }
}
  • 客户端:
  1. 创建socket对象,在构造函数中指定服务器的IP地址和port。
  2. 如果需要传输文字,可以通过String对象的getBytes()方法获取文字以utf-8的方式编码后的字节流。
package socket;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket=new Socket("127.0.0.1",9999);
        OutputStream sout=socket.getOutputStream();
        sout.write("你好,服务器!".getBytes("utf-8"));
        socket.close();
    }
}
③ BIO中资源耗尽的问题
  • 如果为每一个socket都创建一个线程,会存在很多问题:
  1. 线程的创建、销毁、切换,需要消耗大量的系统资源。有的socket连接在创建以后,甚至不会进行任何通信。
  2. 尤其是在Linux操作系统中,每个线程都看作一个轻量级的进程。
  3. 如果存在大量的客户访问服务器,会导致系统资源的压力激增,会出现线程堆栈溢出、创建新线程失败等情况。
  4. 这些原因,最终可能会导致服务器不能对外提供服务
  • 解决办法: 将socket封装成实现了Runnable接口的Task,通过线程池控制socket连接锁占用的资源。
    在这里插入图片描述
2. 非阻塞式I/O(NIO)
① 关于java的NIO
  • NIO即New Input/Output,对应java.nio包,是JDK1.4中引入的。
  • NIO与IO有相同的作用和目的,但是NIO提供了基于通道(channel)和缓冲区(Buffer)的、面向块的I/O,效率比IO高很多
  • 直接内存: 在JVM中,可以通过Native函数库直接分配堆外内存,使用JVM栈中的DirectByteBuffer对象作为这块内存引用进行操作。这样可以在一些场景中显著提高性能,因为避免了在堆內和堆外来回拷贝数据。
② 流与块
  • NIO与IO最大的区别在于数据打包和传输方式:NIO以块的方式处理数据,IO以流的方式处理数据。
  • 面向流的I/O:
  1. 一次处理一个字节数据: 一个输入流产生一个字节数据,一个输出流消费一个字节数据。
  2. 过滤器使得数据处理变得简单: 为流式数据创建过滤器非常容易,链接几个过滤器,每个过滤器负责复杂处理机制的一部分。
  3. 速度慢: 面向流的I/O通常很慢。
  • 面向块的I/O:
  1. 一次处理一个数据块: 按块处理数据比按流处理数据要快的多。
  2. 面向块的I/O缺少一些面向流的I/O的优雅性和简单性,而且通过对java.io以NIO为基础进行重新实现,使得面向流的处理速度变得更快。
③ 核心组件——通道
  • 通道(Channel)是对IO中流的模拟,NIO通过通道读取和写入数据。
  • 通道与流的比较:
  1. 通道是双向的,可读、可写或者同时用于读写。
  2. 流只能在一个方向上移动,比如一个进程的InputStream必须对应另一进程的OutputStream。要想实现双向通信,必须同时创建InputStreamOutputStream
  • NIO中的通道类型:
  1. FileChannel: 从文件中读写数据。
  2. DatagramChannel: 通过UDP读写网络中的数据。
  3. SocketChannel: 通过TCP读写网络中的数据。
  4. ServerSocketChannel:ServerSocket作用一致,监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
④ 核心组件——缓冲区
  • NIO中数据的读写都要先经过缓冲区:
  1. 在NIO中,发送给一个通道的所有数据都要先放到缓冲区中;同样的,从一个通道中读取任何数据都要先读到缓冲区中。
  2. NIO中不会直接对通道进行数据读写操作,而是要先经过缓冲区。
  • NIO中数据读取和写入的示意图:
    在这里插入图片描述
  • 缓冲区对应的Buffer类:
  1. 缓冲区实质上是一个数组,但又不止是一个数组。它不仅提供了对数据的结构化访问,还可以跟踪系统的读/写进程
  2. 缓冲区对应NIO中的Buffer类,除了Boolean类型,基本数据类型都有对应的缓冲区。即除了Boolean类型,基本数据类型都有对应的Buffer类的子类。
  3. 缓冲区的常见类型:ByteBufferCharBufferShortBufferIntBufferLongBufferFloatBufferDoubleBuffer
  4. 最常用的缓冲区是ByteBufferByteBuffer提供了一组操作byte数组的功能
  • 缓冲区中的三个状态变量:
  1. capacity: 记录缓冲区的最大容量,一般新建一个缓冲区时,limitcapacity的值默认相等。
  2. position: 记录已经从缓冲区中读了或者向缓冲区写了多少数据,指向下一个将要读取或者写入的字节。
  3. limit: 记录缓冲区还有多少数据可以写入或多少数据可以读出,值小于等于capacity
  4. 可以通过clear()方法和flip()方法更改这三个状态变量的值。
  • 缓冲区状态变量的举例:
  1. 初始化时,position指向缓冲区的头部,limitcapacity指向缓冲区的末尾。整个过程中,capacity的位置不会发生改变。容量为8的缓冲区,position = 0limit = capacity = 8
    在这里插入图片描述
  2. 输入通道读取5个字节到缓冲区中,position = 5limit保持不变。
    在这里插入图片描述
  3. 调用flip()方法,更新positionlimit的位置,为将数据从缓冲区写到输出通道做准备。position = 0limit = 5
    在这里插入图片描述
  4. 从缓冲区写四个字节到输出通道,position = 4
    在这里插入图片描述
  5. 调用clear()方法将缓冲区清空,position和limit都将回到最初的位置。position = 0limit = 8
  6. flip()clear()方法的代码如下:
public final Buffer flip() {
   limit = position;
   position = 0;
   mark = -1;
   return this;
}

public final Buffer clear() {
   position = 0;
   limit = capacity;
   mark = -1;
   return this;
}
  • NIO的文件读写实例:
package socket;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileRead {
    public static void main(String[] arg) {
        try {
            // 获取源文件的输入字节流
            FileInputStream fin=new FileInputStream("C:\\Users" +
                    "\\zebra\\IdeaProjects\\HelloWorld\\src\\PhilosopherProblem.java");
            // 获取输入字节流的输入通道
            FileChannel fcin=fin.getChannel();
            // 获取目的文件的输出字节流
            FileOutputStream fout=new FileOutputStream("C:\\Users" +
                    "\\zebra\\Desktop\\1.java");
            FileChannel fcout=fout.getChannel();
            // 建立缓冲区
            ByteBuffer bf=ByteBuffer.allocateDirect(1024);
            int len=0;// 记录实际读取数据的大小
            // 以块为单位,快速将源文件的内容拷贝到目的文件
            while (true){
                // 从输入通道读取数据到缓冲区中,并判断是否读到文件末尾
                if ((fcin.read(bf))==-1){
                    break;
                }
                // 切换读写
                bf.flip();
                // 将缓冲区中的数据写入到输出通道
                fcout.write(bf);
                // 清空缓冲区
                bf.clear();
            }
            fcin.close();
            fcout.close();
            fin.close();
            fout.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
⑤ 核心组件——选择器
  • 为什么NIO被叫做非阻塞I/O(Non Blocking I/O)?
  1. NIO提供了与IO中SocketServerSocket两种套接字相对应的套接字通道SocketChannelServerSocketChannel
  2. SocketChannelServerSocketChannel都支持阻塞模式和非阻塞模式,可以通过channel.configureBlocking(false)设置为非阻塞模式。
  3. NIO在网络通信中的非阻塞模式广泛被使用,因此进场将NIO叫做非阻塞I/O。
  • 注意: 只有套接字通道才能设置为非阻塞模式,FileChannel不能设置。
  • NIO实现了I/O多路复用中的Reactor模型:
  1. 占用一个线程的选择器Selector通过轮询的方式监听多个通道上的事件,这样一个线程可以处理多个事件。
  2. 将被监听的通道设置为非阻塞模式Selector可以立即获取通道的状态。
  3. 如果该通道上的事件未就绪,Selector可以继续轮询其他通道。
    在这里插入图片描述
  • Selector的优点: 因为线程创建、切换、销毁的开销比较大,因此使用一个线程管理多个socket连接,对于具有大量socket连接的场景可以带来很好的性能
⑥ NIO与IO的区别
  • NIO与IO的区别:
  1. 是否支持非阻塞I/O: NIO中的socket通信支持阻塞和非阻塞两种模式,而IO只支持阻塞模式。因此我们通常将NIO叫做非阻塞I/O,而非New I/O
  2. 数据处理: NIO面向块进行数据处理,速度较快;IO面向流进行数据处理,处理的速度较慢。
  3. 通行方式: NIO基于通道进行通信,而通道是双向的,可读、可写或同时读写;IO面向流进行通信,只能单向通信。
3. NIO的编程实例
① NIO+Selector实现高性能的服务器
  • 通过selector实现服务的具体思想:
  1. 先创建ServerSocket通道,为通道对应的ServerSocket绑定ip和port。
  2. 创建Selector对象,将ServerSocket通道设置为非阻塞模式,然后向Selector注册ServerSocket通道。
  3. 在while循环中不断调用selector.select()select()会一直阻塞直到至少有一个事件就绪。
  4. 获取selector监测到的就绪事件,这些事件通过SelectionKey进行记录。
  5. 通过迭代SelectionKey,针对不同的就绪事件采取不同的操作。处理完事件后,手动移出该事件。
package NIOSocket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建服务器通道,为服务器通道绑定ip和port
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.socket().bind(new InetSocketAddress("127.0.0.1", 9999));

        // 创建selector对象,将服务器通道置为非阻塞模式,并向selector注册该通道
        Selector selector = Selector.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 循环执行selector的select方法
        while (true) {
            // select方法会监听是否有事件就绪,它会一直阻塞直到有事件就绪
            selector.select();
            // 获取就绪事件的SelectionKey,通过迭代遍历SelectionKey
            // 针对不同的SelectionKey,对其对应的通道实行不同的操作
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 有新的的socket连接到达,需要为该连接创建socket通道
                // 将通道设置为非阻塞模式,然后将新的socket通道注册到selector中
                if (key.isAcceptable()) {
                    ServerSocketChannel temp = (ServerSocketChannel) key.channel();
                    SocketChannel channel = temp.accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 通道可读,从通道上读取数据,读取完后关闭通道
                    SocketChannel temp = (SocketChannel) key.channel();
                    readData(temp);
                    temp.close();
                }
                // 手动移出已经处理过的I/O事件
                iterator.remove();
            }
        }
    }

    public static void readData(SocketChannel socketChannel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        StringBuilder sb=new StringBuilder();// 记录最终的数据
        int len = 0;
        while (true) {
            if ((len = socketChannel.read(buffer)) == -1) {
                break;
            }
            buffer.flip(); // 切换读写
            int limit = buffer.limit();
            // 获取缓冲区的中数据
            char[] chars = new char[limit];
            for (int i = 0; i < limit; i++) {
                chars[i] = (char) buffer.get(i);
            }
            sb.append(chars);
            buffer.clear();
        }
        System.out.println(sb);
    }
}
② 简单的客户端
  • 客户端的具体流程:
  1. 创建SocketChannel,调用通道的connect()方法与服务器进行连接。
  2. 创建缓冲区,向缓冲区中放入数据,通过flip()方法切换读写。
  3. 调用通道的write()方法,向服务器发送数据。
package NIOSocket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.connect(new InetSocketAddress("127.0.0.1", 9999));
        channel.configureBlocking(false);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("hello, lucy!".getBytes());
        buffer.flip();
        channel.write(buffer);
        channel.close();
    }
}
4. 异步I/O(AIO)
① AIO简介
  • AIO是JDK1.7中引入的NIO的改进版,称为NIO 2.0,它是异步的I/O模型。
  • AIO基于事件和回调机制实现: 当执行某个I/O操作时,当前线程不会阻塞,而是立即返回;操作系统在I/O操作完成后,会主动通知相应的线程进行后续的操作。
  • NIO中虽然I/O操作也会立即返回,但是并不保证I/O事件已就绪,需要通过轮询监听I/O事件是否就绪。如果I/O事件已就绪,还需要线程自己去完成数据的读写操作。
  • 关于Netty:
  1. Netty是一个异步事件驱动的,具有高性能高可靠性的网络应用框架。
  2. 实质并没有使用AIO,而是使用的NIO。
  3. Netty解决了NIO不易使用的问题,如果需要使用NIO,可以使用Netty框架。
② java几种I/O的应用场景
  • BIO适用于连接数目比较少且固定的架构,在JDK 1.4以前只能使用BIO进行socket通信。
  • NIO适用于连接数目比较多且连接比较短的架构,如聊天服务器,从JDK 1.4开始支持。编程复杂,可以使用网络框架Netty。
  • AIO适用于连接数目比较多且连接比较长的架构,如相册服务器,从JDK 1.7开始支持。虽然可以充分调用操作系统的并发,但编程复杂。
5. 面经问题总结

**1. Java中NIO的原理 **

  • 基础: NIO面向块进行数据处理,三个核心组件:通道、缓冲区、selector。
  • 进阶: NIO与IO的区别(是否支持非阻塞I/O、数据处理、通信方式)
  • 高阶: 对I/O对路复用的讲解(select、poll、epoll)

2. Java中的BIO、NIO、AIO

  • BIO: 以client和server之间的socket连接为例,accept()函数会一直阻塞直到有连接就绪。包括read()、write()方法都是阻塞式的,一个连接需要对应一个线程。
  • NIO: 不停的轮询事件是否就绪,如果事件已就绪,则执行相应的I/O操作。面向块 + 三个核心组件,为什么会叫做非阻塞I/O而不是New I/O
  • AIO: 异步I/O,基于事件和回调机制。一个线程发起一个I/O操作后可以立即返回,操作系统完成I/O操作后通知线程执行后续操作。
  • NIO编程比较复杂,一般使用Netty网络框架。

3. NIO中缓冲区中的三种状态量

  • 三种状态量:position、limit、capacity。
  • 初始时,从输入通道读取数据、调用flip()方法后,向输出通道发送数据、调用clear()方法。

4. 针对每个连接维护一个线程,开销很大,如何优化?

  • 使用NIO,NIO中使用了Selector实现了I/O多路复用,一个线程便能处理多个事件。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值