乐趣区

关于java:IO那些事

IO(InputOutput): 即输入输出,通常指数据在存储器(外部和内部)或其余周边设备之间的输出和输入,是信息处理系统(例如计算机)与内部世界(可能是人类或另一信息处理系统)之间的通信。说的简略点就是 与外部设备(比方磁盘)传输数据

IO 大抵能够分为 磁盘 IO网络 IO内存 IO。通常所说的 IO 指的是前两者。本文将简略介绍 Linux 的 五大 IO 模型 java 中的 IO 模型,并对 java 的NIO 做一个根本介绍。

IO 根本流程

外围设备的间接读写波及到 中断 ,中断时须要保留过程数据、状态等信息、中断完结后须要复原过程数据和状态,这种 老本是比拟高的 。因而呈现了一个叫 内核缓冲区 (位于内核空间)的货色,咱们的程序 并不是间接与 IO 设施交互的,而是与这个内核缓冲区交互

<center>io 流程示意图 </center>

如图所示,读的时候,先将数据从磁盘或者网卡拷贝到内核缓冲区(这一步是操作系统内核通过读中断实现的),而后从内核缓冲区拷贝到过程缓冲区(位于用户空间)

写的时候,先将数据写到过程缓冲区,而后拷贝到内核缓冲区,而后写到网卡或者刷到磁盘(这一步是通过写中断实现的)。

读中断和写中断何时进行是内核决定的,大多数的 IO 操作并没有理论的 IO,而是在过程缓冲区与内核缓冲区来回拷贝数据。

一个残缺的读流程包含两个阶段:

  1. 筹备数据:将数据从网卡拷贝到内核缓冲区
  2. 拷贝数据:将数据从内核缓冲区复制到过程缓冲区

两个重要的名词

  • 同步与异步:同步就是用户空间是发动 IO 的一方,异步是内核空间是发动 IO 的一方。也能够了解为同步就是本人要去查 IO 状态,异步是内核能够告诉你
  • 阻塞与非阻塞 :阻塞就是当你调用了一个 IO 读或者写时,须要等内核操作 彻底 (筹备与拷贝数据) 实现后能力返回,这一段时间用户空间程序是“卡住的状态”;非阻塞就是,调用了一个读或写时不论内核有没有操作实现,都会立刻返回。

五大 IO 模型

同步阻塞

<center> 同步阻塞 IO 模型 </center>

这个模型印证了上述对同步与异步、阻塞与非阻塞的解释。内核筹备和拷贝数据的过程中,用户空间程序始终阻塞,所以是阻塞;用户空间是发动 io 的一方,所以是同步。

同步非阻塞

<center> 同步非阻塞 IO 模型 </center>

同步非阻塞的特点就是在数据 筹备阶段 发动 io 调用会立刻 返回一个谬误 ,用户空间须要 轮询 发动 IO 调用。在数据从内核缓冲区 拷贝到过程缓冲区 阶段的调用 依然是会被阻塞 的。这种模型须要始终轮询 IO 状态,用的比拟少。

IO 多路复用

<center>IO 多路复用模型 </center>

在 IO 多路复用模型中,引入了一种 新的零碎调用 查问 IO 的就绪状态 。在 Linux 零碎中,对应的零碎调用为select/epoll 零碎调用。通过该零碎调用,一个过程 能够监督多个文件描述符 一旦某个描述符就绪(个别是内核缓冲区可读 / 可写),内核可能将就绪的状态返回给应用程序。随后,应用程序依据就绪的状态,进行相应的 IO 零碎调用。

————来自《Netty、Redis、Zookeeper 高并发实战》

相比于同步阻塞模型,这种模型的劣势在于 一个线程能解决大量的 IO 连贯,而同步阻塞只能靠开很多线程来解决多个 IO 连贯,对于大量的 IO 连贯无能为力。

如果连接数少的话,同步阻塞并不一定比 IO 多路复用性能差,因为 IO 多路复用有两个零碎调用,同步阻塞只有一个。

信号驱动

<center> 信号驱动 IO 模型 </center>

这种 IO 模型用的不多,java 里边找不到对应实现。信号驱动式模型的一个显著特点就是用 户态过程不再期待内核态的数据筹备好,间接能够去做别的事件。然而期待数据从内核缓冲区拷贝到过程缓冲区依然是阻塞的。

异步 IO(AIO)

<center> 异步 IO 模型 </center>

上述几种 IO 模型 实质上都是同步 IO,就算是信号驱动,他在数据从内核缓冲区拷贝到过程缓冲区也是阻塞的。

AIO 的根本流程是:用户线程通过零碎调用,向内核注册某个 IO 操作。内核在整个 IO 操作(包含数据筹备、数据复制)实现后,告诉用户程序,用户执行后续的业务操作.

这种 IO 模型是完满的 IO 模型,然而据说 Linux反对的不太好。赫赫有名的 netty 也是应用的多路复用 IO 模型,还没有应用 AIO。

java 中的 IO

BIO

BIO 就是 Blocking IO, 对应下面说的同步阻塞 IO 模型。咱们常应用的各种 InputStream, 这种 Reader,以及在网络编程用到的 ServerSocket/Socket 都是 BIO。以一个 Socket 程序为例来直观感受一下这种模型。

<center>BIO-server</center>

<center>BIO-client</center>

这两段代码别离展现一个 tcp 服务端和客户端,实现的性能就是客户端从本地读一个文件发送给服务端,服务端将收到的文件写入磁盘。

服务端的 read 办法的调用是阻塞的,这意味着这个服务端同一时刻只能解决一个连贯 ,这显然不合理,为了解决这个问题,咱们能够思考 多线程机制,主线程只负责承受连贯,收到连贯就丢进其余线程进行解决,能够每次都开一个线程,也能够思考应用线程池。如下的代码实现了这个想法。

<center>BIO 的多线程版本 </center>

NIO

NIO,能够说是 java 中的新 IO(New IO), 也能够叫 None-Blocking IO, 他对应的是前文提到的 多路复用 IO 模型

NIO 包含三个核心成员,Buffer、Channel、Selector, 后文会做具体介绍。

这里简略比照一下 NIO 和 BIO:

NIO BIO
面向缓冲区 面向流
非阻塞 阻塞
基于通道的双向数据流 单向数据流
有 Selector 的概念

上边 BIO 的例子能够看到 BIO 是面向流的,NIO 是面向缓冲区的,能够工作他的数据是一块一块的,通过后文的例子能够更分明的看到这一点。

BIO 都是阻塞的,也是就内核在筹备数据拷贝数据阶段,用户空间发动 IO 的过程没法干别的事。NIO 是能够是非阻塞的,他能够通过注册你感兴趣的事件(比方可读)到 Selector 中,而后干别的事(比方接管新的连贯),当收到相应事件后再做解决。

NIO 有一个通道的概念,既能够向通道里写数据也能够从里边读。然而 BIO 就不行,只能从输出流里边读数据,不能写;也只能往输入流写数据,而不能从里边读。

AIO

对应前文提到的异步 IO 模型,这种模型反对不太好,JAVA AIO 框架在 windows 下应用 windows IOCP 技术,在 Linux 下应用 epoll 多路复用 IO 技术模仿异步 IO。鼎鼎大名的 netty 也没有应用 AIO,所以这里也不去深刻探索了。

NIO 根底详解

Buffer

Buffer 是一个抽象类,能够认为是一个 装数据的容器,底层是数组。他有很多子类:

例如:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

应用最多的是 ByteBuffer

Buffer 的根本构造如下:

<center>Buffer 的构造 </center>

这几个属性的含意是必须要搞清楚的,这里简略列举,后文探讨 Buffer 的基本操作会做进一步阐明。

  • position: 示意以后正在读的地位
  • limit: 示意能够读取或者写入的下限地位,只有 小于 这个值的地位才是无效的
  • capacity: 容量,不是字节数,而是能装几个数据 ,与每个数据占用的字节数无关, 创立时确定,不能再扭转
  • mark: 一个标记地位,能够不便的回到这个地位

buffer 的基本操作:

  • put(): 向缓冲区存数据
  • get(): 从缓冲区取数据
  • flip(): 切换到读取数据的模式
  • rewind():position 回到起始地位,能够反复读
  • clear():清空缓冲区,然而数据依然存在,limit,position 回到最后状态
  • hasRemaining():判断是否还有数据能够读
  • remaining():残余几个数据能够读
  • mark(): 标记以后操作的地位
  • reset(): 回到之前标记的地位

咱们间接通过一个 demo 来阐明这些操作:

<center>Buffer 的基本操作 </center>

输入如下:

创立后:position=0,capacity=10,limit=10
写入一个数据后:position=2,capacity=10,limit=10
切换为读模式后:position=0,capacity=10,limit=2
读取一个数据:1
position=1,capacity=10,limit=2
调用 rewind:
position=0,capacity=10,limit=2
再次读一个数据:position=1,capacity=10,limit=2
调用 Buffer.clear 后
position=0,capacity=10,limit=10

通过这个测试能够看出各种操作的根本应用及其对 Buffer 几个属性的影响。

间接缓冲区与非间接缓冲区:

  • 非间接缓冲区:通过 allocate()调配的缓冲区,将缓冲区建设在 jvm 的内存中
  • 间接缓冲区:通过 allocateDirect()调配的缓冲区,将缓冲区建设在物理内存中,zero copy
  • 能够通过 isDirect()判断是否是间接缓冲区

Channel

NIO 中的一个连贯用一个通道示意,通道自身并不存放数据,只能与 Buffer 交互。

常见的通道:

  1. FileChannel: 用于读写文件的通道
  2. SocketChannel:用于 Socket 套接字 TCP 连贯的数据读写
  3. ServerSocketChannel:容许咱们监听 TCP 连贯申请,为每个监听到的申请,创立一个 SocketChannel 套接字通道
  4. DatagramChannel:用于 UDP 协定的数据读写

通道的获取办法:

  1. 通过反对通道的类的 getChannel 办法

本地 io:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
fileInputStream.getChannel();

网络 io:

  • Socket
  • ServerSocket
  • DatagramSocket
socket.getChannel();
  1. 应用各个通道的静态方法 open()获取,jdk>=1.7
FileChannel fileChannel = FileChannel.open(Paths.get("a.jpg"), StandardOpenOption.READ);
  1. 应用 Files 的 newByteChannel()获取,jdk>=1.7
SeekableByteChannel byteChannel = Files.newByteChannel(Paths.get("a.jpg"), StandardOpenOption.WRITE);

通道的基本操作

  1. 读:将通道里的数据读到 buffer 里,返回值示意读取到的数据个数,返回 0 示意没有了。此办法还有几个重载
public int read(ByteBuffer dst) throws IOException
  1. 写: 将 buffer 写入通道,也有几个重载
 public int write(ByteBuffer src) throws IOException
  1. 获取以后通道的大小,单位 byte
public abstract long size() throws IOException
  1. 将一个通道的数据发送到另一个通道
public long transferTo(long position, long count,
                                    WritableByteChannel target)
        throws IOException;
  1. 上述反向
public long transferFrom(ReadableByteChannel src,
                                      long position, long count)
        throws IOException;
  1. 敞开通道
public final void close() throws IOException

此外还有内存映射文件、锁相干内容。限于篇幅,此处不再开展,之后可能专门写一篇探讨。

Selector

咱们能够将一个通道注册到 Selector 中,并且指定你感兴趣的事件(能够是多个,两头用 |)。通过一直调用 select 抉择 IO 就绪事件,在产生相应事件时会失去一个告诉,做后续解决。

选择器的使命是实现 IO 的多路复用。一个通道代表一条连贯通路,通过选择器能够同时监控多个通道的 IO(输入输出)情况。选择器和通道的关系,是监控和被监控的关系。

这里还波及到 SelectionKey 的概念,SelectionKey 选择键就是那些被选择器选中的 IO 事件。

次要办法:

  1. 关上一个 Selector
public static Selector open() throws IOException
  1. 获取 SelectionKey
public Set<SelectionKey> selectedKeys();
  1. 抉择感兴趣的 IO 就绪事件
1. public int select(long timeout)
        throws IOException;
2. public int select() throws IOException;
  1. 敞开 Selector
public void close() throws IOException;

NIO 波及的概念和 API 较多,上面通过一个具体的例子简略演示(移除了异样解决、敞开通道或连贯的操作)

IO 事件:

  • (1)可读:SelectionKey.OP_READ
  • (2)可写:SelectionKey.OP_WRITE
  • (3)连贯:SelectionKey.OP_CONNECT
  • (4)接管:SelectionKey.OP_ACCEPT

并不是所有 Channel 都反对这几个事件,例如 ServerSocketChannel 只反对 OP_ACCEPT

一个 NIO 传文件的例子

/**
    * 移除了一些敞开通道的代码,可能无奈运行
    * 失常应该在 try finally 敞开,或者应用 try with resources 语法主动敞开
    * @throws IOException
    */
@Test
public void server() throws IOException {
    // 取得 channel
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 绑定端口
    serverSocketChannel.bind(new InetSocketAddress(1234));
    // 设置为非阻塞,这很重要!!!
    serverSocketChannel.configureBlocking(false);
    // 关上 Selector
    Selector selector = Selector.open();
    // 将通道注册到 Selector
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    int i = 0;
    while (selector.select() > 0) { // 轮询抉择感兴趣的 io 事件
        // 拿到选择键
        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
        while (iterator.hasNext()) { // 遍历选择键,对特定工夫做解决,能够独自去开线程解决
            SelectionKey key = iterator.next();
            if (key.isAcceptable()) { // 解决接管事件
                ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                SelectableChannel channel = serverChannel.accept();
                channel.configureBlocking(false);
                // 将客户端连贯的 SocketChannel 也进行注册
                channel.register(selector, SelectionKey.OP_READ);
            } else if (key.isReadable()) { // 解决读事件
                ByteBuffer buffer = ByteBuffer.allocate(1 * mb);
                SocketChannel clientChannel = (SocketChannel) key.channel();
                FileChannel fileChannel = FileChannel.open(Paths.get(path, "qrcode" + (++i) + ".png"),
                        StandardOpenOption.WRITE, StandardOpenOption.CREATE);
                int len = -1;
                while ((len = clientChannel.read(buffer)) > 0) {buffer.flip(); // 切换到读模式
                    fileChannel.write(buffer);
                    buffer.clear(); // 切回写模式,别忘了!!}
                clientChannel.close();
                fileChannel.close();}
            // 解决过的事件肯定要移除
            iterator.remove();}
    }
}

@Test
public void client() throws IOException {
    // 获取 channel
    SocketChannel socketChannel = SocketChannel.open();
    // 连贯
    socketChannel.connect(new InetSocketAddress(1234));
    // 设置非阻塞
    socketChannel.configureBlocking(false);
    // 开选择器
    Selector selector = Selector.open();
    // 将 channel 注册进选择器
    socketChannel.register(selector, SelectionKey.OP_WRITE);
    while (selector.select() > 0) { // 抉择感兴趣的事件
        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
        while (iterator.hasNext()) {SelectionKey key = iterator.next();
            SocketChannel channel = (SocketChannel) key.channel();
            if (key.isWritable()) { // 解决可写事件
                FileChannel fileChannel = FileChannel.open(Paths.get(path, "qrcode.png"), StandardOpenOption.READ);
                ByteBuffer byteBuffer = ByteBuffer.allocate(1 * mb);
                int len = -1;
                while ((len = fileChannel.read(byteBuffer)) > 0) {byteBuffer.flip();
                    channel.write(byteBuffer);
                    byteBuffer.clear();}
            }
        }
    }
}

NIO 应用步骤总结

  1. 获取 Channel
  2. 关上 Selector
  3. 将 channel 注册到 Selector
  4. 轮询感兴趣的事件
  5. 遍历 SelectionKey 并最不同事件类型做相应解决

NIO 的难度的确比 BIO 高不少,而且上述只是一个简略的例子,而且可能存在问题,理论中会比这里简单的多,比方粘包拆包、序列化之类的问题。正因如此,才有了 Netty,Netty 有十分宽泛的利用,比方 Dubbo 底层、RocketMQ 等等。Netty 是后边须要和大家一起钻研的话题。

小结

本文介绍了 5 种 IO 模型,同步阻塞、同步非阻塞、多路复用、信号驱动、异步;而后介绍了 java 中的三种 IO 模型;最初对 NIO 的根底反对点做了简略介绍。冀望能帮忙你温习或者理解相干知识点,疏漏之处,请不吝指出。IO 之路,道阻且长,加油~

参考资料

  • 《Netty、Redis、Zookeeper 高并发实战》
  • https://yasinshaw.com/article…
  • https://www.bilibili.com/vide…

退出移动版