文档章节

Java IO: BIO, NIO, AIO(含代码实现)

tantexian
 tantexian
发布于 2016/05/11 19:49
字数 2055
阅读 638
收藏 13

BIO, NIO, AIO,本身的描述都是在Java语言的基础上的。 而描述IO,我们需要从三个层面:

  1. 编程语言
  2. 实现原理
  3. 底层基础

从编程语言层面

BIO, NIO, AIO以Java的角度理解:

  • BIO,同步阻塞式IO,简单理解:一个连接一个线程
  • NIO,同步非阻塞IO,简单理解:一个请求一个线程
  • AIO,异步非阻塞IO,简单理解:一个有效请求一个线程

BIO

在JDK1.4之前,用Java编写网络请求,都是建立一个ServerSocket,然后,客户端建立Socket时就会询问是否有线程可以处理,如果没有,要么等待,要么被拒绝。即:一个连接,要求Server对应一个处理线程。

public class PlainEchoServer {
  public void serve(int port) throws IOException {
    final ServerSocket socket = new ServerSocket(port); //Bind server to port
    try {
      while (true) {
        //Block until new client connection is accepted
        final Socket clientSocket = socket.accept();
        System.out.println("Accepted connection from " + clientSocket);
        //Create new thread to handle client connection
        new Thread(new Runnable() {
          @Override
          public void run() {
            try {
              BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
              PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
              //Read data from client and write it back
              while (true) {
                writer.println(reader.readLine());
                writer.flush();
              }
            } catch (IOException e) {
              e.printStackTrace();
              try {
                clientSocket.close();
              } catch (IOException ex) {
                // ignore on close
              }
            }
          }
        }).start();
        //Start thread
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

NIO

在Java里的由来,在JDK1.4及以后版本中提供了一套API来专门操作非阻塞I/O,我们可以在java.nio包及其子包中找到相关的类和接口。由于这套API是JDK新提供的I/O API,因此,也叫New I/O,这就是包名nio的由来。这套API由三个主要的部分组成:缓冲区(Buffers)、通道(Channels)和非阻塞I/O的核心类组成。在理解NIO的时候,需要区分,说的是New I/O还是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。这里讲的是后面一种。

NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。 NIO基于Selector,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

public class PlainNioEchoServer {
  public void serve(int port) throws IOException {
    System.out.println("Listening for connections on port " + port);
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    ServerSocket ss = serverChannel.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    //Bind server to port
    ss.bind(address);
    serverChannel.configureBlocking(false);
    Selector selector = Selector.open();
    //Register the channel with the selector to be interested in new Client connections that get accepted
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
      try {
        //Block until something is selected
        selector.select();
      } catch (IOException ex) {
        ex.printStackTrace();
        //handle in a proper way
        break;
      }
      //Get all SelectedKey instances
      Set<SelectionKey> readyKeys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = readyKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = (SelectionKey) iterator.next();
        //Remove the SelectedKey from the iterator
        iterator.remove();
        try {
          if (key.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            //Accept the client connection
            SocketChannel client = server.accept();
            System.out.println("Accepted connection from " + client);
            client.configureBlocking(false);
            //Register connection to selector and set ByteBuffer
            client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, ByteBuffer.allocate(100));
          }
          //Check for SelectedKey for read
          if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer output = (ByteBuffer) key.attachment();
            //Read data to ByteBuffer
            client.read(output);
          }
          //Check for SelectedKey for write
          if (key.isWritable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer output = (ByteBuffer) key.attachment();
            output.flip();
            //Write data from ByteBuffer to channel
            client.write(output);
            output.compact();
          }
        } catch (IOException ex) {
          key.cancel();
          try {
            key.channel().close();
          } catch (IOException cex) {
          }
        }
      }
    }
  }
}

AIO

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。

即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。

在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。

public class PlainNio2EchoServer {
  public void serve(int port) throws IOException {
    System.out.println("Listening for connections on port " + port);
    final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
    InetSocketAddress address = new InetSocketAddress(port);
    // Bind Server to port
    serverChannel.bind(address);
    final CountDownLatch latch = new CountDownLatch(1);
    // Start to accept new Client connections. Once one is accepted the CompletionHandler will get called.
    serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
      @Override
      public void completed(final AsynchronousSocketChannel channel, Object attachment) {
        // Again accept new Client connections
        serverChannel.accept(null, this);
        ByteBuffer buffer = ByteBuffer.allocate(100);
        // Trigger a read operation on the Channel, the given CompletionHandler will be notified once something was read
        channel.read(buffer, buffer, new EchoCompletionHandler(channel));
      }

      @Override
      public void failed(Throwable throwable, Object attachment) {
        try {
          // Close the socket on error
          serverChannel.close();
        } catch (IOException e) {
          // ingnore on close
        } finally {
          latch.countDown();
        }
      }
    });
    try {
      latch.await();
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  private final class EchoCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private final AsynchronousSocketChannel channel;

    EchoCompletionHandler(AsynchronousSocketChannel channel) {
      this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
      buffer.flip();
      // Trigger a write operation on the Channel, the given CompletionHandler will be notified once something was written
      channel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
        @Override
        public void completed(Integer result, ByteBuffer buffer) {
          if (buffer.hasRemaining()) {
            // Trigger again a write operation if something is left in the ByteBuffer
            channel.write(buffer, buffer, this);
          } else {
            buffer.compact();
            // Trigger a read operation on the Channel, the given CompletionHandler will be notified once something was read
            channel.read(buffer, buffer, EchoCompletionHandler.this);
          }
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
          try {
            channel.close();
          } catch (IOException e) {
            // ingnore on close
          }
        }
      });
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
      try {
        channel.close();
      } catch (IOException e) {
        // ingnore on close
      }
    }
  }
}

实现原理

说道实现原理,还要从操作系统的IO模型上了解 按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。 如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

收到操作系统的IO模型,又不得不提select/poll/epoll/iocp。 可以理解的说明是:在Linux 2.6以后,java NIO的实现,是通过epoll来实现的,这点可以通过jdk的源代码发现。而AIO,在windows上是通过IOCP实现的,在linux上还是通过epoll来实现的。 这里强调一点:AIO,这是I/O处理模式,而epoll等都是实现AIO的一种编程模型;换句话说,AIO是一种接口标准,各家操作系统可以实现也可以不实现。在不同操作系统上在高并发情况下最好都采用操作系统推荐的方式。Linux上还没有真正实现网络方式的AIO。

底层基础

在windows上,AIO的实现是通过IOCP来完成的,看JDK的源代码,可以发现

WindowsAsynchronousSocketChannelImpl

看实现接口:

implements Iocp.OverlappedChannel

再看实现方法:里面的read0/write0方法是native方法,调用的jvm底层实现。

在linux上,AIO的实现是通过epoll来完成的,看JDK源码,可以发现,实现源码是:

UnixAsynchronousSocketChannelImpl

看实现接口:

implements Port.PollableChannel

这是与windows最大的区别,poll的实现,在linux2.6后,默认使用epoll。

本文转载自:

tantexian
粉丝 225
博文 527
码字总数 746616
作品 0
成都
架构师
私信 提问
漫话:如何给女朋友解释什么是BIO、NIO和AIO?

周末午后,在家里面进行电话面试,我问了面试者几个关于IO的问题,其中包括什么是BIO、NIO和AIO?三者有什么区别?具体如何使用等问题,但是面试者回答的并不是很满意。于是我在面试评价中写...

漫话编程
07/01
343
0
Tomcat的BIO、NIO、APR模式对比与性能测试结果

11.11活动当天,服务器负载过大,导致部分页面出现了不可访问的状态、那后来就要求调优了,下面是tomcat bio、nio、apr模式以及后来自己测试的一些性能结果。 原理方面的资料都是从网上找的,...

DemonsI
02/15
88
0
Java对BIO、NIO、AIO的支持

IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。 Java BIO 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行...

edwardGe
2018/08/04
100
1
Java核心(五)深入理解BIO、NIO、AIO

导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别;BIO、NIO、AIO 的区别;理解和实现 NIO 操作 Socket 时的多路复用;同时掌握 IO 最底层最核心的操作技巧。 BIO、NIO、AIO 的区别是什...

王磊的博客
2018/12/03
1K
0
浅谈Tomcat服务器优化方法

对于JavaWeb开发人员而言,Tomcat已成为默认的web服务器,但是在生产环境下使用Tomcat部署应用,我们如果采用Tomcat默认的配置,尤其是内存和线程的配置,其配置都很低,容易成为性能瓶颈,所...

动力节点
01/03
0
0

没有更多内容

加载失败,请刷新页面

加载更多

最简单的获取相机拍照的图片

  import android.content.Intent;import android.graphics.Bitmap;import android.os.Bundle;import android.os.Environment;import android.provider.MediaStore;import andr......

MrLins
47分钟前
4
0
说好不哭!数据可视化深度干货,前端开发下一个涨薪点在这里~

随着互联网在各行各业的影响不断深入,数据规模越来越大,各企业也越来越重视数据的价值。作为一家专业的数据智能公司,个推从消息推送服务起家,经过多年的持续耕耘,积累沉淀了海量数据,在...

个推
49分钟前
7
0
第三方支付-返回与回调注意事项

不管是支付宝,微信,还是其它第三方支付,第四方支付,支付机构服务商只要涉及到钱的交易都要进行如下校验,全部成功了才视为成功订单 1.http请求是否成功 2.校验商户号 3.校验订单号及状态...

Shingfi
52分钟前
4
0
简述Java内存分配和回收策略以及Minor GC 和 Major GC(Full GC)

内存分配: 1. 栈区:栈可分为Java虚拟机和本地方法栈 2. 堆区:堆被所有线程共享,在虚拟机启动时创建,是唯一的目的是存放对象实例,是gc的主要区域。通常可分为两个区块年轻代和年老代。更...

DustinChan
58分钟前
6
0
Excel插入批注:可在批注插入文字、形状、图片

1.批注一直显示:审阅选项卡-------->勾选显示批注选项: 2.插入批注快捷键:Shift+F2 组合键 3.在批注中插入图片:鼠标右键点击批注框的小圆点【重点不可以在批注文本框内点击】----->调出批...

东方墨天
今天
6
1

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部