Java NIO浅析

准备知识

同步、异步、阻塞、非阻塞

同步和异步说的是服务端消息的通知机制,阻塞和非阻塞说的是客户端线程的状态。
已客户端一次网络请求为例作简单说明:html

  • 同步
    同步是指一次请求没有获得结果以前就不返回。java

  • 异步
    请求不会马上获得最终结果,服务器处理完成再异步通知客户端。react

  • 阻塞
    请求结果返回以前,当前线程被挂起。在此期间不能作任何其余的事情。linux

  • 非阻塞
    请求当即返回,后续由客户端时不时的询问服务器结果或者服务器异步回调。编程

同步IO、异步IO、阻塞IO、非阻塞IO

一般来讲,IO操做包括:对硬盘的读写、对socket的读写以及外设的读写。
已一个IO读取过程为例作简要说明(如图):服务器

  1. DMA把数据读取到内核空间的缓冲区(读就绪)网络

  2. 内核将数据拷贝到用户空间。多线程

io%E5%8E%9F%E7%90%86.png

内核空间是用户代码没法控制的,因此用户空间在读取以前,首先会判断是否已经读就绪。架构

  • 同步IO
    当用户发出IO请求操做以后,内核会去查看要读取的数据是否就绪,若是数据没有就绪,就一直等待。须要经过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户空间。并发

  • 异步IO
    只有IO请求操做的发出是由用户线程来进行的,IO操做的两个阶段都是由内核自动完成,而后发送通知告知用户线程IO操做已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。

  • 阻塞IO
    当用户线程发起一个IO请求操做(以读请求操做为例),内核查看要读取的数据还没就绪,当前线程被挂起,阻塞等待结果返回。

  • 非阻塞IO
    若是数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当前线程在拿到这次请求结果的过程当中,能够作其它事情。

JAVA中的BIO、NIO、AIO

  • BIO
    同步阻塞,传统io方式。
    适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。

  • NIO
    同步非阻塞,jdk4开始支持。
    适用于链接数目多且链接比较短(轻操做)的架构,好比聊天服务器。

  • AIO
    异步非阻塞,jdk7开始支持。
    适用于链接数目多且链接比较长(重操做)的架构。

形象的理解NIO和AIO:
若是把内核比做快递,NIO就是你要本身时不时到官网查下快递是否已经到了你所在城市,而后本身去取快递;AIO就是快递员送货上门了。

Linux下五种IO模型

  • 阻塞I/O(blocking I/O)

  • 非阻塞I/O (nonblocking I/O)

  • I/O复用(select 和poll) (I/O multiplexing)

  • 信号驱动I/O (signal driven I/O (SIGIO))

  • 异步I/O (asynchronous I/O (the POSIX aio_functions))

IO复用模型(IO多路复用)

简言之,就是经过单个线程(进程)来管理多IO流。如图:

io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8.png

IO多路复用避免阻塞在IO上,本来为多进程或多线程来接收多个链接的消息变为单进程或单线程保存多个socket的状态后轮询处理。只有当某个socket读写就绪后,才真正调用实际的IO读写操做。这样能够避免线程切换带来的开销。

实现IO多路复用须要函数来支持,就是你说的linux下的select、poll、epoll以及win下 iocp和BSD的kqueue。这几个函数也会使进程阻塞,可是和阻塞I/O所不一样的是,它能够同时阻塞多个I/O操做。并且能够同时对多个读操做,多个写操做的I/O准备状态进行检测。

IO多路复用为什么比非阻塞IO模型的效率高是由于在非阻塞IO中,不断地询问socket状态是经过用户线程去进行的,而在IO多路复用中,轮询每一个socket状态是内核在进行的,这个效率要比用户线程要高的多。

io%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B.png

五种IO模型以及select、poll、epoll的详细介绍推荐你们看这篇文章
socket阻塞与非阻塞,同步与异步、I/O模型

理解Reactor和Proactor模式

在Reactor模式中,会先对每一个client注册感兴趣的事件,而后有一个线程专门去轮询每一个client是否有事件发生,当有事件发生时(读写就绪),便顺序处理每一个事件,当全部事件处理完以后,便再转去继续轮询,如图所示:

reactor%E6%A8%A1%E5%BC%8F.png

从这里能够看出,多路复用IO就是采用Reactor模式。注意,上面的图中展现的是顺序处理每一个事件,固然为了提升事件处理速度,能够经过多线程或者线程池的方式来处理事件。
在Proactor模式中,当检测到有事件发生时,会新起一个异步操做,而后交由内核线程去处理,当内核线程完成IO操做以后,发送一个通知告知操做已完成,能够得知,异步IO模型采用的就是Proactor模式。

这部分摘选自:Java NIO:浅析I/O模型

Java NIO介绍

Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操做的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操做,数据老是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

Non-blocking IO(非阻塞IO)
Java NIO可让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程仍是能够进行其余事情。当数据被写入到缓冲区时,线程能够继续处理它。从缓冲区写入通道也相似。

Selectors(选择器)
选择器用于监听多个通道的事件(好比:链接打开,数据到达)。所以,单个的线程能够监听多个数据通道。

NIO与IO区别

  IO                 NIO
面向流         面向缓冲
阻塞IO          非阻塞IO
  无                选择器

Channel

Java NIO的通道相似流,但又有些不一样:

  • 既能够从通道中读取数据,又能够写数据到通道。但流的读写一般是单向的。

  • 通道能够异步地读写。

  • 通道中的数据老是要先读到一个Buffer,或者老是要从一个Buffer中写入。

Channel的实现

  • FileChannel (从文件中读写数据)

  • DatagramChannel (经过UDP读写网络中的数据)

  • SocketChannel (经过TCP读写网络中的数据)

  • ServerSocketChannel (能够监听新进来的TCP链接,像Web服务器那样)

Buffer

Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。

Buffer的基本用法

使用Buffer读写数据通常遵循如下四个步骤:

  1. 分配指定大小的buffer空间

  2. 写入数据到Buffer

  3. 调用flip()方法

  4. 从Buffer中读取数据

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

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,须要经过flip()方法将Buffer从写模式切换到读模式。在读模式下,能够读取以前写入到buffer的全部数据。

一旦读完了全部的数据,就须要清空缓冲区,让它能够再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
注:Buffer中的数据并未清除,只是这些标记告诉咱们能够从哪里开始往Buffer里写数据。

Buffer的类型

  • ByteBuffer

  • MappedByteBuffer

  • CharBuffer

  • DoubleBuffer

  • FloatBuffer

  • IntBuffer

  • LongBuffer

  • ShortBuffer

Selector

Selector(选择器)是Java NIO中可以检测一到多个通道,并可以知晓通道是否为诸如读写事件作好准备的组件。这样,一个单独的线程能够管理多个channel,从而管理多个网络链接。

nio.png

为何使用Selector?

仅用单个线程来处理多个Channels的好处是,只须要更少的线程来处理通道。事实上,能够只用一个线程处理全部的通道。对于操做系统来讲,线程之间上下文切换的开销很大,并且每一个线程都要占用系统的一些资源(如内存)。所以,使用的线程越少越好。

可是,须要记住,现代的操做系统和CPU在多任务方面表现的愈来愈好,因此多线程的开销随着时间的推移,变得愈来愈小了。实际上,若是一个CPU有多个内核,不使用多任务多是在浪费CPU能力。无论怎么说,关于那种设计的讨论应该放在另外一篇不一样的文章中。在这里,只要知道使用Selector可以处理多个通道就足够了。

NIO如何实现非阻塞?

服务器上全部Channel须要向Selector注册,而Selector则负责监视这些Socket的IO状态(观察者),当其中任意一个或者多个Channel具备可用的IO操做时,该Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具备可用的IO操做,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合(一个SelectionKey对应一个就绪的通道)。正是经过Selector,使得服务器端只须要不断地调用Selector实例的select()方法便可知道当前全部Channel是否有须要处理的IO操做。
注:java NIO就是多路复用IO,jdk7以后底层是epoll模型。

一个简单的demo

/**
 * NioServer
 * Date: 6/27/2016
 * Time: 8:06 PM
 *
 * @author xiaodong.fan
 */
public class NioServer {

  public static void main(String[] args) throws Exception {
    // 一、初始化一个ServerSocketChannel
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9999);
    serverSocketChannel.configureBlocking(false);// 设置为非阻塞模式,后续的accept()方法会马上返回
    serverSocketChannel.socket().bind(inetSocketAddress, 1024);// 监听本地9999端口的请求,第二个参数限制能够创建的最大链接数
    Selector selector = Selector.open();
    /**
     * 将通道注册到一个选择器上(非阻塞模式与选择器搭配会工做的更好)
     * 注意register()方法的第二个参数。这是一个“interest集合”,意思是在经过Selector监听Channel时对什么事件感兴趣。
     * 能够监听四种不一样类型的事件:OP_CONNECT,OP_ACCEPT,OP_READ,OP_WRITE
     * 若是你对不止一种事件感兴趣,那么能够用“位或”操做符将常量链接起来:SelectionKey.OP_READ | SelectionKey.OP_WRITE
     */
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    // 二、监听链接请求并处理
    while (true) {
      int connects = selector.select(2000);// 每次最多阻塞2秒

      if (connects == 0) {
        System.out.println("没有请求...");
        continue;
      } else {
        System.out.println("请求来了...");
      }

      // 获取监听到有链接请求的channel对应的selectionKey
      Set<SelectionKey> selectedKeys = selector.selectedKeys();
      // 遍历selectionKey来访问就绪的通道
      Iterator<SelectionKey> selectedKeyIterator = selectedKeys.iterator();
      while (selectedKeyIterator.hasNext()) {
        SelectionKey selectionKey = selectedKeyIterator.next();
        if (selectionKey.isValid()) {

          if (selectionKey.isAcceptable()) {// 接收就绪
            ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
            // 返回一个包含新进来的链接SocketChannel,由于前面设置的非阻塞模式,这里会当即返回。
            SocketChannel socketChannel = channel.accept();
            if (socketChannel == null) {
              return;
            }
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("链接创建完成");
            doWrite(socketChannel, "connection is established");// 链接创建完成,给客户端发消息

          } else if (selectionKey.isReadable()) {// 读就绪

            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer readBuffer = ByteBuffer.allocate(10);
            while ((socketChannel.read(readBuffer)) > 0) {// // 读取客户端发送来的消息
              readBuffer.flip();
              byte[] bytes = new byte[readBuffer.remaining()];
              readBuffer.get(bytes);
              String body = new String(bytes, "utf-8");
              doWrite(socketChannel, body);// 将客户端发送的内容原封不动的发回去
              readBuffer.clear();
            }
            socketChannel.close();//读取数据完毕后关闭链接,若是不关闭一直处于链接状态。

          }
        }

        selectedKeyIterator.remove(); // 注意每次必须手动remove(),下次该通道变成就绪时,Selector会再次将其放入已选择键集中
      }
    }
  }

  private static void doWrite(SocketChannel socketChannel, String response) throws IOException {
    if (StringUtils.isNotBlank(response)) {
      byte[] bytes = response.getBytes();
      ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
      writeBuffer.put(bytes);
      writeBuffer.flip();
      // 发送消息到客户端
      socketChannel.write(writeBuffer);
      writeBuffer.clear();
    }

  }

}

参考文章

Java NIO:浅析I/O模型
socket阻塞与非阻塞,同步与异步、I/O模型
Java BIO、NIO、AIO 学习
Java NIO:NIO概述
Java NIO 系列教程
Java网络编程——使用NIO实现非阻塞Socket通讯

相关文章
相关标签/搜索