Java BIO NIO AIO 模型介绍和使用样例

     在计算机的世界中,IO操作是不可避免的一个话题, IO操作涉及到的阻塞非阻塞同步异步这些概念常常让我感到混乱,为此,专门抽出时间对这些概念做了一下简单的研究,记录如下。希望可以帮助还在这些概念中挣扎的同学。

1.阻塞,非阻塞,同步和异步

     IO操作实际上可以分为两步:发起IO请求实际的IO操作。如果在第一步发起IO请求时发生阻塞,那么这个IO操作就可以说阻塞的,否则是非阻塞的。如果在第二步实际IO操作时发生阻塞,那么这个IO操作就是同步的,否则就是异步的。
     换种说法,阻塞和非阻塞是指在用户程序查询IO就绪状态时(比如查询IO是否有数据),用户程序对IO不同的就绪状态所表现出来的不同形式。以读取数据为例,当IO没有数据可供读取时,如果是阻塞IO,程序会一直阻塞直至IO有数据,如果是非阻塞IO,程序会直接返回错误码说当前没有数据,请稍后再来查询。同步和异步是由在进行实际的IO操作时,用户程序是否等待数据操作完成来决定。还是以读取数据为例,如果是同步IO,用户程序会等待读取数据完成,在此期间这个线程什么也不做,如果是异步IO,用户程序可以去作别的事情,内核在完成数据读取后,会以回调的方式通知用户程序。

2.同步阻塞IO,同步非阻塞IO,IO多路复用和异步IO模型

     在Unix中,共有五种IO模型,阻塞IO,非阻塞IO,IO多路复用和信号驱动IO以及异步IO,Java中实现了除信号驱动IO外的其他四种。

(1) 同步阻塞IO

     同步阻塞IO模型相对比较简单,操作流程如下,从用户发起read请求,到数据从IO读取至用户buffer,整个过程中用户线程始终处于阻塞状态,无法做别的事情。
Java BIO NIO AIO 模型介绍和使用样例_第1张图片
(2) 同步非阻塞IO
     同步非阻塞IO模型的操作流程如下,用户发起read请求,这时候如果IO数据没有准备好,那么read函数立即返回,用户线程需要不停的去轮询是否有数据到来,当有数据到来时,用户程序再次发起读取数据操作,这个时候用户线程会发生阻塞,直至数据从IO拷贝至buffer操作完成,用户线程继续处理读取到的数据,这也是同步的意义所在。
Java BIO NIO AIO 模型介绍和使用样例_第2张图片
(3) IO多路复用
     IO多路复用本质上还是属于同步非阻塞IO模型,不同点在于它通过selector实现在一个线程中监听多路通道数据。IO多路复用的操作流程如下,用户线程首先需要把需要监听的socket注册至selector,然后selector负责去轮询各个socket通道是否有数据到来,一旦有数据到来,select返回,最后用户程序读取IO数据,这个过程仍然是用户线程阻塞直至数据读取完成。需要注意的是,这里的socket通道必须配置为非阻塞,这样select才能去依次轮询所有注册在selector中的socket通道。 
Java BIO NIO AIO 模型介绍和使用样例_第3张图片
      IO多路复用采用Reactor设计模式,操作流程如下。用户线程需要首先在Reactor中注册一个事件处理器,然后Reactor(相当于上文提到的selector)负责轮询各个通道是否有新的数据到来,当有新的数据到来时,Reactor通过先前注册的事件处理器通知用户线程有数据可读,此时用户线程向内核发起读取IO数据的请求,用户线程阻塞直至数据读取完成。 
Java BIO NIO AIO 模型介绍和使用样例_第4张图片

(4) 异步IO
     最后是异步IO。异步IO采用Proactor设计模式,操作流程如下,跟Reactor模式一样,用户线程首先也需要向Proactor注册一个事件处理器,然后告诉内核要读取IO数据,这个时候用户线程就去做别的事情了,剩下监听IO数据以及IO数据的读取都由内核来完成,完成之后内核通过用户线程注册的事件处理器通知用户线程,“数据已经读取完成,你可以对这些数据做你自己的处理了”,最后用户线程拿到数据开始做自己的处理。
Java BIO NIO AIO 模型介绍和使用样例_第5张图片
          上面分析了阻塞,非阻塞,同步和异步的区别以及各种IO模型的操作流程,下面我们用实际的例子来说明在Java中各个IO模型的使用。

3. Java中BIO,NIO和AIO使用样例
     下面我们主要介绍Java中BIO,NIO和AIO三种IO模型如何使用。需要注意的是,本文中所提到的所有样例都是在一个server对应一个client的情况下工作的,如果你想扩展为一个server服务多个client,那么代码需要做相应的修改才能使用。另外,本文只会讲解server端如何处理,客户端的操作流程可以仿照服务端进行编程,大同小异。

(1) BIO(Blocking I/O)
     在Java中,BIO是基于流的,这个流包括字节流或者字符流,但是细心的同学可能会发现基本上所有的流都是单向的,要么只读,要么只写。在实际上编程时,在对IO操作之前,要先获取输入流或输出流,然后对输入流读或对输出流写即完成实际的IO读写操作。 首先需要新建一个ServerSocket对象监听特定端口,然后当有客户端的连接请求到来时,在服务器端获取一个Socket对象,用来进行实际的通信。
ServerSocket serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accept();
获取到Socket对象后,通过这个Socket对象拿到输入流和输出流就可以进行相应的读写操作了。
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
由于BIO的编程的模型比较简单,这里就写这么多。

(2) NIO(New I/O, or Nonblocking I/O)
     BIO的编程模型简单易行,但是缺点也很明显。由于采用的是同步阻塞IO的模式,所以server端要为每一个连接创建一个线程,一方面,线程之间在进行上下文切换的时候会造成比较大的开销,另一方面,当连接数过多时,可能会造成服务器崩溃的现象产生。
     为了解决这个问题,在JDK 1.4的时候,引入了NIO(New IO)的概念。NIO主要由三个部分组成,即Channel,Buffer和Selector。Channel可以跟BIO中的Stream类比,不同的是Channel是可读可写的。当和Channel进行交互的时候需要Buffer的支持,数据可以从Buffer写到Channel中,也可以从Channel中读到Buffer中,他们的关系如下图。
       channel&buffer 以SocketChannel为例,Channel和Buffer交互的例子如下。ByteBuffer是Buffer的一种实现,在使用ByteBuffer之前,需要为其分配空间,然后调用Channel的read方法将数据写入Buffer中,在完成后,在使用Buffer中的数据之前需要调用Buffer的flip方法。Buffer中有个position常量,记录当前操作数据的位置,当向Buffer中写数据时,position会记录当前写的位置,当写操作完成后,flip会把position至为0,这样读取Buffer中的数据时,就会从0开始了。另外需要注意的是处理完Buffer中的数据后需要调用clear方法将Buffer清空。向Channel中写数据的操作比较简单,这里不再赘述。
// Read data from channel to buffer
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();  
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);  
while (socketChannel.read(byteBuffer) > 0) {  
    byteBuffer.flip();
    while(byteBuffer.hasRemaining()){
        System.out.print((char) byteBuffer.get());
    }
    byteBuffer.clear();}

// Write data to channel from buffer
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
       NIO中另一个重要的组件是Selector,Selector可以用来检查一个或多个Channel是否有新的数据到来,这种方式可以实现在一个线程中管理多个Channel的目的,示意图如下。

Java BIO NIO AIO 模型介绍和使用样例_第6张图片

 selector 在使用selector之前,一定要注意把对应的Channel配置为非阻塞。否则在注册的时候会抛异常。
serverSocketChannel.configureBlocking(false);
然后调用select函数,select是个阻塞函数,它会阻塞直到某一个操作被激活。这个时候可以获取一系列的SelectionKey,通过这个SelectionKey可以判断其对应的Channel可进行的操作(可读,可写或者可接受连接),然后进行相应的操作即可。这里还要注意一个问题就是在判断完可执行的操作后,需要将这个SelectionKey从集合中移除。
selector.select();
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey selectionKey = iterator.next();
    if (!selectionKey.isValid())
        continue;
    if (selectionKey.isAcceptable()) {
        // ready for accepting
    } else if (selectionKey.isReadable()) {
        // ready for reading
    } else if (selectionKey.isWritable()) {
        // ready for writing
    }
    iterator.remove();
}
NIO这里最后一个问题是,什么时候Channel可写,这个问题困扰了我很久,经过从网上查资料最后得出的结论是,只要这个Channel处于空闲状态,都是可写的。这个我也从实际的程序中论证了。

(3) AIO(Asynchronous I/O)
     在JDK 1.7时,Java引入了AIO的概念,AIO还是基于Channel和Buffer的,不同的是它是异步的。用户线程把实际的IO操作以及数据拷贝全部委托给内核来做,用户只要传递给内核一个用于存储数据的地址空间即可。内核处理的结果通过两种方式返回给用户线程。一是通过Future对象,另外一种是通过回调函数的方式,回调函数需要实现CompletionHandler接口。这里只给出通过回调方式处理数据的样例,其中关键的步骤已经在程序中添加了注释。
// 创建AsynchronousServerSocketChannel监听特定端口,并设置回调AcceptCompletionHandler
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));  
serverSocketChannel.accept(serverSocketChannel, new AcceptCompletionHandler());

// 监听回调,当用连接时会触发该回调private static class AcceptCompletionHandler implements CompletionHandler {  
    @Override
    public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 注册read请求以及回调ReadCompletionHandler
        result.read(byteBuffer, result, new ReadCompletionHandler(byteBuffer, "client"));
        // 递归监听
        attachment.accept(attachment, this);
    }
    @Override
    public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
        // 递归监听
        attachment.accept(attachment, this);
    }}

// 读取数据回调,当有数据可读时触发该回调public class ReadCompletionHandler  implements CompletionHandler {  
    private ByteBuffer byteBuffer;
    private String remoteName;
    public ReadCompletionHandler(ByteBuffer byteBuffer, String remoteName) {
        this.byteBuffer = byteBuffer;
        this.remoteName = remoteName;
    }
    @Override
    public void completed(Integer result, AsynchronousSocketChannel attachment) {
        if (result <= 0)
            return;

        byteBuffer.flip();
        System.out.println("[" + this.remoteName + "] " + new String(byteBuffer.array()));

        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }

    @Override
    public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }}
     上面给出了BIO,NIO以及AIO在Java中的使用的部分程序,并且分析了其中关键步骤的使用及其需要注意的事项。

你可能感兴趣的:(#,Java相关)