NIO、IO复用模型及AIO

首先先回顾IO的概念,IO流是实现输入和输出的基础,而计算机角度的IO就是输入设备以及输出设备就是一组有顺序的、有起点的和重点的集合,是对于数据传输的总称和抽象。输入时是从数据源到程序,输出时是由程序到数据源。

IO模型

IO模型分类

IO模型有阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动式IO模型和异步IO模型。其实也可以分为两类,同步IO和异步IO。

IO操作可以分为两部分:发起IO请求和进行IO操作。同步IO和异步IO的区别在于进行IO操作的方式,如果实际的IO读写操作阻塞以请求进程,就是同步IO,将IO操作交给操作系统来进行,只需要接收接收结果那么就是异步IO,所以阻塞式IO、非阻塞式IO、IO复用模型、信号驱动式IO模型和都是同步IO模型,阻塞式IO和非阻塞式IO说的是对于发起IO请求是否会被阻塞,阻塞式IO是只能监听一个客户端,而非阻塞式是其中的选择器监听多个通道。 

阻塞IO模型(同步阻塞)

阻塞IO模型用户进程发起请求IO调用,这是内核中的数据还未准备好,用户进程就会阻塞一直等待内核数据准备好,再从内核缓冲区中拷贝数据到用户空间,拷贝成功,返回成功指示为止。阻塞IO经典的就是阻塞Socket和Java BIO。

阻塞IO缺点就是性能低,如果内核中数据一直没有准备好,那么就会一直阻塞。

非阻塞IO模型(同步非阻塞)

非阻塞IO会去请求IO调用时,内核如果数据没有准备好,可以先返回一个错误数据,通过轮询的方式来请求IO调用,在数据准备好后再阻塞用户进程。NIO就是非阻塞IO模型

虽然非阻塞IO解决了阻塞IO请求调用阻塞的问题,但是是通过轮询频繁的进行系统调用实现的,使性能还是较低的,消耗了CPU的资源,可以使用IO复用模型来解决这个问题。

非阻塞IO模型步骤如下:

  • 应用进程向内核发送recvfrom请求读取内核数据
  • 内核数据未准备好,返回错误码EWOULDBLOCK
  • 进程轮询调用recvfrom,进程不阻塞等待,继续发送请求
  • 内核数据准备好,内核缓冲区拷贝数据到用户空间,拷贝成功后返回成功指示
  • 处理数据

recvfrom()用来接收远程主机指定的Socket传送的数据,并将数据由参数buf指向的内存空间。

IO复用模型(同步阻塞)

在IO复用模型中,select、poll、epoll都是多路复用器,这些多路复用器都有各自的不同,IO复用模型的原理就是利用多路复用器监控内核中数据状态,等到数据准备好给多路复用器返回可读条件,然后进程再通过recvfrom()进行系统调用将数据复制等到应用缓冲区,再到复制完成,处理数据。(在此期间是阻塞的),改善了NIO用户进程自己反复轮询调用recvfrom()来查看内核中的数据状态。

IO 多路复用之 select

select多路复用器,它与IO复用模型的基本原理过程是大体相同的,只是它监听的最大连接数有限制,在Linux系统上基本是1024,而且当事件发生后,只直到有几个事件发生,不知道是哪几个,因此当select返回后,需要通过遍历fdset来找到相应的fd,也就是需要遍历所有流。

那么poll解决了监听的最大连接数有限的问题,但是仍需要通过遍历所有的文件描述符来找到就绪的Socket,这样的话如果大量客户端连接上来,就会随着文件描述符的增多而性能降低。

IO 多路复用之 epoll

因为select和poll在某些情况下还存在一些问题,因此有了epoll的诞生。它采用了事件驱动来实现。

epoll()先通过epoll_create在内核中创建一个空间并返回一个文件描述符代表着这个空间,epoll_ctl来注册一个fd文件描述符,存放在这个空间中,一旦某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当线程调用epoll_wait()时,返回的是链表其中是就绪的事件。最后的最后还是需要进程自己去调用recvfrom()进行系统调用内核中的数据,将数据从内核复制到用户空间。

select

pollepoll
底层数据结构

数组

链表

红黑树+双链表
获取就绪的fd遍历遍历事件回调
事件复杂度O(n)O(n)O(1)
最大连接数1024无限制无限制
fd数据拷贝每次调用select都需要将fd从用户空间调用到内核空间每次调用poll都需要将fd从用户空间调用到内核空间使用内存映射,不需要用户空间频繁拷贝fd到内核空间
  epoll 明显优化了 IO 的执行效率,但在进程调用 epoll_wait()时,仍然可能被阻塞。能不能不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动 IO 模型

信号驱动IO模型(同步非阻塞)

信号驱动IO模型不再需要反复确认数据是否准备好,用户进程向内核发送一个信号,然后就去干其他事情,等到内核数据准备就绪时,给用户进程返回一个信号,通知应用进程数据准备好的可读状态。用户进程收到信号后,立刻调用recvfrom()进行系统调用内核数据并进行操作。

但说了这么多其实这些IO模型都还是同步IO模型。他们的共同点就是进程进行系统调用内核中的数据时,进行操作还是会阻塞,也就是在数据从内核复制到应用缓冲的时候,都是阻塞的。而AIO就是真正的异步IO模型。

异步IO模型

同步IO尽管在发起IO请求的阶段进行了改善,由阻塞进程到不阻塞进程,但在数据从内核复制到应用缓冲的时候还是阻塞的。因此我们引入了异步IO模型,它在数据从内核复制到应用缓冲时是不阻塞的。AIO实现了IO全流程的非阻塞应用进程向内核进行系统调用,内核会立即返回,但返回的并不是处理结果,只是告诉应用进程这个请求提交成功了。内核将数据准备好后,内核将数据从内核复制到应用缓冲,复制完成后递交信号告诉应用进程IO操作执行完毕。

异步IO模型是非阻塞的,它只用向内核发送一次请求,并能立刻得到返回,并不会陷入阻塞状态,主要就是数据状态的询问和数据拷贝进行阻塞,异步IO很好的解决了这个问题。在现实场景中,比如大批量的转账,后端接收到数据可以先告诉前端提交成功,等处理完成后再返回处理的结果。

JavaNIO概述

Java NIO(Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API,NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。

核心思想

NIO中客户端发送IO请求并不会被阻塞,核心就是注册感兴趣的特定IO事件,当特定事件发生时,就通知用户进程。NIO实现非阻塞IO进程的核心就是Selector(将select、poll、epoll都进行了综合),Selector就是注册各种IO事件的地方。 

从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获取SelectionKey,通过得到的SelectionKey得到当前发生的事件和事件所属的通道SelectableChannel,以获得客户端送来的数据。NIO非阻塞是指不阻塞IO事件本身,也就是请求IO时不阻塞,但是select()方法其实是阻塞的。

JavaNIO的核心部分

Java NIO的组成其实还有很多的类和组件,但是核心的就是上图中说到的,Buffer(缓冲区),Channel(通道),Selector(选择器)。

Buffer(缓冲区):本质上就是一个块内存空间,这个内存空间可以写入数据,可以读取数据,Java将这块内存包装成为了Java NIO的对象,并提供了一系列的方法,便于访问和调用。

Buffer(缓冲区)的基本属性有容量(capacity)缓冲区作为一块内存肯定有它的容量大小,容量不能为负,内存容量在创建后就不可以再更改了。限制(limit)表示缓冲区中可以操作数据的大小(limit后的数据不能进行读写),limit不能为负,也不能大于缓冲区的容量。在写入模式时等于buffer的容量,在读入模式时等于缓冲区中数数据所占的大小。位置(position)下一个要读写数据的索引位置,不能为负也不能超过限制大小。标记(mark)是一个记录索引,调用mark()方法可以记录当前的positon。重置(reset)reset()方法可以回到标记的位置,position回到这个位置。

Java NIO中的缓冲区buffer提供了很多不同数据类型的类,以byte buffer来举例,byte buffer可以是两种类型:一种是基于直接内存(非堆内存),另一种是基于非直接内存(堆内存),基于直接内存的IO操作会提高JVM的工作性能,因为是直接作用于本地IO操作。而基于非直接内存的IO操作,因为是在堆内存中,所以需要先从本进程中复制数据到直接内存中,再在本地IO操作。

从数据流的角度来看,非直接内存的作用链:

本地IO---->直接内存---->非直接内存---->直接内存---->本地IO

直接内存的作用链:

本地IO---->直接内存---->本地IO 

很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要耗费更高的性能。不过这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。

使用场景:存储的数据量大且生命周期很长;网络高并发场景,IO操作频繁

Channel(通道):Channel表示IO源与目标打开的连接Channel通道类似流,但Channel在读数据和写数据时并不会阻塞,是双向的流通常读写的单项的通道是支持读取或者写入缓冲区的,也可以异步地读写,但是是不能直接访问数据的。

常用的Channel实现类

  • FileChannel: 用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过UDP读写网络中的数据通道。
  • SocketChannel: 通过TCP读写网络中的数据。
  • ServerSocketChannel: 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。[ServerSocketChanne 类似ServerSocket , SocketChannel类似Socket]

Selector选择器

 Selector是Java NIO的组件,可以检查一个或多个NIO通道,负责多个通道看哪个已经将数据准备就绪,这样的话,一个线程就可以负责多个Channel,从而管理多个网络连接,提高IO效率

总结:

  • 一个Channel通道对应一个Buffer缓冲区
  • 一个Selector对应多个Channel
  • Buffer缓冲区是一个内存块,底层是数组
  • 程序切换到哪个Channel是由事件决定的
  • Selector根据不同的事件,在通道上切换
  • 数据中的读取和写入是通过Buffer来进行操作的,Java BIO的读取和写入是通过输入流或者输出流(都是单向流)来实现的。而Java NIO中的Buffer可以读取数据也可以输出数据,是双向的,并不会阻塞。
  • 通道是负责连接IO设备的通道,缓冲区是负责存放数据的,操作缓冲区对数据进行处理。简而言之就是Channel负责连接IO设备并传输数据,Buffer负责存取数据。

将数据写出到目标文件中

public class WDemo1 {
    public static void main(String[] args) {
        try {
            //让流与目标文件来连接
            FileOutputStream fileout=new FileOutputStream("wdemo1.txt");
            //得到流中的通道
            FileChannel channel=fileout.getChannel();
            //创建缓冲区
            ByteBuffer bf= ByteBuffer.allocate(1024);
            bf.put("两只老虎爱跳舞,小兔子乖乖拔萝卜!".getBytes());
            //切换到写出模式
            bf.flip();
            channel.write(bf);
            channel.close();
            System.out.println("写数据成功");
        } catch (FileNotFoundException e) {
            System.out.println("写数据失败");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("写数据失败");
            e.printStackTrace();
        }

    }
}

 将目标文件中的数据读取并显示出来

public class RDemo1 {
    public static void main(String[] args) {
        try {
            FileInputStream input=new FileInputStream("wdemo1.txt");
            FileChannel channel=input.getChannel();
            ByteBuffer bf=ByteBuffer.allocate(1024);
            channel.read(bf);
            bf.flip();
            String s=new String(bf.array(),0,bf.remaining());
            System.out.println(s);

        } catch (FileNotFoundException e) {
            System.out.println("读取失败");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("读取失败");
            e.printStackTrace();
        }
    }
}

从一个IO设备中(文件)传输到另一个文件中,要确保它传输的完整,所以clear()方法和flip()方法的使用是很必要的。在读取一部分到缓冲区时需要在操作前清空缓存区中的内容,读取后再写出,写出前也要切换到写出模式就需要调用flip()这个时候limit()会限制到当前数据长度的位置。

public class RWDemo {
    public static void main(String[] args) throws Exception {
        File input=new File("D:\\Java\\Javapic.png");
        File output=new File("D:\\Tend\\Javapic1.png");
        FileInputStream is=new FileInputStream(input);
        FileOutputStream os=new FileOutputStream(output);
       //通过channel与目标文件建立连接
        FileChannel cis=is.getChannel();
        FileChannel cos=os.getChannel();
       //创建一个缓冲区,大小为1024
        ByteBuffer bf=ByteBuffer.allocate(1024);
        while(true){
            //先清空缓存再读取数据到缓存,不然可能会重复
            bf.clear();
            //先读到缓冲区内
            int flag=cis.read(bf);
            //缓冲区中无数据了,就退出
            if(flag==-1){
                break;
            }
            //切换到写出模式,写出缓冲区
            bf.flip();
            cos.write(bf);
        }
        cis.close();
        cos.close();
    }
}

 将文本读到另一个文件下,将数据分散读取(Scatter)到两个缓冲区,再通过聚集写入(Gathering)到Channel中(聚集写入(Gathering)是指将多个Buffer中的数据“聚集”到Channnel​​​​​​​,主要是定义多个缓冲区用来做数据分散,再通过一个同类型的Buffer数组来接收这些缓冲区)

分散读取(Scatter)是指Channel通道的数据读入到多个缓冲区中去。

public class RWDemo2 {
    public static void main(String[] args) throws Exception {
        FileInputStream input=new FileInputStream("wdemo1.txt");
        FileOutputStream output=new FileOutputStream("demo2.txt");
        FileChannel is=input.getChannel();
        FileChannel os=output.getChannel();
        //定义两个缓冲区
        ByteBuffer bf1= ByteBuffer.allocate(6);
        ByteBuffer bf2=ByteBuffer.allocate(1024);
        ByteBuffer[] bfs=new ByteBuffer[]{bf1,bf2};
        is.read(bfs);
        for(ByteBuffer bf:bfs){
            bf.flip();
            System.out.println(new String(bf.array(),0,bf.remaining()));
        }
        os.write(bfs);
        os.close();
        is.close();
    }
}

 加入Selector的服务端流程

public class ServerDemo {
    public static void main(String[] args) {
        try {
            //当客户端连接服务端时,服务端会通过ServerSocektChannel得到SocketChannel
            //也就是获得通道
            ServerSocketChannel channel=ServerSocketChannel.open();
            //切换非阻塞模式
            channel.configureBlocking(false);
            //绑定连接
            channel.bind(new InetSocketAddress(9999));
            //获取选择器
            Selector selector= Selector.open();
            //将通道注册到选择器上,并指定“监听接收事件”
            channel.register(selector, SelectionKey.OP_ACCEPT);
            //轮询式的获取选择器上已经“准备就绪”的事件
            while(selector.select()>0){
                System.out.println("轮一轮");
                //获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
                Iterator<SelectionKey> it=selector.selectedKeys().iterator();
                while(it.hasNext()){
                    //获取准备“就绪”的事件
                    SelectionKey sk=it.next();
                    //判断具体时什么事件准备就绪
                    if(sk.isAcceptable()){
                        //若是接收就绪,获得客户端连接
                        SocketChannel schannel=channel.accept();
                        //切换非阻塞模式
                        schannel.configureBlocking(false);
                        //将该通道注册到选择器上
                        schannel.register(selector,SelectionKey.OP_READ);

                    }else if(sk.isReadable()){
                        //获取当前选择器上“读就绪”状态的通道
                        SocketChannel schannel=(SocketChannel)sk.channel();
                        //读取数据
                        ByteBuffer buf=ByteBuffer.allocate(1024);
                        int len=0;
                        while((len=schannel.read(buf))>0){
                            buf.flip();
                            System.out.println(new String(buf.array(),0,len));
                            buf.clear();
                        }
                    }
                    it.remove();
                }
            }
        } catch (IOException e) {
            System.out.println("IO连接失败");
            e.printStackTrace();
        }

    }
}

客户端流程

public class ClientDemo {
    public static void main(String[] args) {
        try {
            //获取通道
            SocketChannel schannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
            //切换到非阻塞模式
            schannel.configureBlocking(false);
            //分配指定大小的缓冲区
            ByteBuffer buf=ByteBuffer.allocate(1024);
            //发送数据给服务器
            Scanner scanner=new Scanner(System.in);
            System.out.println("请说:");
            while(scanner.hasNext()){
                String s=scanner.nextLine();
                buf.put(("波妞:"+s).getBytes());
                buf.flip();
                schannel.write(buf);
                buf.clear();
            }
            schannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

在群聊的情况下,客户端和服务端的流程

BIO和NIO的区别

  • BIO以流的方式来处理数据,NIO以块的方式来处理数据,块的IO效率比流的IO效率高很多。
  • BIO是同步阻塞IO,NIO是同步非阻塞IO,主要区别在于在IO请求的时候NIO不阻塞用户进程,Java中用select()方法进行阻塞。
  • BIO是基于字节流和字符流来进行数据操作,NIO 是基于Buffer缓冲区和Channel通道来进行数据处理。数据总是从通道读到缓冲区或者是从缓冲区写入到通道中,Selector用于监听多个通道的事件,这样就可以实现用一个线程完成对多个客户端通道的监听。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值