Java IO&NIO
Java I/O的类库的基本架构
IO是任何编程语言都无法避免的问题,是整个人机交互的核心问题。
Java的IO大致分为四类:
「1」字节操作:InputStream和OutputStream
「2」字符操作:Write和Reader
「3」磁盘操作:File
「4」网络操作:Socket
字节操作
基于字节的输入和输出操作的接口是:InputStream和OutputStream,分别表示输入和输出。
InputStream框架
InputStream(抽象类)
⊢ObjectInputStream
|构造:一个InputStream的实现类的对象
⊢PipedInputStream
|构造:PipedOutputStream对象可额外设定大小
⊢ByteArrayInputStream
|构造:一个byte的数组,用于缓冲
⊢FileInputStream
|构造:一个路径字符串,或是一个File的对象
|∟SocketInputStream
∟FilterInputStream.
构造:InputStream的实现类的对象
⊢BufferedInputStream
构造:InputStream的实现类的对象,或另加缓冲大小
⊢DataInputStream
构造:InputStream的实现类的对象 ∟InflaterInputStream
构造:InputStream的实现类的对象
∟ZipInputStream
构造:InputStream的实现类的对象,或另加一个字符类型
OutStream框架
OutputStream(抽象类)
⊢ObjectOutputStream
|构造:一个OutputStream的实现类的对象
⊢PipedOutputStream
|构造:PipedInputStream对象
⊢ByteArrayOutputStrea
|构造:数组的长度
⊢FileOutputStream
|构造:file的对象
|∟SocketInputStream
∟FilterOutputStream
构造:OutputStream的实现类对象
⊢BufferedOutputStream
构造:OutputStream的实现类对象
⊢DataOutputStream
构造:OutputStream的实现类对象
∟ZipOutputStream
构造:OutputStream的实现类对象或额外加一个字符
它们的每一个子类分别操作不同的操作类型。
「1」各种I/O可以叠加使用,比如:
public static void main(String[] args) throws IOException {
OutputStream outputStream = new BufferedOutputStream(
new FileOutputStream("index.txt"));
outputStream.write(0);
}
「2」使用Output的时候,要了解最最终写到了哪里,磁盘还是网络。
字符操作
基于字符的输入和输出操作的接口是:Printer和Writer,分别表示输入和输出。
Reader框架
Reader (抽象类)
⊢InputStreamReader
|构造:InputStream的对象
|∟ FileReader
|构造: File对象或加一个字符
⊢ BufferedReader
|构造:Reader的实现类的对象
⊢CharArrayReader
|构造:一个字符型数组
⊢FilterReader
|构造:file或增加一个字符
⊢PipedReader
|构造:一个PipedWriter对象
∟StringReader
构造:一个字符串
Writer框架
Writer (抽象类)
⊢OutputStreamWriter
|构造:OutputStream的对象
|∟FileWriter
| 构造:File对象或加一个字符
⊢BufferedWriter
|构造:一个Writer的实现类的对象
⊢CharArrayWriter
|构造:空,或数组长度
⊢FilterWriter
|构造:一个Writer的实现类的对象
⊢PipedWriter
|构造:一个PipedReader对象
∟StringWriter
构造:空,或字符串长度
一个使用Printer和Writer输入输出的例子:
import java.io.*;
import java.nio.charset.StandardCharsets;
// @ author :zjxu time:2018/12/1
public class charStreamDemo {
public static void main(String[] args) throws IOException {
Writer writer =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("index.txt")));
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream("index.txt"), StandardCharsets.*US_ASCII*));
writer.append("xxx");
writer.flush();
String string = reader.readLine();
System.out.println(string);
reader.close();
writer.close();
}
}
字符与字节的转化接口
InputStreamReader类是从字节到字符转化的桥梁。
OutputStreamWriter类是从字符到子节转化的桥梁。
StreamEnCoder完成了编码的过程。
磁盘的I/O工作的机制
几种文件访问的方式
为了加快访问的效率使用了缓存的形式对磁盘中的文件进行访问。
「1」标准访问文件的方式
缓存结构:(两层缓存)
< 1 > read和write在用户地址空间的应用缓存中读取和写入。
< 2 > 磁盘对内核地址空间中的高速缓存页缓存中读取和写入。
< 3 > 内核地址空间中的高速页缓存和用户地址空间的应用缓存这两者之间相互写入和读取。
「2」直接I/O的方式不使用高速页缓存,磁盘直接和用户地址空间的应用缓存I/O。
「3」同步访问文件的方式,同步操作。
「4」异步访问文件的操作
类似于多线程中的高并发的future模式,在给出访问的请求之后,线程会接着去处理别的内容,而不是阻塞等待。这样可以减少等待的时间,提高效率但是不是提高文件访问的效率,而是在等待的时间继续工作,提高时间利用率。
「5」内存映射的方式
将内存的某一区域和磁盘中的文件关联起来,当要访问内存中的一段数据的时候,转换为访问文件的某一段数据。
将内存中的缓存转化为文件,当要访问的时候,直接读取对应的文件就可以的想要的缓存的内容。
Java访问磁盘文件
File对象表示一个指向某个存在的路径的一个虚拟对象,并不代表这个文件。
将File文件传入FileInputStream,然后fileInput对象根据这个file对象的地址,去操作磁盘的文件。
Java的序列化技术
Java的序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数组来达到持久化的目的。
网络I/O的工作机制
TCP状态的转换
「1」CLOSED:起始点,在超时或者是连接关闭的时候进入这个状态
「2」LISTEN:server端在等待连接时的状态,server端为此调用Socket、bind、listen函数,就能进入这个状态。
「3」SYN-SENT:客户端发起连接,发送SYN给服务端。
影响网络传输的因素
「1」宽带网络:一条无力链路在1s内能传输的最大bit数,不是byte数。
「2」传输距离:光在光纤中是有传输时间的,这个距离越长,延时就越大。
「3」TCP拥塞控制:TCP是一个“停-等-停-等”的协议,传输方要和接收方步调一致,这样的同步要受到拥塞控制。
Socket的工作机制
可以看作是一个应用程序和TCP/UDP端口的一个桥梁,使用Socket将应用程序和TCP/UDP端口连接起来。使用socket指定端口,再通过IP和无力链路实现网络数据交换。
建立通信链路
客户端的Socket
「1」客户端建立socket实例。操作系统分配一个没有被使用的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字的数据结构,连接关闭删除。
「2」创建socket返回之前,实现TCP的三次握手,TCP握手协议完成之后,Socket完成创建。否则抛出IOException错误。
服务端的Socket
服务器端的ServerSocket创建比较简单。
操作系统也会为ServerSocket创建一个数据结构,这个结构里面包含一个端口号和监听地址的通配符‘*’,即监听所有的地址。
当调用accept方法的时候,进入阻塞状态,等待客户端的请求。
当请求到来的时候,为这个连接创建一个新的套接字数据结构,这个数据结构包含地址和端口信息并关联到serverSocket的数据结构中。
完成三次握手之后,创建客户端的Socket。
数据传输
每一个socket都有outputstrem 和inputstream ,通过它们 来交换数据。网络通道是使用字节通道来谈书数据的。在初三书数据的时候,会给OS 和 IS分配一个缓存区,读写都是通过缓存区完成的。
NIO的工作方式
BIO带来的挑战
BIO就是阻塞的IO,无论是磁盘还是网络,在IS 和OS的时候,都有可能发生阻塞。一旦阻塞,线程就失去的CPU的控制权。基于这样的考虑,我们需要一种非阻塞的IO机制。
NIO的工作机制
NIO的核心类有三个Buffer、Channel、Selector,是NIO中最重要的类。
将Selector比作一个车站的调度系统,负责监控每一个交通工具的运行状态。
将Channel比作拥有多个座位的交通工具,它会受到Selector的调度。
Buffer是一个缓存区。
Channel
是一个通道,InputStream 和 OutputStream相似,也是一个传输数据的通道。但是不同的是,在传统的意义上来说,无论是output和input,都是一个单向的通道。但是channel是一个双向的通道,input和ouput都能完成。
Buffer
是一个缓存区域,在NIO中读取和写入的数据都只能先暂时存放在缓存区Buffer中。
Buffer是一个顶层父类,基本的八种数据类型都有Buffer的类。如:IntBuffer、ByteBuffer、CharBuffer ······
对于网络的读写,使用的最广泛的是ByteBuffer。
Selector
轮询每一个注册的Channel,一旦发现有事件发生的话,就获取事件,进行处理。
Selector是NIO的核心类,Selector能够检测多个注册的通道上面的是否有事件发生,如果有事件发生,获取这个事件,然后处理这个事件。使用一个Selector来检测多个Channel,这样可以花费少量的资源控制大量的传输通道,并且只有在有事件发生的时候,才会调用函数处理这个事件。
client ---> Buffer ---> Channel <===> Channel ---> Buffer ---> server
箭头的指向为数据传输的方向
Buffer的工作方式
secket监测到通道有数据IO请求时,通过select方法取得,SocketChannel,将数据读取或写入buffer。
buffer是一个基本数据类型的元素列表。通过几个变量来保存这个数据的当前位置,也就是说有四个index。
「1」capacity:缓冲区数字的总长度。
「2」pasition:下一个要操作的数据元素的位置。
「3」limit:缓冲区不可操作的下一个位置的位置。
「4」mark:记录position前一个位置,default:0。
「1」创建方法:ByteBuffer.allocate(n) //创建一个长度为n的Byte的缓冲区。
这个时候的 capacity 和 limit的大小都是数组的长度;
position的大小是0,数组的首端。
「2」在写入了数据之后,position会变为数组没有存储数据的位置的一个位置。
「3」使用byteBuffer.flip方法,limit = position,position = 0,然后就可以正确的读取这鞋数据,并且将数据发送出去。
「4」然后使用 byteBuffer.clear方法,将会到刚刚创建的状态。
关于 mark 方法 ,使用mark方法的时候,mark会记录position - 1 的数据大小。当再次使用reset的时候,position将不会还原,会回到mark的值。
NIO数据的访问方式
在文件访问方面,NIO有两种优化方法:
FileChannel.transferTo & FileChannel.transferFrom 、FileChannel.map。
FileChannel.TransferXXX
传统的方面,物理硬盘和内核空间里面的高速缓存交互,但是是读写独立成块的,在高速内存里面并没有交流,它们都和应用的缓存单向数据交换。
使用FileChannel.TransferXXX的方法,就是在内核地址空间内,原本属于读写的两块区域可以在内核空间里面进行数据的交换。
FileChannel.map
这歌方法将文件按照一定的大小映射一段区域,当文件需要对这个文件进行操作的时候,直接对这个内存区域进行访问、操作就行了,不需要花费大量的时间对这个文件从内核空间向用户空间进行复制。
*尤其适合大文件的MD5 校验。
I/O调优
2.5.1 磁盘I/O调优
2.5.2 TCP网络参数调优
2.5.3 网络I/O优化
准则:减少网络交互的次数、减少数据传输量的大小。、尽量坚守编码。
设计方式:同步&异步、阻塞&非阻塞、两种方式结合
同步&异步
这里的同步和并发中的同步和异步还是有不一样的地方的。
同步指的是后一个任务需要依赖前一个任务的完成,这样的机制,保证了前后任务都完成。
异步指的是前一个任务在运行,通知后一个任务需要完成什么工作,这时候前后两个个任务同时运行,但是前一个任务对后一个任务的成功情况并不关心。
阻塞&非阻塞
阻塞就是说CPU会等待操作的完成,再执行别的任务。
非阻塞指的是CPU在等待操作的完成的时候,同时会执行别的操作。
非阻塞的操作可以提高CPU的工作效率, 但是,这样的机制会涉及到频繁的线程切换,这是额外的损耗。
基于同步&异步、阻塞&非阻塞,一共有四种排列组合的方式。
设计模式之适配器模式
适配器模式的结构
Target(目标接口):所需要转换的所期待的接口。
Adaptee(源角色):需要适配的接口。
Adapter(适配器):将目标接口配置成可以引用源角色的接口:继承目标接口,实现对源角色的引用。
适配器举例
InputStreamReader 和 OutputStreamWriter 分别继承了 Reader 和 Writer 接口。
但是在构造它们的时候,需要传入一个InputStream 和 OutputStream。
InputStreamReader 和 OutputStreamWriter的作用也就是将InputStream 和OutputStream的对象适配到 Reader 和 Writer 。
继承的是:目标接口;传入引用的是源角色。
在这个例子里面:
适配器是InputStreamReader;
源角色是InputStream;
目标接口就是Reader;
其实本质就是,目标接口可以实现的某些功能,在某些情况下,我的源角色也需要使用那些功能,这个时候就要将源角色的对象传入某个类中,并且要求这个类可以实现目标接口的功能。那么这个类就是适配器类。就是说目标接口带着源角色做目标接口才能做的事,这样的实现类就是适配器类。
设计模式之装饰器模式
装饰器模式的结构
Component(抽象组件):定义组件的功能。
ConcreteComponent(实现抽象组件):实现组件的功能。
(我的理解是上面这个部分和装饰器模式并没有关系,这是装饰器的对立面,即直接实现。)
Decorator(装饰器角色):持有一个对抽象组件的引用,并且实现了装饰器的接口。
ConcreteDecorator(实现装饰器):装饰器的实现者。
装饰器举例
InputStream:抽象组件;(抽象类)
FileInputStream:具体组件;(class)
FilterInputStream:装饰器角色;(class)
BufferedInputStream:装饰器实现者;(class)
在FilterInputStream里,引用了InputStream的对象,并且实现了InputStream的功能,它的作用是为所有的字节装饰类提供一个标准的借口。
BufferedInputStream是FilterInputStream的具体实现者,也就是说按照装饰器角色的标准,实现了这个装饰器,并且给InputStream添加了功能:将数据缓存在内存中。(其他的装饰器有别的不同的附加的功能)
适配器模式和装饰器模式
适配器指某个类的对象想实现某个接口的类。然后构建起这样的一个类,实现了该接口,并且将这个对象传入这个类,让这个对象也可以实现这个接口的方法。
而装饰器模式就是说,抽象组件需要一个统一的标准,在同一个基础类的条件下,对抽象组件的扩展。