[I/O]javaI/O工作机制

释放双眼,带上耳机,听听看~!

  摘要:IO问题可以说是当今web应用中面临的主要问题之一。因为在这个数据爆发的时代,海量的数据在网络到处流动,而在这个过程中都会涉及IO问题,可以说IO问题已经成为web应用的瓶颈之一。如何优化?以此提高效率,了解IO的工作机制就显得尤为重要了。

一、概述

  java的io类库在java.io包下,大概将近80个类,按照功能大致可以分为一下4组:

    • 基于字节操作的IO接口:InputStream/OutputStream
    • 基于字符操作的IO接口:Writer/Reader
    • 基于磁盘操作的IO接口:File
    • 基于网络操作的IO接口:Socket

  前两组按照数据传输的格式划分,后两组按照数据传输的方式划分。(虽然Socket不是java.io包下类库,但是它和数据的传输方式息息相关,因此在此一起分析),输入根据操作类型和操作方式又被分为若干个子类。由于本篇不是讲解IO的具体操作,而是站在更高的抽象位置分析java IO 的工作机制,所以就不讲如何使用了(后续学习源码了之后再分享)。但是需要说明的是,java IO 不仅仅是操作本地磁盘的文件,也可以把网络传输作为数据的目的地。不管是磁盘传输还是网络传输,最小的存储单元都是字节,而不是字符。因此java IO操作的也是字节,之所以有字符的操作接口,完全是为了方便我们在程序中的使用,因为我们平时处理的多是字符类型的数据。底层存储的依然是以字节为单位。由于字节到字符的的转换涉及到编码问题,所以我们在使用的过程中一定注意字符编码的问题。下面就分别以java本地IO和网络传输IO分析其原理和工作机制。

二、磁盘I/O工作机制

2.1 从操作系统讲起——程序访问文件的几种方式

  程序关于IO的操作——读写文件都是都是通过系统调用的方式来工作的,因为磁盘设备是由操作系统(OS)维护的,读写文件通过系统调用的read()和write()方式,但是只要是系统调用,就会存在内核空间和用户空间地址的转换和数据的复制,如果IO请求很大的时候,就会造成系统缓慢,为此操作系统引入了缓存的概念。以此提高IO的响应时间。那么访问文件的方式有哪些呢?一一道来。

a)标准访问文件的方式

[I/O]javaI/O工作机制

  当程序调用read() 接口时,OS检查缓存中是否有目标数据,有直接返回,没有的话从磁盘读取并放入缓存。write()的时候,OS将数据从用户空间复制到内核空间的缓存中,此时对于用户来说写入已经完成,接下来由OS决定何时持久化,如需立即同步,显示调用sync;

b)直接IO的方式

[I/O]javaI/O工作机制

  程序直接访问磁盘数据而不经过OS内核的高速缓存,减少了用户空间到内核空间的复制,这样的应用的程序通常是自身控制数据的缓存机制,它自己知道哪些数据应该内缓存,哪些不用。甚至可以提前把热点数据加载到内存中,以此来提高访问的效率。典型的实现是数据库管理系统。

c)同步访问文件的方式

[I/O]javaI/O工作机制

  数据的读取和写入都是同步的,它与标准方式访问文件不同的是,只有当数据成功持久化到磁盘后才返回,这样大大的降低了系统性能只有对数据安全性考虑较高的情况下使用。

d)异步访问文件的方式

[I/O]javaI/O工作机制

  当数据访问的线程发出后,线程转而去做其他的事情,而不是阻塞等待。这种方式可以明显的提高系统的效率,但是不能根本的解决数据访问的问题。

e) 内存映射访问的方式

[I/O]javaI/O工作机制

  OS将内存中的某一块区域与磁盘的文件相关联起来,将访问内存中的数据转换为访问文件的数据,这种方式减少了数据复制的操作。

2.2 java访问磁盘文件

  java访问磁盘文件都是基于java.io包下定义的对数据操作的接口,以一个程序从磁盘读取一段文本字符为例,说明java访问磁盘数的过程:当传入一个文件路径时,根据这个路径来创建一个File对象来标识这个文件,然后根据这个File对象创建真正读取文件的操作对象,这时会创建一个和真实磁盘文件关联的文件描述符FileDescriptor对象,通过这个文件描述符对象可以直接控制磁盘文件,由于我们读取的是字符格式,因此需要一个StreamDecoder对象来将byte转换为char。再往下就是如何从磁盘驱动器访问数据了,这一步由操作系统完成,不同的操作系统有不同的实现。

 [I/O]javaI/O工作机制

代码示例:

public class ReadFileDemo {

    public static void main(String[] args) {

        // 根据传入的路径 创建一个File对象来标识这个文件
        File file = new File(\"D:/test.txt\");

        // 缓存数组,防止文件过大
        char[] c = new char[1024];

        Reader reader = null;
        StringBuffer str = new StringBuffer();

        try {
            /**
             * 当我们创建 FileInputStream 对象真正操作文件时,才真正关心文件的有效性 因此抛出运行时异常 FileNotFoundException
             * 根据File对象创建一个真正操作文件的对象,此时会创建一个FileDescriptor文件描述符,可以直接操作磁盘文件 
             */
            // 由于我们访问的是字符格式数据,因此此时会创建一个StreamDesciptor对象转换数据格式
            reader = new InputStreamReader(new FileInputStream(file));   
            // 真正的开始读取文件时。也会抛出 IOException
            while (reader.read(c) != -1) {  // 通过 FileDescriptor 访问文件
                str.append(c);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null)
                    reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        System.out.println(str);

    }

}

 

 2.3 java序列化技术

  java序列化是将一个对象转换为二进制表示的字节数组,通过保存或者转移达到对象持久化的目的。某个java对象需要序列化,那么需要实现java.io.Serializable 接口,反序列化则是相反的过程,但是我们需要有原始类作为模板,才能将对象反序列化。但我们在应用中使用序列化技术时,需要注意几个问题:

    • 当父类继承了Serializable 接口,那么它的子类都可以被序列化。
    • 当子类实现了Serializable 接口,但是父类没有实现时,父类的属性不能被序列化(不会报错,但是数据会丢失),子类属性可以正常序列化。
    • 如果序列化的属性是对象,那么这个对象也必须实现Serializable 接口,否则会报错。
    • 在反序列化时,如果对象的属性有变化或者修改,则修改的部分属性会丢失,但是不会报错。
    • 在反序列化时,如果序列化的ID修改了,那么反序列化也会失败。

  在纯java环境中,序列化可以很好地工作,但是在多语言环境下,java序列化的对象用其他语言很难还原,因此尽量使用通用的数据结构,如json,xml,或者其他的序列化技术。

三、网络IO工作机制

  数据从一台主机发送到网络中的另一台主机的过程,首先需要和目标主机建立起连接,和使用相同的通信协议和交流语言,才能完成数据的传输。因此在介绍java 中Socket传输数据之前,需要先讨论一下网络。如何建立和关闭一个TCP连接,TCP的连接状态转换有哪些?

3.1 TCP连接状态转换

 [I/O]javaI/O工作机制

序号 状态 描述
1 CLOSED 起始点,连接超时或者连接关闭时,进入此状态。
2 LISTEN Server端在等待连接时的状态,Server端为此要调用Socket,bind,listen。称为被动打开。
3 SYN-SENT Client发送SYN给Server,如果不能连接那么直接进入CLOSED状态。
4 SYN-RCVD 与3对应,Server端接收Client的SYN,由LISTEN变为此状态,并给Client回应一个 ACK,一个SYN.另外一种情况是Client在等待Server的ACK的时候,同时受到Server的SYN,那么Client也会进入此状态。
5 ESTABLISHED Server与Client在完成三次握手后进入的状态,此时说明已经可以传输数据了。
6 FIN-WAIT-1 主动发起关闭的一方,由状态5进入此状态,具体动作是发送FIN给对方
7 FIN-WAIT-2 主动关闭的一方,在收到对方FIN-ACK后进入此状态。此时不能再接收对方的数据,但是可以向对方发送数据
8 CLOSED-WAIT 收到FIN后,被动关闭的一方进入此状态,动作是收到FIN的同时给对方回应ACK
9 LAST-ACK 被动关闭的一方,发起关闭请求,由8进入此状态,动作是发送FIN给对方,同时在接收到ACK时进入CLOSED状态
10 CLOSING 两边同时发起关闭请求时,会由FIN-WAIT-1进入此状态,动作是收到FIN请求,同时响应一个ACK.
11 TIME-WAIT 可由三种状态转换为此状态。

 

哪几种情况会让TCP连接进入TIME-WAIT状态?

  1. 双方不同时发起FIN的情况下,由FIN-WAIT-2 转换为TIME-WAIT,主动关闭的一方在完成自身发起的请求后,接收到被动一方的FIN后进入的状态。

  2. 双方同时关闭的情况下,由CLOSING转换到TIME-WAIT,双方都发起了FIN,同时接收到了FIN并回应ACK的情况下,进入TIME-WAIT状态。

  3. 同时收到FIN和ACK,它与上面情况不同的是本身发起的FIN回应的ACK先于对方的FIN请求到达。

3.2 影响网络传输的因素

  将一台主机中的数据传到网络中的另一台主机锁需要的时间我们叫做响应时间,影响这个响应时间的因素有很多,常见的有如下几种:

a).网络带宽:带宽就是一条物理链路在1s内能够传输的最大比特数。注意是比特,而不是字节。

b).传输距离:传输的距离,国内和国外肯定是不一样的。

c).TCP拥堵控制

四、java Socket的工作机制

  Socket的概念没有对应到一个具体的实体,它描述的是计算机之间相互通信的一种抽象功能。一般情况下,我们使用的 Socket都是基于TCP/IP的流套接字,它是一种稳定的通信协议。下图是一个典型的基于Socket通信的场景。

[I/O]javaI/O工作机制

  上图中主机A要和主机B建立通信,必须通过Socket建立通信,而在Socket连接底层是基于TCP/IP的TCP连接,TCP连接通过IP在网络中寻找目标主机,主机上不同的应用程序又通过本地端口号来区分,那么基于套接字(192.168.10.2:8080)这样的ip:port 创建的唯一Socket就代表主机上一个应用程序的通信链路了。

4.1 建立通信链路

  当Client需要与Server通信的时候,Client首先建立一个指定套接字的Socket示例,此时操作系统将为这个Socket示例分配一个包含本地地址,远程地址,端口号的数据结构,这个数据结构将一直保存到当前连接关闭。在创建Socket示例的构造函数正确返回前,会进行TCP的3次握手协议,握手完成后,正确返回Socket,否则将抛出异常IOException;

  与之对应的Server将创建一个ServerSocket示例,创建ServerSocket比较简单,只要保证指定监听的端口号没有被占用,一般都没有问题。同样的操作系统也会其分配一个数据结构(但是不同的是,在没有Client连接之前,该数据结构只包含了监听的端口号和匹配的地址符)。然后调用accept()进入阻塞状态,等待Client的连接;当Client请求的时候,此时操作系统为该连接实例分配一个数据结构并放在未完成的数据结构列表中,注意,此时的套接字数据结构并没有完成,还需要等到Client的3次握手完成后,Server端的Socket实例才能返回,并从未完成的列表中移动到已经完成的列表中。所以ServerSocket关联的列表中,每一个数据结构都代表了一个Client的连接实例。

4.2 数据传输

  建立连接的目的是传输数据,那么连接成功后,数据传输的基本流程和方式是在怎么样的呢?

  当连接成功后,Server和Client都有都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过通过这两个对象来交换数据。同时网络IO是以字节流来传输数据的,当创建Socket对象时,系统会为InputStream和OutputStream分配一个缓冲区,数据的写入和读取都是通过这个缓冲区完成的。写入端将数据写入到OutputStream的SendQ队列中,当队列满时,数据将被转移到另一端InputStream的RevcvQ中,如果这个RecvQ也满了,那么OutputStream将会的write方法将会阻塞,知道RecvQ队列有空闲的空间容纳SendQ发送的数据。需要注意的是,这个缓冲区的大小及写入端和读取端的速度直接影响了这个连接的传输效率,由于可能会发生阻塞,所以网络IO和磁盘IO最大的不同就是数据的写入和读取还需要一个协调的过程。如果两边同时传输数据就很可能会造成死锁。NIO解决了数据传输阻塞的问题。

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

【前端必备】七、页面性能优化

2020-11-9 4:04:02

随笔日记

Spring Boot Web 自定义注解篇(注解很简单很好用)

2020-11-9 4:04:04

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索