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:

NIOBIO
面向缓冲区面向流
非阻塞阻塞
基于通道的双向数据流单向数据流
有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读取一个数据:1position=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    */@Testpublic 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();        }    }}@Testpublic 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...