美文网首页
IO与NIO 基础知识

IO与NIO 基础知识

作者: 8090的大叔 | 来源:发表于2020-04-11 21:48 被阅读0次

    面试过程中我们经常会遇到一些IO框架的问题,并且因为Java1.4中引入的NIO框架可以有效的解决高并发、大量连接、I/O处理的问题,越来越多的在大型网络应用中使用,所以也是需要掌握的框架之一。

一、Java IO(同步、阻塞)

首先看一张图,是对java.io中,流式部分的超类的整理。

java.io.png

1. IO流的分类

处理的对象:字节流 和 字符流,二进制数据和编码处理后符合某种格式规定的特定数据。
数据流向:输入流 和 输出流,输入、输出方向不同。

2. 字符流和字节流的区别

1)使用方式上非常相似;

2)缓冲区的使用:字节流在操作时不会使用缓冲区(内存),而是直接操作文件本身,而字符流是使用缓冲区的;字节流操作文本时,即使不关闭(close方法),信息就已经输出了。而字节流不关闭时是不会有任何内容输出的,可以使用flush方法强制刷新缓冲区输出内容。

3)读写单位不同:字节流,以字节(8bit)为单位;字符流,以2个字节的Unicode字符为单位。

4)处理对象不同:字节流,能处理所有类型的对象(音频、视频、图片等);字符流只能处理字符类型的数据;

缓冲区:比较特殊的内存区域;(频繁操作一个资源(文件或数据库)性能会很低,所以将一部分数据读写到缓冲区后,然后一次性读写,类似批量操作,效率比较高)


3. 部分常用流的使用

字节流中,ByteArrayInputStream 和 ByteArrayOutputStream 作为数组字节流,在开发工作中有很多应用的地方。

ByteArrayInputStream:如果需要读取的数据是byte数组(String类型数据可以直接getBytes方法),并转换为流。并将其使用在后续需要的地方。

ByteOutputStream:如果我们需要将一个数据输出到OutputStream,并返回字节数组的情况下,ByteArrayOutputStream用起来就很方便。

    static void testByteArrayInputStream() throws Exception {
        String str = "test ByteArray";
        ByteArrayOutputStream  bos = new ByteArrayOutputStream();
        bos.write(str.getBytes());
        //通过toByteArray方法获取到byte[],在转为字符串输出
        System.out.println("ByteArrayOutputStream处理,将byte[]转为字符串:"+new String(bos.toByteArray()));
        //使用ByteArrayInputStream读取byte[]
        InputStream bis = new ByteArrayInputStream(bos.toByteArray());
        //读取流中的字节读取到[]bytes中
        byte[] bytes = new byte[bos.size()];
        bis.read(bytes);
        System.out.println("ByteArrayInputStream处理,将byte[]转为字符串:"+new String(bytes));
    }

java.io.File:是IO包下的非流式类,文件和目录路径的抽象表示,提供了对文件和目录的操作方法,我们就基于这两个文件,进行测试。

public static File fileA = new File("D:/fileA.txt");
public static File fileB = new File("D:/fileB.txt");

文件读写操作中流的使用
在做文件读取和写入过程中,有几种方案供我们选择,一下列出几种方式

  1. 单纯使用 FileInputStream 和 FileOutputStream 完成读取和写入(数据量越大,效率越低
  static void testFileStream() throws Exception{
        String str = "test FileStream";
        OutputStream fos = new FileOutputStream(fileA);
        fos.write(str.getBytes());
        System.out.println("FileOutputStream 写入完成!");
        //读取刚刚写入的文件中的内容
        InputStream fis = new FileInputStream(fileA);
        byte[] bytes = new byte[fis.available()];   //创建一个和文件大小相同的字节数组
        fis.read(bytes);    //写入字节数组
        System.out.println("FileInputStream读取完成,将byte[]转为字符串:"+new String(bytes));
  }
  1. 使用BufferedInputStream / BufferedOutputStream 包装FileInputStream / FileOutputStream完成读取和写入(效率高于第一种方式)
  static void testBufferedStream() throws Exception {
        long begin = System.currentTimeMillis();
        String str = "test BufferedStream";
        FileOutputStream fos = new FileOutputStream(fileB);
        //使用BufferedOutoutStream进行一次包装,使用bos对象写文件
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        bos.write(str.getBytes());
        bos.flush();
        System.out.println("FileOutputStream 写入完成!");
        //读取刚刚写入的文件中的内容
        FileInputStream fis = new FileInputStream(fileB);
        BufferedInputStream bis = new BufferedInputStream(fis);
        byte[] bytes = new byte[bis.available()];
        bis.read(bytes);
        System.out.println("BufferedInputStream 读取完成,将byte[]转为字符串:"+new String(bytes));
        long end = System.currentTimeMillis();
        System.out.println("FileWriter执行耗时:" + (end - begin) + " 豪秒");
   }
  1. 使用FileReader 和 FileWriter 进行文件读写操作,大文件处理效率高于前两个。
  static void testFileReaderAndWriter() throws IOException {
        String str = "test FileReader And FileWriter";
        FileWriter fw = new FileWriter(fileC);
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(str);
        bw.flush();
        System.out.println("FileWriter 写入完成!");
        FileReader fr = new FileReader(fileC);
        BufferedReader br = new BufferedReader(fr);
        System.out.println("FileReader 读取完成:" + br.readLine());
    }
  1. SequenceInputStream表示其他流的逻辑串联,其实就是一次读取多个流。
  static void testSequenceInputStream() throws Exception{
        InputStream osA = new FileInputStream(fileA);
        InputStream osB = new FileInputStream(fileB);
        InputStream osC = new FileInputStream(fileC);
        //建立一个输入流的集合
        List<InputStream> inputStreamList = new ArrayList<InputStream>();
        inputStreamList.add(osA);
        inputStreamList.add(osB);
        inputStreamList.add(osC);
        //通过Collections.enumeration得到 enumeration 对象
        Enumeration<InputStream> enumeration= Collections.enumeration(inputStreamList);
        SequenceInputStream sis = new SequenceInputStream(enumeration);
        //转BufferedReader 进行读取
        BufferedReader br = new BufferedReader(new InputStreamReader(sis));
        System.out.println("testSequenceInputStream 读取完成:"+br.readLine());
    }

4. 关于流的关闭问题

上述代码中,并没有进行流的关闭操作和异常的处理,这里着重记录一下关于流的关闭问题。
这里抛出几个问题,并作出相应解答。

1)流打开后需不需要关闭?
打开的流就一定要关闭,使用close方法。
2)如果关闭的话,应该在什么位置关闭?
放入finally语句块中执行,先判断流是否为NULL,不为NULL进行关闭。
3)多个流嵌套使用时,关闭规则是什么?
理论上讲,先开启后关闭,后开启先关闭。如使用处理流,可直接关闭处理流,处理流会调用节点流的关闭方法。

Java7之后,InputStream、OutputStream、Reader、Writer,都实现了。Closeable/AutoCloseable接口。可以使用try-with-resources语句来释放java流对象。

//选择流
  try(FileInputStream is = new FileInputStream(fileA);FileOutputStream os = new FileOutputStream(fileB)) {  
    //操作
  }catch(FileNotFoundException e) {
    e.printStackTrace();
  }catch(IOException e) {
    e.printStackTrace();
  }

二、Java NIO(同步、非阻塞)

了解NIO前,先要整理一下IO的一些特点,并且和NIO进行一些对比。然后根据对比情况,再来讨论一下IO与
NIO的选择,因为NIO的编码相对比较复杂,其实多数主流中间件都是支持NIO的,所以不必我们自己编码实现,了解其原理和性能更为重要。

传统 java.io 包中的 IO 的特点

  1. 基于stream模型实现,提供了常见的IO功能: File抽象、输入输出流;
  2. 交互方式:同步、阻塞的方式,面向流(BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter应用了缓冲区技术);
  3. 在读写动作完成前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序;

java.nio包中NIO的特点

  1. 可以构建多路复用、同步、非阻塞IO程序,面向缓冲区(Buffer API,Java程序可以直接操作缓冲区);
  2. 同时提供了更接近操作系统底层的高性能数据操作方式;
  3. NIO加入了Selector(选择器),可以让一个线程监视(事件驱动机制)多个Channel进行管理,减少线程开销;

两者特点对比下

  1. 理论上IO面向流,从Stream中逐步读取数据,并没有使用缓冲区,而NIO面向缓冲区,数据操作更加高效,但BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter应用了缓冲区技术,如果合理使用,特定场景下效率并不弱于NIO;
  2. IO线程是阻塞的,也就是当进行read or writer 时,该线程只能等待至操作完成,期间线程不能做任何事情;而NIO线程是非阻塞的,通过Selector选择器选择合适的Channel进行处理,当一个Channel没有数据可用时,线程就可以取操作其他的Channel进行处理,这样更高效。

1. NIO的核心组件

1. Buffer(缓冲区):NIO的缓冲区不只是简单的byte数组,而是封装过的Buffer类,用于特定原始类型的数据容器,通过Buffer的API,我们可以灵活的操作各类型数据。
2. Channel(通道):负责打开IO设备(文件、硬件设备、网络套接字)的链接,本身不存储数据。
3. Selector(选择器):用于采集Channel的事件,首先将Channel注册到Selector并设置好关心的事件,当该Channel发生对应类型事件时,线程就为其服务。无事件发生,线程挂起让出CPU资源。
4. Charset(字符集):包含了字节和 Unicode 字符之间转换的 charset,还定义了用于创建解码器和编码器以及获取与 charset 关联的各种方法。

2. Buffer(缓冲区)

重要概念
capacity(容量):表示Buffer的最大数据容量,创建后不能更改;
limit(限制):缓冲区读写数据的终止点,limit位置后的区域无法访问;
position(位置):缓冲区读写数据的位置,初始值为0,随着数据加入改变;
mark(标记):调用mark()标记当前position;
rest(重置):重置position至mark()标记;

常用实现类
ByteBuffer , CharBuffer , DoubleBuffer , FloatBuffer , IntBuffer , LongBuffer , ShortBuffer

Buffer下子类的用法都比较类似,使用IntBuffer作为示例,来看一下其中方法使用

static void testBuffer(){
        IntBuffer buffer = IntBuffer.allocate(10); //创建一块大小为10的缓冲区
        System.out.println("缓冲区信息:"+buffer.toString());
        buffer.put(1);  //写入数据1
        System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
        buffer.put(2);  //写入数据2
        buffer.mark();  //标记当前的position
        System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
        buffer.reset(); //重置position至2的位置
        buffer.put(3);  //写入3
        System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
    }

控制台打印结果
缓冲区信息:java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
缓冲区信息:java.nio.HeapIntBuffer[pos=1 lim=10 cap=10];内容:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
缓冲区信息:java.nio.HeapIntBuffer[pos=2 lim=10 cap=10];内容:[1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
缓冲区信息:java.nio.HeapIntBuffer[pos=3 lim=10 cap=10];内容:[1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
1;2;3;0

ByteBuffer 的直接缓冲区和非直接缓冲区

直接缓冲区:通过allocateDirect()方法分配的缓冲区,将缓冲区建立在物理内存中,可提高效率,但资源消耗大,不易控制;
非直接缓冲区:通过allocate()方法分配的缓冲区,将缓冲区建立在JVM内存中;

3. Charset(字符集)java.nio.charset包

在学习Channel前,为了方便后续的代码演示,先对Charset做简单的了解,

重要概念
包含了字节Unicode字符之间转换的charset。
CharsetEncode(编码器):将字符转换成字节。
CharsetDecode(解码器):将字节转换为字符。
解码器和编码器都不能直接创建,需要一个Charset对象来创建对应的解码器和编码器。

4. Channel(通道)java.nio.channels包

本身不能存储数据,通过Buffer(缓冲区)进行读写操作,read()方法,读取通道数据到缓冲区,writer()方法,将缓冲区数据写入通道。
需要节点作为创建基础。例如FileInputStream和FileOutputStream()的getChannel()。RandomAccessFile也能创建文件通道,支持读写模式。通过IO流创建的通道是单向的,使用RandomAccessFile创建的通道支持双向。

常用实现类
FileChannel:读取、写入、映射、操作文件的通道
DataChannel:通过 UDP 读写网络数据通道
SocketChannel:通过 TCP 读写网络数据通道
ServerSocketChannel:监听多个 TCP 连接,对每一个新进来的的链接,都创建一个SocketChannel

读写文件代码示例

    //为方便演示代码中未处理Exception
    static void testChannels() throws Exception {
        File fileA = new File("D:/fileA.txt");  //读取的文件
        File fileB = new File("D:/fileB.txt");  //写入的文件
        //1. 通过RandomAccessFile 或 FileInputStream getChannel() 获取Channel;
        FileChannel fileAChannel =  new RandomAccessFile(fileA,"r").getChannel(); // new FileInputStream(fileA).getChannel();
        //创建缓冲区,初始大小
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将Channel中数据读取到 byteBuffer
        fileAChannel.read(byteBuffer);
        System.out.println(byteBuffer.toString());  //读取后,未翻转状态下,缓冲区信息
        byteBuffer.flip();      //翻转,将缓冲区状态存数据转为取数据,下方我们要进行数据显示。
        System.out.println(byteBuffer.toString());  //翻转后,缓冲区信息,pos=0,lim等于读取数据的长度
        //默认字符集创建解码器
        CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
        System.out.println("缓冲区信息:"+byteBuffer.toString()+";内容:" + decoder.decode(byteBuffer).toString());

        //2. 通过Channel将A文件内容,写入B文件
        byteBuffer.flip();   //翻转,将缓冲区状态取数据转为存数据
        System.out.println(byteBuffer.toString());  //翻转后,缓冲区信息,pos=读取数据的长度,lim等于读取数据的长度
        FileChannel fileBChannel = new FileOutputStream(fileB).getChannel();
        fileBChannel.write(byteBuffer);
    }

控制台打印结果
java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
缓冲区信息:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024];内容:我是fileA
java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]

5. Selector(选择器)java.nio.channels包

以上概念中,我们知道了一个词,多路复用器,简单解释一下。

这里的IO一般指网络IO,多路可以理解为多个TCP/UDP连接(或多个Channel),复用就是复用一个或者少量线程。
IO多路复用:多个TCP/UDP连接产生的IO操作,用一个或少量线程来处理。

但在现有硬件设施基础场CPU有多个内个,如果不适用多任务处理,貌似时在浪费资源,本文先不讨论这个问题,只单纯了解Selector的一些知识。

Selector的使用方法,Selector.open()就是将Channel注册到Selector,并且Channel设置为非阻塞模式,然后Selector通过它的事件驱动机制采集Channel的事件,分配线程为其服务。

事件以及对应的SelectionKey
Accept:有可以接受的连接 ;SelectionKey.OP_ACCEPT
Connect:连接成功 ;SelectionKey.OP_CONNECT
Read:有数据可读 ;SelectionKey.OP_READ
Write:可以写入数据了 ;SelectionKey.OP_WRITE

通过一个小程序,来对NIO有一定深入理解,为了简化,只写了服务端读和客户端写的事件。有兴趣可以自行完善。

服务端示例

    //模拟的一个服务端与客户端通信的过程,仅测试代码使用
    static void testSelector() throws Exception{
        CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
        //创建一个Channel对象,并设置为非阻塞
        ServerSocketChannel  serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8088));
        //创建Selector对象
        Selector selector = Selector.open();
        //向Selector注册事件,validOps() 返回此通道支持的操作
        serverSocketChannel.register(selector, serverSocketChannel.validOps());

        while (true) {
            System.out.println(new Date()+" 等待事件发生");
            int number = selector.select();  //该调用会阻塞,直到至少有一个事件就绪、准备发生
            if (number == 0) continue;
            Set<SelectionKey> selectionKeySet = selector.selectedKeys();   //获取连接的集合
            Iterator<SelectionKey> keyIterator = selectionKeySet.iterator();    //迭代,多客户端连接
            while (keyIterator.hasNext()) {
                final SocketChannel client;
                SelectionKey selectionKey = keyIterator.next();
                if (selectionKey.isAcceptable()) {   //可连接
                    System.out.println("a connection was accepted by a ServerSocketChannel.");
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                    client = channel.accept();  //接受套接字的连接
                    client.configureBlocking(false);
                    //接受连接后,将通道绑定为读事件,读取客户端消息
                    client.register(selector,SelectionKey.OP_READ);
                } else if (selectionKey.isConnectable()) {  //连接成功
                    System.out.println("a connection was established with a remote server.");
                } else if (selectionKey.isReadable()) { //有数据读
                    System.out.println("a channel is ready for reading");
                    SocketChannel readChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    readChannel.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println("客户端消息:"+decoder.decode(byteBuffer));
                    byteBuffer.clear();
                } else if (selectionKey.isWritable()) { //可以写数据了
                    System.out.println("a channel is ready for writing");
                }
                keyIterator.remove();   //入不删除,会一直循环
            }
        }
    }

客户端示例

    static void testClient() throws Exception{
        SocketChannel socketChannel = SocketChannel.open(); //创建通道
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost",8088)); //连接服务端
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);  //注册事件
        Scanner scanner = new Scanner(System.in);
        while (true){
            int select = selector.select(); //阻塞等待事件触发
            if(select == 0) continue;
            Set<SelectionKey> keys = selector.selectedKeys();   //事件的集合
            Iterator<SelectionKey> keyIterator = keys.iterator();   //迭代
            while (keyIterator.hasNext()) {
                SelectionKey selectionKey = keyIterator.next();
                if (selectionKey.isConnectable()) {   //连接完成
                    System.out.println(new Date()+" 连接完成...");
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    if(channel.isConnectionPending()){  //连接完成
                        channel.finishConnect();    //确认通道连接已建立
                    }
                    channel.register(selector,SelectionKey.OP_WRITE);  //注册通道为写模式
                }else if(selectionKey.isWritable()){
                    System.out.println(new Date()+" 输入发送信息");
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    String str = scanner.nextLine();
                    if("exit".contains(str)){
                        break;
                    }
                    CharsetEncoder charset = Charset.defaultCharset().newEncoder();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    byteBuffer.put(str.getBytes());
                    byteBuffer.flip();
                    channel.write(byteBuffer);
                }
                keyIterator.remove();
            }


        }
    }

5. Tomcat中BIO、NIO、APR模式

简单些一下相关配置,具体性能可自行百度。

<!-- bio server.xml 配置 (重启生效)-->
<Connector port="8080" protocol="HTTP/1.1"  connectionTimeout="20000" redirectPort="8443" />

<!-- nio server.xml 配置 (重启生效)-->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443" />

<!-- apr server.xml 配置 (重启生效)需要安装 apr 、 apr-utils 、tomcat-native包 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="20000" redirectPort="8443" />

三、结尾

在讲NIO开始时说过,其实我们不必自己搭建NIO相关代码,有很多很好用的NIO框架可供我们使用,比如Netty。在后续文章中我会写一下Netty的入门教程。

相关文章

  • note

    Java IO,NIO,NIO2 以及与操作系统,磁盘 IO NIO模型selector NIO的核心是IO线程池...

  • Java NIO

    1、IO和NIO的区别? 1)IO面向流、NIO面向缓冲;2)IO是阻塞IO、NIO是非阻塞IO;3)无 与 选择...

  • java NIO详解

    NIO原理 NIO与IO的区别 首先来讲一下传统的IO和NIO的区别,传统的IO又称BIO,即阻塞式IO,NIO就...

  • IO与NIO 基础知识

    面试过程中我们经常会遇到一些IO框架的问题,并且因为Java1.4中引入的NIO框架可以有效的解决高并发、大量连接...

  • 29、 Java IO与 NIO的区别(补充)

    Java IO与 NIO的区别(补充) NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同...

  • Java NIO

    # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...

  • 图解Java NIO

    目录: NIO结构 NIO与传统IO异同 NIO使用步骤 NIO代码 ByteBuffer难点解析 1:NIO结构...

  • Java常见面试题汇总-----------Java基础(NIO

    18. NIO与IO的区别   NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和...

  • 18道IO常问面试题,题题惊险!

    大厂招聘IO常问面试题 NIO与IO的区别 NIO和IO适用场景 BIO, NIO, AIO有什么区别,分别是什么...

  • Java之NIO(非阻塞IO)

    【1】NIO的与IO的区别: 总的来说java 中的IO 和NIO的区别主要有3点:1)IO是面向流的,NIO是面...

网友评论

      本文标题:IO与NIO 基础知识

      本文链接:https://www.haomeiwen.com/subject/yicymhtx.html