Java NIO学习总结

文章目录

  • 概述
    • 阻塞与非阻塞的简单类比
    • 为什么要使用NIO?
    • NIO性能一定比BIO好吗?
  • 原理
      • 相关概念
        • Channel
        • Buffer
          • position与limit
          • buffer.flip()
        • Selector
          • 事件驱动模型
  • API使用
    • 一、使用FileChannel读取文件
    • 二、使用NIO进行网络编程
      • 服务端代码:
      • 客户端代码:

先祭上大神的文章:http://tutorials.jenkov.com/java-nio/overview.html

概述

Java NIO,全称Java non-blocking IO(也有说是Java New IO,个人认为前者各贴切),是Java在1.4版本时引入的一套新的IO和网络编程的API,可以作为Java标准IO的替代选择。

Java BIO(Java blocking IO,即Java standard IO)是同步阻塞IO,Java NIO的引入是为了实现同步非阻塞的IO,从而提供多路(non-blocking) 非阻塞式的高伸缩性网络I/O 。

阻塞与非阻塞的简单类比

阻塞IO和非阻塞IO的主要差异,这里用生活中的例子类比一下:

阻塞IO

老板:小明,去取个包裹,取不到你就不要回来了。

于是小明一直在快递站等到包裹送来并取件才回到公司。

非阻塞IO

老板:小明,去取个包裹,取不到的话你就先回来写生产报告。

小明去了快递站,包裹还没到,于是小明先回到公司写报告了。

通常非阻塞IO是需要循环调用的,大概就是下面这个场景

老板:小明,去取个包裹,取不到的话你就先回来写生产报告,过一会儿再去取快递。

小明去了快递站,包裹还没到,于是小明先回到公司写报告了。

写了会儿报告小明又去快递站取快递了,还是没有到,于是又回公司写报告,如此往复直到取到快递。

为什么要使用NIO?

从上面的描述我们直到,非阻塞最主要就是为了让当前线程,在进行IO操作时无论没有获取到资源就立即返回,从而避免阻塞当前线程。其实要达到避免阻塞当前线程的目的,使用BIO也可以实现,那就是配合多线程,每当需要进行一个IO操作时,就新建一个线程,或者使用线程池,就可以避免阻塞当前线程。

那为什么我们还要使用NIO呢,这是因为在频繁大量的IO操作时,每次都新建线程,对系统的开销非常大,即使使用线程池,依然需要维护大量的线程,系统开销依然很大,而如果使用NIO,则一个线程就可以实现非阻塞,系统开销小。

NIO性能一定比BIO好吗?

其实并不一定,NIO性能比BIO性能好主要体现在两者均为单线程时,因为NIO没有获取到资源时可以立即返回,而BIO则会阻塞,性能差异主要体现在这里,所以这种情况NIO性能确实比BIO好。

但如果是我们上面说的多线程配合BIO使用,多个线程同时进行IO操作,可以认为是同时进行的,而且当前线程也不会阻塞(一些地方将这种方式成为伪异步),而NIO虽然不会阻塞,但是当前线程中始终只有一个IO操作在进行,所以多线程BIO的性能理论上优于单线程NIO,但系统开销更大。

当然,NIO也可以和多线程配合使用,这个时候无论是整体性能,还是系统开销,都优于多线程BIO。

所以可以肯定的是,在单个线程中,NIO的处理性能远优于BIO。

原理

相关概念

Channel

在BIO中我们使用流(Stream)来进行资源的输入输出,而在NIO中,是使用信道(Channel,很多地方译为管道,但通常我们理解中的管道是单向的,所以觉得信道可能更贴切),stream和channel很相似,都是用来输入输出数据资源的,但它们有一些差异:

  1. 流是单向的,输入需要使用输入流,输出需要使用输出流,而信道是双向的,同一个信道既可以读,也可以写;
  2. 信道可以异步读写;
  3. 信道总是针对缓存(Buffer)操作。

Buffer

我们在使用传统的IO时,在读写数据时,通常都是直接实现的缓存,在NIO中,Java直接引入了缓存概念,信道在进行读写时,都是针对缓存进行操作。

NIO提供了以下类型的缓存:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

可以看到,这些缓存类型,涵盖了Java所有的基础数据类型。

position与limit

Buffer有两个很重要的概念,position和limit。

position:游标当前所在的位置。

limit:缓存的边界。

在read和write中,position和limit有一些差异。read时,position是当前写入的位置,limit是Buffer的末尾位置;write时,position是当前读取的位置,limit是有效数据的末尾位置。

下面是示意图:

Java NIO学习总结_第1张图片

buffer.flip()

上面了解了position和limit,flip()就是用来改变buffer的position和limit状态的。

调用flip函数后,position会重置为0,即最开始位置,而limit如果此时在buffer末尾,那么limit会变更为有效内容的末尾,如果limit此时在有效内容的末尾,那么会变更为buffer末尾。

通过flip改变状态后,buffer可以用来读取数据或重新写入数据。

Selector

NIO中有一个很有意思也很重要的概念,就是选择器Selector,选择器是为了在一个线程中同时管理多个channel而设计出来的,channel可以将自己注册到一个selector,并告诉selector想要进行的操作,selector可以定时从所有注册的channel中选出已经准备好进行之前声明的操作的channel对应的key,继而进行相应的的预期操作。

大家都说NIO采用了事件驱动模型,先了解一下什么是事件驱动模型。

事件驱动模型

参考链接:https://baike.baidu.com/item/%E4%BA%8B%E4%BB%B6%E9%A9%B1%E5%8A%A8%E6%A8%A1%E5%9E%8B/1419787?fr=aladdin

通常,我们写服务器处理模型的程序时,有以下几种模型:

(1)每收到一个请求,创建一个新的进程,来处理该请求;

(2)每收到一个请求,创建一个新的线程,来处理该请求;

(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

上面的几种方式,各有千秋,

第(1)种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。

第(3)种方式,事件实现了异步处理,事件源线程不用阻塞,提高了响应速度,但是在写应用程序代码时,逻辑比前面两种都复杂。

综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。

事件驱动模型图解

Java NIO学习总结_第2张图片

其实NIO的事件驱动模型,正是基于Selector实现的,当我们需要进行一次IO操作,主要步骤是这样的:

  1. 产生一个用于IO操作的channel;
  2. 将这个channel注册到selector中,并告知selector需要进行的操作;
  3. selector定时检查并获取已准备就绪可以处理的IO事件;
  4. 按照要求处理IO事件。

可以看到其实和上图中所描述的一样的,需要注意的是,因为Selector是基于事件驱动模型设计的,所以要求注册到Selector的channel必须是non-blocking的,可以通过channel.configureBlocking(boolean block)进行设置。

FileChannel不支持设置为non-blocking,所以FileChannel不能基于Selector实现异步IO。

API使用

一、使用FileChannel读取文件

当使用FileChannel读取一个文件时,代码如下:

static void channelStudy() throws IOException {
    //获取需要读写的文件,并包装为RandomAccessFile实例
    RandomAccessFile aFile = new RandomAccessFile("C:\\Users\\admin\\Desktop\\tmp/application.yml", "rw");
    //获取需要的channel
    FileChannel inChannel = aFile.getChannel();
    //刚分配的buffer为write模式
    ByteBuffer buf = ByteBuffer.allocate(128);
    //从channel中读取数据写入buffer中
    int bytesRead = inChannel.read(buf);
    //当读取到数据末尾时,channel.read()会返回-1
    while (bytesRead != -1) {
        //将buffer置为read模式,position置为0,limit置为实际读取到的数据长度
        buf.flip();
        //通过buf.hasRemaining()判断buffer中是否还有未读取的数据
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        //读取完毕后将buffer清空
        buf.clear();
        //继续从channel中向buffer中读取数据
        bytesRead = inChannel.read(buf);
    }
    //使用完毕后将file关闭
    aFile.close();
}

解析:

上面已经提到,FileChannel不能被设置为non-blocking的,所以不能结合Selector使用,这段代码依然是阻塞的。

二、使用NIO进行网络编程

使用NIO进行网络编程,主要关注Selector、ServerSocketChannel和SocketChannel,Selector前面已经详细介绍过,ServerSocketChannel可以理解为一个网络服务器,是用来接收网络请求的,而一个SocketChannel就代表着一次请求。

下面的代码实现了一个简单的网络时间服务和客户端,主要讲解已经写在代码注释中。

学习代码仓库地址:https://gitee.com/imdongrui/study-repo

仓库中的ioserver和ioclient

服务端代码:

package com.dongrui.study.ioserver.nioserver;

import com.google.common.primitives.Bytes;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;

/**
 * 简单时间服务器
 *
 * @author dongrui
 */
public class SimpleTimeServer implements Runnable {

    /**
     * 多路控制器
     */
    private Selector selector;

    /**
     * 用于接收请求的ServerSocket通道,对应于BIO中的ServerSocket
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * 是否继续执行监听的标识
     */
    private boolean isContinue = true;

    /**
     * 初始化
     *
     * @param port
     */
    public SimpleTimeServer(int port) {
        try {
            selector = Selector.open();//打开多路控制器
            serverSocketChannel = ServerSocketChannel.open();//打开ServerSocketChannel
            serverSocketChannel.configureBlocking(false);//将阻塞模式设置为非阻塞,否则无法注册到多路控制器
            serverSocketChannel.bind(new InetSocketAddress(port));//绑定端口
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//注册到多路控制器
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理SelectionKey的函数
     *
     * @param key 当前需要处理的SelectionKey
     * @throws IOException
     */
    private void handleKey(SelectionKey key) throws IOException {
        if (key.isValid()) {
            if (key.isAcceptable()) {
                //ServerSocketChannel才会出现accept事件,调用accept函数获取接收到的SocketChannel,并将其注册到多路控制器
                ((ServerSocketChannel) key.channel()).accept().configureBlocking(false).register(selector, SelectionKey.OP_READ);
            } else if (key.isReadable()) {//处理read事件
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(2);//此处取buffer长度为2,是为了验证多次read
                int byteCount = channel.read(buffer);
                List<Byte> bytes = new ArrayList<>();//获取完整byte数据后再进行处理
                while (byteCount > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        bytes.add(buffer.get());
                    }
                    buffer.clear();
                    byteCount = channel.read(buffer);
                }
                System.out.println("time server received client request: " + new String(Bytes.toArray(bytes), "utf-8"));
                key.interestOps(SelectionKey.OP_WRITE);//将当前key的interest设置为write,也可以在read后直接write
            } else if (key.isWritable()) {//处理write事件
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(256);
                buffer.put((new Date().getTime() + "").getBytes()).flip();
                channel.write(buffer);
                //处理完毕后,将key的状态置为canceled,将channel关闭,本次请求处理完毕
                key.cancel();
                channel.close();
            }
        }
    }

    /**
     * 停止监听
     */
    public void stop() {
        this.isContinue = false;
    }

    @Override
    public void run() {
        while (isContinue) {
            try {
                selector.select(5000);//多路控制器获取需要处理的key的数量,如果当前没有可处理的key,则会等待timeout时间
                Set<SelectionKey> selectionKeys = selector.selectedKeys();//获取可以处理的key
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    handleKey(key);//处理当前key
                    iterator.remove();//处理后将其移除
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new SimpleTimeServer(8080).run();
    }
}

客户端代码:

需要注意在使用时如果配合多线程使用,必须保证主线程后于子线程结束,否则主线程结束,子线程会被强制结束,通讯失败。

package com.dongrui.study.ioclient.nioclient;

import com.google.common.primitives.Bytes;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class SimpleTimeClient implements Runnable {
    private Selector selector;
    private boolean isContinue = true;
    private String host;
    private int port;

    public SimpleTimeClient(String host, int port) throws IOException {
        this.host = host;
        this.port = port;
        selector = Selector.open();
    }

    /**
     * 产生SocketChannel并进行连接
     *
     * @throws IOException
     */
    private void doConnect() throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress(host, port));
        if (channel.finishConnect()) {
            //连接成功,注册到多路控制器,触发事件为write
            channel.register(selector, SelectionKey.OP_WRITE);
        } else {
            //连接不成功,则注册到多路控制器,触发事件为connect,在connect中会再次尝试连接
            channel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    private void handleKey(SelectionKey key) throws IOException {
        if (key.isConnectable()) {//尝试再次连接
            SocketChannel channel = (SocketChannel) key.channel();
            channel.connect(new InetSocketAddress(host, port));
            if (channel.finishConnect()) {
                key.interestOps(SelectionKey.OP_WRITE);
            } else {
                System.out.println("连接失败");
                key.cancel();
                channel.close();
            }
        } else if (key.isReadable()) {//读取服务端返回数据
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(2);
            List<Byte> bytes = new ArrayList<>();
            int byteCounts = channel.read(buffer);
            while (byteCounts > 0) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    bytes.add(buffer.get());
                }
                buffer.clear();
                byteCounts = channel.read(buffer);
            }
            System.out.println("当前时间:" + new String(Bytes.toArray(bytes), "utf-8"));
            key.cancel();
            channel.close();
        } else if (key.isWritable()) {//写入发送给服务端的数据
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(256);
            buffer.put("识相的快点报时".getBytes()).flip();
            channel.write(buffer);
            key.interestOps(SelectionKey.OP_READ);
        }
    }

    public void stop() {
        this.isContinue = false;
    }

    @Override
    public void run() {
        try {
            while (isContinue) {
                Thread.sleep(1000);
                doConnect();
                selector.select(5000);
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    handleKey(key);
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            new SimpleTimeClient("localhost", 8080).run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

你可能感兴趣的:(Java)