NIO总结

一  NIO介绍

  NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操做,而NIO基于Channel和Buffer(缓冲区)进行操做,数据老是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(好比:链接打开,数据到达)。所以,单个线程能够监听多个数据通道。html

  NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的Java IO面向流意味着每次从流中读一个或多个字节,直至读取全部字节,它们没有被缓存在任何地方。此外,它不能先后移动流中的数据。若是须要先后移动从流中读取的数据,须要先将它缓存到一个缓冲区。linux

  NIO的缓冲导向方法略有不一样。数据读取到一个它稍后处理的缓冲区,须要时可在缓冲区中先后移动。这就增长了处理过程当中的灵活性。可是,还须要检查是否该缓冲区中包含全部您须要处理的数据。并且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里还没有处理的数据。编程

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

  NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,可是它仅能获得目前可用的数据,若是目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,因此直至数据变得能够读取以前,该线程能够继续作其余的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不须要等待它彻底写入,这个线程同时能够去作别的事情。线程一般将非阻塞IO的空闲时间用于在其它通道上执行IO操做,因此一个单独的线程如今能够管理多个输入和输出通道(channel)。缓存

二  IO模型分类 

  按照《Unix网络编程》的划分,I/O模型能够分为:阻塞I/O模型、非阻塞I/O模型、I/O复用模型、信号驱动式I/O模型和异步I/O模型,按照POSIX标准来划分只分为两类:同步I/O和异步I/O。服务器

  如何区分呢?首先一个I/O操做其实分红了两个步骤:发起IO请求(即内核准备数据报)和实际的IO操做(即将数据报从内核复制到用户空间)。同步I/O和异步I/O的区别就在于第二个步骤是否阻塞,若是实际的I/O读写阻塞请求进程,那么就是同步I/O,所以阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O都是同步I/O,若是不阻塞,而是操做系统帮你作完I/O操做再将结果返回给你,那么就是异步I/O。网络

  阻塞I/O和非阻塞I/O的区别在于第一步,发起I/O请求是否会被阻塞,若是阻塞直到完成那么就是传统的阻塞I/O,若是不阻塞,那么就是非阻塞I/O。数据结构

(1)阻塞I/O模型 :在linux中,默认状况下全部的socket都是阻塞的,即从发起请求到最终获取到数据报都处于阻塞状态。一个典型的读操做流程大概是这样:并发

    

(2)非阻塞I/O模型:linux下,能够经过设置socket使其变为non-blocking,当发起recvfrom系统调用时,若是内核中数据报尚未准备好,则直接返回一个EWOULDBLOCK标志,而不阻塞用户线程,应用经过轮询调用recvfrom判断数据报是否准备好,若是数据报已经准备好,这时再调用recvfrom系统调用,数据报会从内核复制到用户空间,复制的这段时间会阻塞用户进程。当对一个non-blocking socket执行读操做时,流程是这个样子:app

    

(3)I/O复用模型:咱们能够调用selectpoll,阻塞在这两个系统调用中的某一个之上,而不是真正的IO系统调用上,select/poll的好处就在于单个线程就能够同时处理多个网络链接的IO。它的基本原理就是select/epoll这个方法会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。当用户进程调用了select,那么整个进程会被block,而同时,内核会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从内核拷贝到用户进程。:

    

   名词解释:

  一、文件描述符fd

  Linux的内核将全部外部设备均可以看作一个文件。那么对外部设备的操做均可以看作对文件进行操做。咱们对一个文件的读写,都经过调用内核提供的系统调用;内核给咱们返回一个filede scriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区等一些属性)。那么咱们的应用程序对文件的读写就经过对描述符的读写完成。

  二、select

  select 函数监视的文件描述符分3类,分别是writefds、readfds和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写或者有except)或者超时(timeout指定等待时间,若是当即返回设为null便可)函数返回。当select函数返回后,能够经过遍历fdset,来找到就绪的描述符。

  缺点:

  一、select最大的缺陷就是单个进程所打开的FD是有必定限制的,它由FDSETSIZE设置,32位机默认是1024个,64位机默认是2048。 通常来讲这个数目和系统内存关系很大,具体数目能够cat /proc/sys/fs/file-max察看。

  二、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。 当套接字比较多的时候,每次select()都要经过遍历FDSETSIZE个Socket来完成调度,无论哪一个Socket是活跃的,都遍历一遍。这会浪费不少CPU时间。

  三、须要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

  三、poll

  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,而后查询每一个fd对应的设备状态,若是设备就绪则在设备等待队列中加入一项并继续遍历,若是遍历完全部fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了屡次无谓的遍历。

  它没有最大链接数的限制,缘由是它是基于链表来存储的,可是一样有一个缺点:

    一、大量的fd的数组被总体复制于用户态和内核地址空间之间,而无论这样的复制是否是有意义。

    2 、poll还有一个特色是“水平触发”,若是报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

  注意:从上面看,select和poll都须要在返回后,经过遍历文件描述符来获取已经就绪的socket。事实上,同时链接的大量客户端在一时刻可能只有不多的处于就绪状态,所以随着监视的描述符数量的增加,其效率也会线性降低。

  四、epoll

  epoll是在2.6内核中提出的,是以前的select和poll的加强版本。相对于select和poll来讲,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

  基本原理:epoll支持水平触发和边缘触发,最大的特色在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,而且只会通知一次。还有一个特色是,epoll使用“事件”的就绪通知方式,经过epoll_ctl注册fd,一旦该fd就绪,内核就会采用相似callback的回调机制来激活该fd,epoll_wait即可以收到通知。

  epoll的优势:

    一、没有最大并发链接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。

    二、效率提高,不是轮询的方式,不会随着FD数目的增长效率降低。 只有活跃可用的FD才会调用callback函数;即Epoll最大的优势就在于它只管你“活跃”的链接,而跟链接总数无关,所以在实际的网络环境中,Epoll的效率就会远远高于select和poll。

    三、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减小复制开销。

(4)信号驱动式I/O模型:咱们能够用信号,让内核在描述符就绪时发送SIGIO信号通知咱们:

    

(5)异步I/O模型:用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从内核的角度,当它受到一个asynchronousread以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,内核会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,内核会给用户进程发送一个signal,告诉它read操做完成了:

    

三  传统IO和NIO具体实际用法区别

传统IO:

 public static void method2(){
        InputStream in = null;
        try{
            in = new BufferedInputStream(new FileInputStream("src/nomal_io.txt"));
            byte [] buf = new byte[1024];
            int bytesRead = in.read(buf);
            while(bytesRead != -1)
            {
                for(int i=0;i<bytesRead;i++)
                    System.out.print((char)buf[i]);
                bytesRead = in.read(buf);
            }
        }catch (IOException e)
        {
            e.printStackTrace();
        }finally{
            try{
                if(in != null){
                    in.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

NIO:

public static void method1(){
        RandomAccessFile aFile = null;
        try{
            aFile = new RandomAccessFile("src/nio.txt","rw");
            FileChannel fileChannel = aFile.getChannel();//获取通道
            ByteBuffer buf = ByteBuffer.allocate(1024);//建立Buffer并分配空间
            int bytesRead = fileChannel.read(buf);//从通道中读取数据到Buffer中
            System.out.println(bytesRead);
            while(bytesRead != -1)
            {
                buf.flip();//切换为读模式
                while(buf.hasRemaining())
                {
                    System.out.print((char)buf.get());//从Buffer中读取数据
                }
                buf.compact();//清空已读的数据,未读的数据会整理到Buffer头部
                bytesRead = fileChannel.read(buf);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile != null){
                    aFile.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

四  Buffer

  Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO库里,全部数据都是用缓冲区处理的,在读取数据时,它是直接读到缓冲区中的,在写入数据时,写入到缓冲区中。任什么时候候访问NIO中的数据,都是经过Buffer缓冲区进行操做。

  Buffer其实是一个数组。Buffer关注的是存放什么类型的数据,只支持基本数据类型,并且不支持基本类型的boolean类型。因此基础的Buffer,就是7种,对应Java的7个基础类型:

  

  一、从上面的示例中能够总结出使用Buffer通常遵循下面几个步骤:

    (1)分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); 还有一种allocateDirector后面再陈述)

    (2)写入数据到Buffer(int bytesRead = fileChannel.read(buf);)

    (3)调用filp()方法( buf.flip();)

    (4)从Buffer中读取数据(System.out.print((char)buf.get());)

    (5)调用clear()方法或者compact()方法

  二、Buffer顾名思义:缓冲区,其实是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,可是读写的数据都必须通过Buffer。以下图:

    

  向Buffer中写数据:

    (1)从Channel写到Buffer (fileChannel.read(buf))

    (2)经过Buffer的put()方法 (buf.put(…))

  从Buffer中读取数据:

    (1)从Buffer读取到Channel (channel.write(buf))

    (2)使用get()方法从Buffer中读取数据 (buf.get())

  能够把Buffer简单地理解为一组基本数据类型的元素列表,它经过几个变量来保存这个数据的当前位置状态:capacity,position,limit,mark。其中capacity在读写模式下都是固定的,就是分配的缓冲大小,position相似于读写指针,表示当前读(写)到什么位置,limit在写模式下表示最多能写入多少数据,此时和capacity相同,在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同:

索引

说明

capacity

缓冲区数组的总长度

position

下一个要操做的数据元素的位置

limit

缓冲区数组中不可操做的下一个元素的位置:limit<=capacity

mark

用于记录当前position的前一个位置或者默认是-1

  咱们经过ByteBuffer.allocate(11)方法建立了一个11个byte的数组的缓冲区,初始状态图,position的位置为0,capacity和limit默认都是数组长度。  

     

  当写入5个字节时,变化以下图:

     

  当须要将缓冲区中的5个字节数据写入Channel的通讯信道,因此咱们调用ByteBuffer.flip()方法,即调用flip方法后的变化以下图所示(position设回0,并将limit设成以前的position的值):

     

  这时底层操做系统就能够从缓冲区中正确读取这个5个字节数据并发送出去了。在下一次写数据以前再调用clear()方法,缓冲区的索引位置又回到了初始位置。

  在写模式下调用flip()方法,buffer从写模式切换到读模式,limit会设置为position当前的值(即当前写了多少数据),postion会被置为0,以表示读操做从缓存的头开始读。也就是说调用flip以后,读写指针指到缓存头部,而且设置了最多只能读出以前写入的数据长度(而不是整个缓存的容量大小)。

  注意:buffer.flip();必定得有,若是没有,就是从文件最后开始读取的,固然读出来的都是byte=0时候的字符。经过buffer.flip();这个语句,就能把buffer的当前位置更改成buffer缓冲区的第一个位置。

   调用clear()方法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉咱们能够从哪里开始往Buffer里写数据。若是Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着再也不有任何标记会告诉你哪些数据被读过,哪些尚未。若是Buffer中仍有未读的数据,且后续还须要这些数据,可是此时想要先写些数据,那么使用compact()方法。compact()方法将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素正后面。limit属性依然像clear()方法同样,设置成capacity。如今Buffer准备好写数据了,可是不会覆盖未读的数据。

   经过调用Buffer.mark()方法,能够标记Buffer中的一个特定的position,以后能够经过调用Buffer.reset()方法恢复到这个position。Buffer.rewind()方法将position设回0,因此你能够重读Buffer中的全部数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。

五  Channel

  Channel是一个通道,网络数据经过Channel读取和写入。Channel和IO中的Stream(流)是差很少一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream。而Channel是双向的,既能够用来进行读操做,又能够用来进行写操做。

  NIO中的Channel的主要实现有:

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

  分别能够对应文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是围绕这4个类型的Channel进行陈述的。

  Channel 必需要配合 Buffer 一块儿使用,咱们永远不可能将数据直接写入到 Channel 中,一样也不可能直接从 Channel 中读取数据。都是经过从 Channel 读取数据到 Buffer 中或者从 Buffer 写入数据到 Channel 中,以下:

  

   上面的示例代码是FileChannel的使用方式。

  这里使用SocketChannel来继续探讨NIO。NIO的强大功能部分来自于Channel的非阻塞特性,套接字的某些操做可能会无限期地阻塞。在传统的Socket IO中,对accept()方法的调用可能会由于等待一个客户端链接而阻塞;对read()方法的调用可能会由于没有数据可读而阻塞,直到链接的另外一端传来新的数据。总的来讲,建立/接收链接或读写数据等I/O调用,均可能无限期地阻塞等待,直到底层的网络实现发生了什么。慢速的,有损耗的网络,或仅仅是简单的网络故障均可能致使任意时间的延迟。然而不幸的是,在调用一个方法以前没法知道其是否阻塞。

  NIO的channel抽象的一个重要特征就是能够经过配置它的阻塞行为,以实现非阻塞式的信道。

  channel.configureBlocking(false)

  在非阻塞式信道上调用一个方法老是会当即返回。这种调用的返回值指示了所请求的操做完成的程度。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,若是有链接请求来了,则返回客户端SocketChannel,不然返回null。

  这里先举一个TCP应用案例,客户端采用NIO实现,而服务端依旧使用BIO实现。

  客户端代码(案例3):

  public static void client(){
        ByteBuffer buffer = ByteBuffer.allocate(1024);//建立Buffer并分配空间
        SocketChannel socketChannel = null;
        try{
            socketChannel = SocketChannel.open();//获取客户端SocketChannel
            socketChannel.configureBlocking(false);//设置为非阻塞
            socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));//链接服务端地址
            if(socketChannel.finishConnect())//若是完成了链接
            {
                int i=0;
                while(true)
                {
                    TimeUnit.SECONDS.sleep(1);
                    String info = "I'm "+i+++"-th information from client";
                    buffer.clear();
                    buffer.put(info.getBytes());//写入数据到Buffer
                    buffer.flip();//切换为读模式
                    while(buffer.hasRemaining()){//若是buffer没有读完
                        System.out.println(buffer);
                        socketChannel.write(buffer);//写入数据到通道
                    }
                }
            }
        } catch (IOException | InterruptedException e){
            e.printStackTrace();
        } finally{
            try{
                if(socketChannel!=null){
                    socketChannel.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

  服务端代码(案例4):

  public static void server(){
        ServerSocket serverSocket = null;
        InputStream in = null;
        try {
            serverSocket = new ServerSocket(8080);//建立传统的阻塞模式服务端Socket
            int recvMsgSize = 0;
            byte[] recvBuf = new byte[1024];
            while(true){
                Socket clntSocket = serverSocket.accept();//打开接受客户端链接,若是没有客户端链接,则阻塞在这个方法上
                SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();//获取客户端地址
                System.out.println("Handling client at "+clientAddress);
                in = clntSocket.getInputStream();//获取客户端输入,阻塞IO模式
                while((recvMsgSize=in.read(recvBuf))!=-1){//若是尚未读完
                    byte[] temp = new byte[recvMsgSize];
                    System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
                    System.out.println(new String(temp));
                }
            }
        } catch (IOException e){
            e.printStackTrace();
        } finally{
            try{
                if(serverSocket!=null){
                    serverSocket.close();
                }
                if(in!=null){
                    in.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

  根据上面的案例,总结一下SocketChannel的用法。

  (1)打开SocketChannel:

  socketChannel = SocketChannel.open();
  socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));

  发送数据到Server:

  String info = "I'm "+i+++"-th information from client";
  buffer.clear();
  buffer.put(info.getBytes());//写入数据到Buffer中
  buffer.flip();
  while(buffer.hasRemaining()){
    System.out.println(buffer);
    socketChannel.write(buffer);//从Buffer写数据到channel中,并发送到Server端
  }

  注意SocketChannel.write()方法的调用是在一个while循环中的。write()方法没法保证能写多少字节到SocketChannel。因此,咱们重复调用write()直到Buffer没有要写的字节为止。

  非阻塞模式下,read()方法在还没有读取到任何数据时可能就返回了。因此须要关注它的int返回值,它会告诉你读取了多少字节。  

  关闭:

  socketChannel.close();

5、Selector

  Selector运行单线程处理多个Channel,若是你的应用打开了多个通道,但每一个链接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,

而后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就能够处理这些事件,事件的例子有如新的链接进来、数据接收等。

  若是用传统的方式来处理这么多客户端,使用的方法是循环地一个一个地去检查全部的客户端是否有I/O操做,若是当前客户端有I/O操做,则可能把当前客户端扔给一个线程池去处理,若是没有

I/O操做则进行下一个轮询,当全部的客户端都轮询过了又接着从头开始轮询;这种方法是很是笨并且也很是浪费资源,由于大部分客户端是没有I/O操做,咱们也要去检查;

  一个Selector实例能够同时检查一组信道的I/O状态。选择器就是一个多路开关选择器,由于一个选择器可以管理多个信道上的I/O操做。它在内部能够同时管理多个I/O,当一个信道有I/O操做的时候,他会通知Selector,Selector就是记住这个信道有I/O操做,而且知道是何种I/O操做,是读呢?是写呢?仍是接受新的链接;因此若是使用Selector,它返回的结果只有两种结果,一种是0,即在你调用的时刻没有任何客户端须要I/O操做,另外一种结果是一组须要I/O操做的客户端,这时你就根本不须要再检查了,由于它返回给你的确定是你想要的。这样一种通知的方式比那种主动轮询的方式要高效得多!

  要使用选择器(Selector),须要建立一个Selector实例(使用静态工厂方法open())并将其注册(register)到想要监控的信道上(注意,这要经过channel的方法实现,而不是使用selector的方法)。最后,调用选择器的select()方法。该方法会阻塞等待,直到有一个或更多的信道准备好了I/O操做或等待超时。select()方法将返回可进行I/O操做的信道数量。如今,在一个单独的线程中,经过调用select()方法就能检查多个信道是否准备好进行I/O操做。若是通过一段时间后仍然没有信道准备好,select()方法就会返回0,并容许程序继续执行其余任务。

  下面将上面的TCP服务端代码改写成NIO的方式(案例5):

public class ServerConnect{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    public static void main(String[] args){
        selector();
    }
    public static void handleAccept(SelectionKey key) throws IOException{
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();//获取服务端通道
        SocketChannel sc = ssChannel.accept();//开启接收客户端链接
        sc.configureBlocking(false);//设置为非阻塞
        sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));//把通道注册到Selector上,而且接收的事件的读事件
    }
    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }
    }
    public static void handleWrite(SelectionKey key) throws IOException{
        ByteBuffer buf = (ByteBuffer)key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while(buf.hasRemaining()){
            sc.write(buf);
        }
        buf.compact();
    }
    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            selector = Selector.open();//获取Selector
            ssc= ServerSocketChannel.open();//开启服务端通道
            ssc.socket().bind(new InetSocketAddress(PORT));//绑定监听端口
            ssc.configureBlocking(false);//设置为非阻塞
            ssc.register(selector, SelectionKey.OP_ACCEPT);//注册到Selector上
            while(true){
                if(selector.select(TIMEOUT) == 0){//若是没有客户端链接
                    System.out.println("==");
                    continue;
                }
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();//
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}
View Code

   与Selector一块儿使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一块儿使用,由于FileChannel不能切换到非阻塞模式。而套接字通道均可以。一个通道能够被注册到多个选择器上,而对于同一个选择器则只能被注册一次。若是内核版本>=2.6则底层使用Linux操做系统的Epoll实现,不然使用poll实现。

  register()方法的第二个参数,是一个"interest集合",意思是在经过Selector监听Channel时对什么事件感兴趣。能够监听四种不一样类型的事件:

    1. Connect  --- 链接事件

    2. Accept   --- 接收事件

    3. Read    --- 读事件

    4. Write   --- 写事件

  通道触发了一个事件意思是该事件已经就绪。因此,某个channel成功链接到另外一个服务器称为“链接就绪”。一个server socket channel准备好接收新进入的链接称为“接收就绪”。一个有数据可读的通道能够说是“读就绪”。等待写数据的通道能够说是“写就绪”。

  这四种事件用SelectionKey的四个常量来表示:

    1. SelectionKey.OP_CONNECT

    2. SelectionKey.OP_ACCEPT

    3. SelectionKey.OP_READ

    4. SelectionKey.OP_WRITE

   SelectionKey

  当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性:

    (1)interest集合  

    (2)ready集合

    (3)Channel

    (4)Selector

    (5)附加的对象(可选)

  interest集合:就像向Selector注册通道一节中所描述的,interest集合是你所选择的感兴趣的事件集合。能够经过SelectionKey读写interest集合。

  ready 集合是通道已经准备就绪的操做的集合。在一次选择(Selection)以后,你会首先访问这个ready set。能够这样访问ready集合:

  int readySet = selectionKey.readyOps();

  能够用像检测interest集合那样的方法,来检测channel中什么事件或操做已经就绪。可是,也可使用如下四个方法,它们都会返回一个布尔类型:

  selectionKey.isAcceptable();
  selectionKey.isConnectable();
  selectionKey.isReadable();
  selectionKey.isWritable();

  从SelectionKey访问Channel和Selector很简单。以下:

  Channel  channel  = selectionKey.channel();
  Selector selector = selectionKey.selector();

  能够将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,能够附加与通道一块儿使用的Buffer,或是包含汇集数据的某个对象。使用方法以下:

  selectionKey.attach(theObject);
  Object attachedObj = selectionKey.attachment();

  还能够在用register()方法向Selector注册Channel的时候附加对象。如:

  SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

   经过Selector选择通道

  一旦向Selector注册了一或多个通道,就能够调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如链接、接受、读或写)已经准备就绪的那些通道。换句话说,若是你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。

  下面是select()方法:

    int select()

    int select(long timeout)

    int selectNow()

  select()阻塞到至少有一个通道在你注册的事件上就绪了。

  select(long timeout)和select()同样,最长会阻塞timeout毫秒(参数)。

  selectNow()不会阻塞,无论什么通道就绪都马上返回(译者注:此方法执行非阻塞的选择操做。若是自从前一次选择操做后,没有通道变成可选择的,则此方法直接返回零。)。

  select()方法返回的int值表示有多少通道已经就绪,即自上次调用select()方法后有多少通道变成就绪状态。若是调用select()方法,由于有一个通道变成就绪状态,返回了1,若再次调用select()方法,若是另外一个通道就绪了,它会再次返回1。若是对第一个就绪的channel没有作任何操做,如今就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

  一旦调用了select()方法,而且返回值代表有一个或更多个通道就绪了,而后能够经过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。以下所示: 

  Set selectedKeys = selector.selectedKeys();

  当向Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象表明了注册到该Selector的通道。

  注意每次迭代末尾的keyIterator.remove()调用。Selector不会本身从已选择键集中移除SelectionKey实例。必须在处理完通道时本身移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

  SelectionKey.channel()方法返回的通道须要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。

  一个完整的使用Selector和ServerSocketChannel的案例能够参考案例5的selector()方法。

六  浅谈零拷贝

1  引言

  传统的 Linux 操做系统的标准 I/O 接口是基于数据拷贝操做的,即 I/O 操做会致使数据在操做系统内核地址空间的缓冲区应用程序地址空间定义的缓冲区之间进行传输。这样作最大的好处是能够减小磁盘 I/O 的操做,由于若是所请求的数据已经存放在操做系统的高速缓冲存储器中,那么就不须要再进行实际的物理磁盘 I/O 操做。可是数据传输过程当中的数据拷贝操做却致使了极大的 CPU 开销,限制了操做系统有效进行数据传输操做的能力。

  零拷贝( zero-copy )技术能够有效地改善数据传输的性能,在内核驱动程序(好比网络堆栈或者磁盘存储驱动程序)处理 I/O 数据的时候,零拷贝技术能够在某种程度上减小甚至彻底避免没必要要 CPU 数据拷贝操做。

  零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另一块存储的技术。针对操做系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各类零拷贝技术极大地提高了特定应用程序的性能,而且使得这些应用程序能够更加有效地利用系统资源。这种性能的提高就是经过在数据拷贝进行的同时,容许 CPU 执行其余的任务来实现的。

  零拷贝技术能够减小数据拷贝和共享总线操做的次数,消除传输数据在存储器之间没必要要的中间拷贝次数,从而有效地提升数据传输效率。并且,零拷贝技术减小了用户应用程序地址空间和操做系统内核地址空间之间由于上下文切换而带来的开销。进行大量的数据拷贝操做实际上是一件简单的任务,从操做系统的角度来讲,若是 CPU 一直被占用着去执行这项简单的任务,那么这将会是很浪费资源的;若是有其余比较简单的系统部件能够代劳这件事情,从而使得 CPU 解脱出来能够作别的事情,那么系统资源的利用则会更加有效。

  综上所述,零拷贝技术的目标能够归纳以下:

  1. 避免数据拷贝

  (1)避免操做系统内核缓冲区之间进行数据拷贝操做。

  (2)避免操做系统内核和用户应用程序地址空间这二者之间进行数据拷贝操做。

  (3)用户应用程序能够避开操做系统直接访问硬件存储。

  (4)数据传输尽可能让 DMA 来作。

  2. 将多种操做结合在一块儿

  (1)避免没必要要的系统调用和上下文切换。

  (2)须要拷贝的数据能够先被缓存起来。

  (3)对数据进行处理尽可能让硬件来作。

2  零拷贝原理

1  IO读写方式

(1)中断  

  中断方式的流程图以下

  

  1. 用户进程发起数据读取请求

  2. 系统调度为该进程分配cpu

  3. cpu向io控制器(ide,scsi)发送io请求

  4. 用户进程等待io完成,让出cpu

  5. 系统调度cpu执行其余任务

  6. 数据写入至io控制器的缓冲寄存器

  7. 缓冲寄存器满了向cpu发出中断信号

  8. cpu读取数据至内存

  缺点:中断次数取决于缓冲寄存器的大小

(2) DMA : 直接内存存取

  DMA方式的流程图以下:

   

  1. 用户进程发起数据读取请求

  2. 系统调度为该进程分配cpu

  3. cpu向DMA发送io请求

  4. 用户进程等待io完成,让出cpu

  5. 系统调度cpu执行其余任务

  6. 数据写入至io控制器的缓冲寄存器

  7. DMA不断获取缓冲寄存器中的数据(须要cpu时钟)

  8. 传输至内存(须要cpu时钟)

  9. 所需的所有数据获取完毕后向cpu发出中断信号

  优势:减小cpu中断次数,不用cpu拷贝数据

2  数据拷贝

传统IO

  下面展现了传统方式读取数据后并经过网络发送所发生的数据拷贝:

  

  1. 一个read系统调用后,DMA执行了一次数据拷贝,从磁盘到内核空间

  2. read结束后,发生第二次数据拷贝,由cpu将数据从内核空间拷贝至用户空间

  3. send系统调用,cpu发生第三次数据拷贝,由cpu将数据从用户空间拷贝至内核空间(socket缓冲区)

  4. send系统调用结束后,DMA执行第四次数据拷贝,将数据从内核拷贝至协议引擎

  5. 另外,这四个过程当中,每一个过程都发生一次上下文切换

以上过程总结以下:

  1. 数据须要从磁盘拷贝到内核空间,再从内核空间拷到用户空间(JVM)。

  2. 程序可能进行数据修改等操做

  3. 再将数据拷贝到内核空间,内核空间再拷贝到网卡内存,经过网络发送出去(或拷贝到磁盘)

  磁盘到内核空间属于DMA拷贝(DMA即直接内存存取,原理是外部设备不经过CPU而直接与系统内存交换数据)。而内核空间到用户空间则须要CPU的参与进行拷贝,既然须要CPU参与,也就涉及到了内核态和用户态的相互切换。

NIO的零拷贝

  零拷贝的数据拷贝以下图:

  

  改进的地方:

    咱们已经将上下文切换次数从4次减小到了2次;

    将数据拷贝次数从4次减小到了3次(其中只有1次涉及了CPU,另外2次是DMA直接存取)。

  但这尚未达到咱们零拷贝的目标。若是底层NIC(网络接口卡)支持gather操做,咱们能进一步减小内核中的数据拷贝。在Linux 2.4以及更高版本的内核中,socket缓冲区描述符已被修改用来适应这个需求。这种方式不但减小屡次的上下文切换,同时消除了须要CPU参与的重复的数据拷贝。用户这边的使用方式不变,而内部已经有了质的改变:

   

  NIO的零拷贝由transferTo()方法实现。transferTo()方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由native方法transferTo0()来实现,它依赖底层操做系统的支持。在UNIX和Linux系统中,调用这个方法将会引发sendfile()系统调用。

使用场景通常是:

文件较大,读写较慢,追求速度
JVM内存不足,不能加载太大数据
内存带宽不够,即存在其余程序或线程存在大量的IO操做,致使带宽原本就小

  以上都创建在不须要进行数据文件操做的状况下,若是既须要这样的速度,也须要进行数据操做怎么办?
  那么使用NIO的直接内存!

NIO的直接内存

  首先,它的做用位置处于传统IO(BIO)与零拷贝之间,为什么这么说?

  传统IO,能够把磁盘的文件通过内核空间,读到JVM空间,而后进行各类操做,最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操做。

  零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。因为它没有读取文件数据到JVM这一环,所以程序没法操做该文件数据,尽管效率很高!

  而直接内存则介于二者之间,效率通常且可操做文件数据

  直接内存(mmap技术)将文件直接映射到内核空间的内存,返回一个操做地址(address),它解决了文件数据须要拷贝到JVM才能进行操做的窘境。而是直接在内核空间直接进行操做,省去了内核空间拷贝到用户空间这一步操做。

  NIO的直接内存是由MappedByteBuffer实现的。核心便是map()方法,该方法把文件映射到内存中,得到内存地址addr,而后经过这个addr构造MappedByteBuffer类,以暴露各类文件操做API。

  因为MappedByteBuffer申请的是堆外内存,所以不受Minor GC控制,只能在发生Full GC时才能被回收。而DirectByteBuffer改善了这一状况,它是MappedByteBuffer类的子类,同时它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。所以它既能够经过Full GC来回收内存,也能够调用clean()方法来进行回收。

  另外,直接内存的大小可经过jvm参数来设置:-XX:MaxDirectMemorySize

  NIO的MappedByteBuffer还有一个兄弟叫作HeapByteBuffer。顾名思义,它用来在堆中申请内存,本质是一个数组。因为它位于堆中,所以可受GC管控,易于回收。

参考:

一、Java NIO?看这一篇就够了!  https://mp.weixin.qq.com/s/c9tkrokcDQR375kiwCeV9w?

二、NIO相关基础篇  https://mp.weixin.qq.com/s/ln5YR__A0RPpvqTVbj3o-g

三、IO多路复用之select、poll、epoll详解  https://www.cnblogs.com/jeakeven/p/5435916.html

四、select、poll、epoll之间的区别总结[整理]  https://www.cnblogs.com/Anker/p/3265058.html  https://www.cnblogs.com/aspirant/p/9166944.html

五、NIO技术概览  http://www.ideabuffer.cn/2017/08/13/NIO%E6%8A%80%E6%9C%AF%E6%A6%82%E8%A7%88/

六、浅谈NIO与零拷贝 https://blog.csdn.net/localhost01/article/details/83422888

相关文章
相关标签/搜索