深入分析Java I/O工作机制 学习笔记

来源:互联网 发布:graphic 软件 编辑:程序博客网 时间:2024/05/29 19:48

Java的I/O类库的基本架构

Java的I/O操作类在包java.io下,大概有将近80个类,这些类大概可以分成如下四组。

1.基于字节操作的I/O接口:InputStream和OutputStream。

基于字节的I/O操作接口输入和输出分别是InputStream和OutputStream,输入流根据数据类型和操作方式又被划分成若干个子类,每个子类分别处理不同操作类型,OutputStream的类层次结构也类似。

操作数据的方式是可以组合使用的,如这样组合使用:

OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"));

流最终写到什么地方必须要指定,要么是写到磁盘,要么是写到网络中。

写网络实际上也是写文件,只不过写网络还有一步需要处理,就是让底层操作系统再将数据传送到其他地方而不是本地磁盘。

2.基于字符操作的I/O接口:Writer和Reader

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以I/O操作的都是字节而不是字符。

但是为什么有操作字符的I/O接口呢?这是因为我们的程序中通常操作的数据都是字符形式的,为了操作方便当然要提供一个直接写字符的I/O接口。

我们知道字符到字节必须要经过编码转换,而这个编码又非常耗时,而且还会经常出现乱码问题,所以I/O编码问题经常是让人头疼的问题。

不管是Writer还是Reader类,它们都只定义了读取或写入的数据字符的方式,也就是怎么写或读,但是并没有规定数据要写到哪去,写到哪去就是我们后面要讨论的基于磁盘和网络的工作机制。

3.字节和字符的转化接口

数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或字节到字符的转化。InputStreamReader类是字节到字符的转化桥梁,InputStream到Reader的过程要指定编码字符集,否则将采用操作系统默认字符集,很可能会出现乱码问题。StreamDecoder正是完成字节到字符的编码的实现类。也就是当你用如下方式读取一个文件时:

try {

StringBuffer str = new StringBuffer();

Char[] buf = new char[1024];

FileReader f = new FileReader("file");

while(f.read(buf)>0) {

str.append(buf);

}

str.toString();

} catch (IOException e) {

}

FileReader类就是按照上面的工作方式读取文件的,FileReader继承了InputStreamReader类,实际上是读取文件流,然后通过StreamDecoder解码成char,只不过这里的编码字符集是默认字符集。

写入也是类似的过程,通过OutputStreamWriter类完成字符到字节的编码过程,由StreamEncoder完成编码过程。

基于磁盘操作的I/O接口:File

基于网络操作的I/O接口:Socket

前两组主要是传输数据的格式,后两组主要是传输数据的方式,虽然Socket类并不在java.io包下,但是笔者仍然把它们划分在一起,因为笔者认为I/O的核心问题要么是数据格式影响I/O操作,要么是传输方式影响I/O操作,也就是将什么样的数据写到什么地方的问题。

I/O只是人与机器或者机器与机器交互的手段,除了它们能够完成这个交互功能外,我们关注的就是如何提高它的运行效率了,而数据格式和传输方式是影响效率最关键的因素。


磁盘I/O工作机制

1.几种访问文件的方式

读取和写入文件I/O操作都调用操作系统提供的接口,因为磁盘设备是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作。读和写分别对应read()和write()两个系统调用。而只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题,这是操作系统为了保护系统本身的运行安全而将内核程序运行的内存空间和用户的内存空间隔离造成的。但是这样虽然保证了内核程序运行的安全性,但是也必然存在数据可能需要从内核空间向用户空间复制的问题。

如果遇到非常耗时的操作,如磁盘I/O,数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常缓慢。这时操作系统为了加速I/O访问,在内核空间使用缓存机制。

标准访问文件方式:

当应用程序调用read()接口时,操作系统检查内核的高速缓存中有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回,如果没有,从磁盘中读取,然后缓存在操作系统中

直接I/O方式:

应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区

同步访问文件方式:

数据的读取和写入都是同步操作的,它与标准访问文件方式不同的是,只有当数据被成功写到磁盘时才返回给应用程序成功标志。

异步访问文件方式:

当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。

内存映射方式:

操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中一段数据时,转换为访问文件的某一段数据。


2.Java访问磁盘文件

数据在磁盘中的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单元

java中通常的File并不代表一个真实存在的文件对象,当你指定一个路径描述符时,它就会返回一个代表这个路径的一个虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。在真正读取这个文件时会检查这个文件在不在。


3.Java序列化技术

将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。需要序列化,对象必须继承java.io.Serializable接口。反序列化则是相反的过程,将这个字节数组再重新构造成对象。

反序列化时,必须有原始类作为模板,才能将这个对象还原。序列化的数据并不能像class文件那样保存类的完整的结构信息。

序列化的文件二进制字节数据:

第一部分:序列化文件头,第二部分:要序列化的类的描述,第三部分:对象中各个属性项的描述,第四部分:输出该对象的父类信息描述,第五部分:输出对象的属性项的实际值,如果属性项是一个对象,那么这里还将序列话这个对象,规则和第二部分一样。

复杂对象情况总结:

当父类继承Serializable接口,所有子类都可以被序列化。

子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据会丢失),但是子类中属性仍能正确序列化。

如果序列化的属性是对象,这个对象也必须实现Serializable接口,否则会报错。

在反序列化时,如果对象的属性有修改或删减,修改的部分属性会丢失,但不会报错。

在反序列化时,如果serialVersionUID被修改,那么反序列化时会失败。


网络I/O工作机制

数据从一台主机发送到网络中的另一台主机需要经过很多步骤。首先需要有相互沟通的意向。其次要有能够沟通的物理渠道(物理链路),双方见面语言要能够交流,而且双方说话的步调要一致,明白什么时候该自己说话,什么时候该对方说话(通信协议)

影响网络传输的因素:

将一份数据从一个地方正确的传输到另一个地方所需要的时间我们称为响应时间,影响这个响应时间的因素有很多:

网络带宽:一秒钟一条物理链路最大能够传输的比特数。

传输距离:数据在光纤中要走的距离。

TCP拥塞控制:TCP传输是一个“停-等-停-等”协议,传输方和接受方的步调要一致,要达到这个步调一致就要通过拥塞控制来调节。


Java Socket的工作机制

Socket这个概念没有对应到一个具体的实体,它描述计算机之间完成相互通信的一种抽象功能。

主机A的应用程序要能和主机B的应用程序通信,必须通过Socket建立连接,而建立Socket连接必须由底层TCP/IP协议来建立TCP连接。建立TCP连接需要底层IP协议来寻址网络中的主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过TCP或UDP的地址也就是端口号来指定。这样就可以通过一个Socket实例唯一代表一个主机上的应用程序的通信链路了。


建立通信链路

当客户端要与服务端通信时,客户端首先要创建一个Socket实例,操作系统将为这个Socket实例分配一个没有被使用的本地端口号,并创建一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。在创建Socket实例的构造函数正确返回之前,将要进行TCP的三次握手协议,TCP握手协议完成后,Socket实例对象将创建完成,否则将抛出IOException。

与之对应的服务端将创建一个ServerSocket实例,创建ServerSocket比较简单,只要指定的端口号没有被占用,一般实例创建都会成功。同时操作系统也会为ServerSocket实例创建一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下都是‘*’,即监听所有地址。之后当调用accept()方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新创建的数据结构将会关联到与之对应的ServerSocket实例的一个未完成的连接数据结构列表中。注意,这时服务器端的与之对应的Socket实例并没有完成创建,而是要等到与服务器端的三次握手完成后,这个服务端的Socket实例才会返回,并将这个Socket实例对应的数据结构从未完成列表中移到已完成列表中。

所以ServerSocket所关联的列表中每个数据结构都代表与一个客户端建立的TCP连接。



0 0
原创粉丝点击