普通IO流

JAVA普通IO流总图如下所示:
图片说明
总的来说,可以从以下几个方向进行分类:

  • 按传输单位分:字节流、字符流
    1字节=8bits,1字符=2字节。字节流以字节为单位传输,即每次读取(写出)一个字节,字符流则按字符为单位传输。而一个中文占一个字符(两个字节),所以以字节流传输中文的时候会出现乱码,字符流则不会。
  • 按传输方向分:输入流、输出流
    输入:从外部到程序的方向;输出:从程序到外部的方向。
  • 按功能分:节点流、处理流
    节点流:从一个地方读、或向一个地方写的流
    处理流:对一个已存在的流进行包装,调用这个已存在的流进行读写。而处理流的作用则是可以在被包装的流的基础上增加一些独有的功能。

    字节流

    输入字节流

    InputStream:抽象类,定义了字节输入流的基本方法,所有字节输入流都继承或间接与它。
  • ByteArrayInputStream:字节数组输入流,该类的功能就是从字节数组(byte[])中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去,我们拿也是从这个字节数组中拿。
  • PipedInputStream:管道字节输入流,它和PipedOutputStream一起使用,用于实现多线程间的管道通信
  • FilterInputStream:一个装饰者类,具体不同的功能由它的子类实现。即继承他的子类都是用来装饰别的流对象的,也就是说它的子类都是处理流。
  • BufferedInputStream:缓冲流,继承自FilterInputStream,对别的流进行装饰,增强。内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送。效率更高。
  • DataInputStream:数据输入流,继承自FilterInputStream,它是用来装饰其它输入流,允许程序以与机器无关方式从底层输入流中直接读取基本Java 数据类型
  • FileInputSream:文件输入流。它通常用于对文件进行读取操作。
  • ObjectInputStream:对象输入流,反序列化使用,用来从序列化流中获取JAVA对象。

    输出字节流

  • OutputStream 是所有的输出字节流的父类,是一个抽象类,定义了字节输出流的基本行为
  • ByteArrayOutputStream:用于向字节数组中写入数据。
  • PipedOutputStream:管道流,用于向与其它线程共用的管道中写入数据。
  • FilterOutputStream:装饰器,其子类都是实现了不同功能的、用于装饰别的流的装饰器,即处理流。
  • BufferedOutputStream:处理流,继承自FilterOutputStream,有一个缓冲区,缓冲区满了再输出,提高效率。
  • DataOutputStream:处理流,继承自FilterOutputStream,使用它可以直接将JAVA数据类型转换成字节流输出。
  • PrintStream:处理流,继承自FilterOutputStream,使用它可以直接将字符串输出到流中,该类底层将字符串转换成字节。
  • FileOutputStream:文件输出流,用于向本地文件中写入数据。
  • ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流(序列化中使用)。

字符流

字符输入流

Reader:是所有的输入字符流的父类,它是一个抽象类。

  • CharReader:从char数组读取数据
  • StringReader:从String中读取数据。
  • PipedReader:管道字符输入流,从与其它线程共用的管道中读取字符数据。
  • BufferedReader:一个装饰器,它和其子类负责装饰其它Reader对象,增加缓存作用。
  • FilterReader:是所有自定义具体装饰流的父类。
  • PushbackReader:FilterReader的子类,对Reader对象进行装饰,会增加一个行号。
  • InputStreamReader:是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。
  • FileReader:继承自InputStreamReader,用于从文件中读取字符。

字符输出流

  • Writer 是所有的输出字符流的父类,它是一个抽象类。
  • CharArrayWriter:它们分别向Char数组写入数据。
  • StringWriter:向String 中写入数据。
  • PipedWriter:是向与其它线程共用的管道中写入数据。
  • BufferedWriter:是一个装饰器为Writer,提供缓冲功能。
  • PrintWriter 和PrintStream 极其类似,功能和使用也相似。
  • OutputStreamWriter 是OutputStream 到Writer 转换的桥梁。
  • FileWriter:继承自OutputStreamWriter,向文件中输出字符。

字符流和字节流的选择

字符流和字节流的使用范围:字节流一般用来处理图像,视频,以及PPT,Word类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,字节流可以用来处理纯文本文件,但是字符流不能用于处理图像视频等非文本类型的文件。
结论:只要是处理纯文本数据,就优先考虑使用字符流。除此之外都使用字节流。

字符流和字节流的转换

同上文所说,使用InputStreamWriter和OutputStreamWriter来实现字符流和字节流的转换。文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流输出保存。
字节和字符流互相转换时可以指定编码格式进行读取/输出。

System类的IO

System.in:系统输入流,指键盘的输入数据
System.out:系统输出流,指输出到显示器显示的输出流
System.error:标准错误输出,也是一条到显示器的输出流

NIO

优点

同步非阻塞

与上述传统IO方式相比,JAVA NIO有着很多优势。最大的优势就是他是同步非阻塞的,而传统IO是同步阻塞的。关于同步、异步、阻塞、非阻塞的辨析,如下所示:

  • 同步:同步指的是调用者就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了,即调用者会一直“关心着”调用的结果。
  • 异步:调用者在调用在发出之后,这个调用就直接返回了,所以没有返回结果。最终的结果是被调用者通过通知或者回调函数来来返回结果给调用者的,即调用者调用后,就“不管了”。
  • 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,不能执行别的任务。调用线程只有在得到结果之后才会返回。
  • 非阻塞:非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,进程可以去执行别的任务。

以烧水壶的例子来辨别上述的关系:

  • 同步阻塞:用锅烧水,我站在旁边,盯着水开了没有,直到烧开之前都不离开。
  • 同步非阻塞:用锅烧水,我去干点别的事,但我一直关心水开了没有,每过一段时间去检查一下,水没开则继续做别的事。
  • 异步阻塞:用带响的水壶烧水,我站在旁边,直到水开之前都不走。但是我不会盯着水开了没有。而是如果水开了,水壶通过响声自动通知我。
  • 异步非阻塞:响水壶烧水,我去干点别的事。如果水开了,水壶通过响声自动通知我。

一个线程管理多个通道

通过NIO的多路复用器,可以让一个线程同时管理多个通道。而由于传统IO是阻塞的,一个线程必然只能管理一个流。当然也可以使用线程池的方式堆线程进行复用,但是终究还是一条线程只能管理一个流对象。所以在譬如客户机-服务器的网络模型中,如果一个服务器为10000个客户机提供服务,则不得不开辟10000条线程进行IO。而NIO则不需要如此。具体的原理见下文NIO组件说明。

能前后移动数据

原始的IO面向流且 不存在缓存的概念。这意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。而NIO是面向块、面向缓存的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。

NIO核心组件

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作。通道负责进行数据传输,而读数据时总是要先从通道读取到缓冲区中,或者写数据时则是从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

Buffer(缓冲区)

缓冲区是一个容器、一个连续数组。顾名思义,要读数据,总是先要将通道的数据读入缓冲区,然后再在缓冲区中获取数据。写数据则是先将数据写入缓冲区,然后缓冲区再将数据放到通道中传输。关于缓冲区由以下三个重要的参数需要理解:
图片说明

  • position:下一个要操作的元素的位置。
  • limit:字面理解是当前操作中最多能操作到的位置,也就是position总是小于等于limit,具体案例见下文。
  • capacity:缓冲区的最大容量。只能往其中写入capacity个byte、long、char等类型。

举个例子:

  1. 一开始是写模式,初始limit=capacity,position=0,因为当前写操作写,下一个要写的位置就是缓冲区的起始位置0,最多能写满,也就是写到capacity的位置,所以limit=capacity。每写一个元素,position往下移1,假设最终写到了如上图中左图的位置。
  2. 调用buffer.flip(),将缓冲区从写模式切换到读模式。此时position=0,因为下一个要读的元素的位置就是刚才写时的第一个数据,也就是起始位置的数据。然后limit等于写模式下的position,因为读模式下最多只能读到刚刚写的数据中最后一个数据的位置,也就是写模式下的position指向的位置。

往buffer中写数据

写数据到buffer中,自然可能是两种写。一是用户写到buffer中,也就是用户要发送数据时。二是从通道中获取数据写到buffer中,也就是用户要获取数据时。

  1. 通过Buffer的put()方法写数据到Buffer:
    buf.put(byte);
  2. 将channel中的数据写到Buffer中:
    int bytes = channel.read(buf);

从buffer中获得数据

同样,根据用途/传输方向的不同,有两种方式从buffer中获得数据

  1. 程序从Buffer中读取数据:
    byte bt = buf.get();
  2. channel从buffer中获得取数据:
    int bytes = channel.write(buf); 

Buffer其他常用方法

  • flip():将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值。
  • clear():清空缓冲区所有内容。
  • compact():清空缓冲区已读内容,未读的保留。
  • rewind():将position设置为0,使得可以重读Buffer中的所有数据,limit保持不变。
  • mark():可以标记一个特定的position。
  • reset():和mark()配合使用,使position恢复到原来的position上。

Channel(通道)

用户1通过NIO发送数据到用户2,使用Channel和Buffer传输的方式如下图所示:
图片说明

通道和流的对比

  • 通道是一个双向通道,可以同时进行读写,而流只能读或者只能写。
  • 通道可以实现异步读写数据。
  • 通道可以从缓冲读数据,也可以写数据到缓冲。

通道的类型

  • FileChannel:文件传输
  • SocketChannel:TCP客户端
  • ServerSocketChannel:TCP服务端
  • DatagramChannel:UDP

Selector(多路复用器)

可以让多个Channel注册到一个Selector上,从而就实现了单线程处理多通道。因此Selector被称为多路复用器。它可以检测多个Channel的状态,看读或者写事件是否就绪,当监听到某一Channel的某个状态时,才允许对Channel进行相应的操作。要求注册Channel必须是非阻塞的。

创建Selector:

调用Selector.open()方法创建一个Selector对象。

注册Channel到Selector上:

channel.register(selector, Selectionkey.OP_READ);

register()方法的第二个参数是一个“感兴趣集合”。意思是在通过Selector监听Channel时,对Channel上发生的什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write。

SelectionKey

一个Selector管理多个Channel,那么用什么标识来管理各不同的Channel?SelectionKey。一个SelectionKey表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系,所以可以用SelectionKey来管理Selector中特定的Channel,而从上文channel的注册也发现,注册的就是和一个SelectionKey绑定的过程。
有以下的方法来通过SelectionKey管理Channel:

key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
key.channel(); // 返回该SelectionKey对应的channel。
key.selector(); // 返回该SelectionKey对应的Selector。
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask
key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。

参考

https://www.cnblogs.com/CQqf/p/10795656.html
https://zhuanlan.zhihu.com/p/127911769
https://www.jianshu.com/p/5bb812ca5f8e
https://ifeve.com/overview/