1、文件基础技术

IO熟悉而又陌生的话题,由于项目特性,可能很少用到流处理,文件读写。

或者经常使用封装好的API早就忘记了原始的配方原始的味道。

万变不离其宗,还是有必要系统的回顾下IO相关知识。

我们更多时间再处理JSON,VO。

无论使用什么对象,什么语言,其实本质未曾变过,核心还是计算机原理,编码技术,序列化。

也离不开:磁盘、内存、网卡、键盘、显示器、CPU、操作系统等基础信息,本质还是010101。

以下都是皮毛,都是为了Java Io做一些概念上的准备工作,而作为一个程序员必须要对服务器文件系统和对应命令有深入了解,更要知道彼此之间的差异

例如:centos、Ubuntu、欧拉之间的差异,以免写自动化脚本不适配。

VIM命令可以在OS上修改Jar中配置文件的配置信息的,知道之后服务器上调试一些配置就方便很多了(只对配置文件打入jar包的有效,对于引入配置中心配置,或者conf目录独立出来忽略)

1、文件概述

1.1、基本概念和知识

1.1.1、二进制思维

  • 程序员必须要有二进制思维

  • 所有文件都是以0和1的二进制形式保存的。

  • 看到结果图像、视频,文本都是应用程序对二进制解析的结果。

  • 我们应该有工具查看文件的二进制形式。

1.1.2、文件类型

  • 数据类型概念- 数据都是以二进制形式保存的,但是为了方便处理数据,高级语言引入了数据类型概念。

  • 文件类型-文件处理类似,所有文件都是以二进制形式保存的,但是为了便于理解和处理文件,文件也有文件类型的概念

  • 扩展名-大多数啊况文件类型是通过扩展名的形式体现的

  • 格式- 有些事公开的,有些是私有的,我们也可以自定义文件格式

每种文件类型都有一定的格式,代表着文件含义和二进制之间的映射关系。

这个知识点很重要,做文件类型安全校验(从流中公开的特殊标志判断)

扩展名不是必须的,也未必是可靠的,我们可以随便改变扩展名,其实本质上还是二进制的内容以及定义的格式是否真正的正确。

我们可以把JPG改成TXT,但是打开会报错,一堆乱码。

  • 文件类型可以大致分为两类:文本文件(字符类型的人类可读的文件),二进制文件(压缩文件,MPS,EXCEL等)

文本文件可以直接用文本编辑器打开,LINUX上可以用VI、 VIM,每个字节表示字符

二进制文件中,每个字节就不一定表示字符了,可能是字体,声音等。

1.1.3、文本文件的编码

文本文件基本都是可打印字符,但是字符到二进制的映射(即编码)却有多种方式,如UTF-8、GB18030等等。

编码必须作为核心知识,程序员必须掌握的基础,和计算机原理,数据结构一样的内功。

编码往往是未知的,大多采用程序或者操作系统默认编码格式。

例如UTF-8,头部加入BOM-byte order mark, 0xEF 0xBB 0xBF 作为标记识别。

但是不是所有的应用程序都支持带BOM头。

二进制思维:不仅要看文件的显示,还要看文件背后的二进制。

文件存储是要知道换行符等等都是01数据,存储结构和看到的显示结果是两码事。

  • windows系统换行符默认一般是“\r\n”,即ASCII码的13,10.

  • Linux系统中,换行符一般是一个字符“\n”.

所以一些做自动化的系统,编排脚本要做适配,否则未必可执行。

1.1.4、文件系统

文件一般存储在硬盘上,一个机器会有多个硬盘,而操作系统会隐藏物理硬盘概念,提供逻辑上的结构

这个思维必须要有,尤其做OS层的开发,和自动化,感知类的项目,和window和Linux长期打交道是必须的。

  • Windows一般会被分为 C\D\E\F等盘符(可能物理硬盘是一块或多块),每个盘为被格式化为不同的文件系统,目前常用的是NTFS,也有旧版的FAT32。

  • Linux只有一个逻辑根目录/ ,Linux支持文件系统有 Ext2/Ext3/Ext4

    作为Java开发程序员,往往需要做系统加固,防止入侵。而在做数据盘挂载的时候,如果填错了文件系统类型,往往导致系统无法启动。

文件系统是文件的组织方式、结构和特点,高级编程语言和类库一般为我们提供了API,我们不需要关注细节,但是要知道有这么回事。

有了根目录了,文件组织起来九幽了文件结构树,从而每个文件都有了文件路径的概念:

  • 绝对路径(从系统文件系统根目录开始):如 /usr/local/openresty/nginx/lua/GetJob.lua
  • 相对路径如:../lua/GetJob.lua

相对路径是相对比较复杂的东西,前提是你必须知道启动程序的运行路径。

currentPath是核心概念,程序员必须对自己运行程序的当前路径了如指掌,如go的启动路径,python脚本的运行路径,lua脚本的路径,shell脚本的运行路径。

做一些云服务的时候,往往先对路径的自动化会让用户懵逼,为什么我自定义的脚本无法执行我上传的插件。

File.separator路径分隔符要理解,因为window和linux分隔符不一样,windows是/ Linux是\ 用File.separator可以规避硬编码和

System.getProperty("user.dir"),可以得到运行Java程序的当前路径,System这个类Java核心类,应该多读读源码,对于容器类的工作,时间戳的使用,系统环境配置数据获取都有很有价值。

为了安全Jar包配置信息往往需要做工作秘钥加密,而工作秘钥不想放到启动参数中明文显示方式ps -ef看到,怎么办,可以配置到profile。java也可以自动读取,原理就。

文件元数据:

做自动化的时候会经常遇到一些奇葩问题,手工搞的脚本怎么调试都没问题,通过Java下发的脚本怎么执行都不行。

可能存在如下问题:先对路径不对,编码格式不对,无执行权限,所属用户不对等等。

文件是由很多属性的,而这些属性就是文件的元数据。

这也是为什么送检会做文件属性的修改如果不能使用777,必须设置为750,所属不能是root必须改为自己的程序账号例如gaiaadmin等等。

  • 文件名
  • 创建时间
  • 修改时间
  • 文件大小
  • 是否隐藏:windows是一个属性可以设置,linux就是 .filname 文件名前面多个点,通过ls -a才能看到隐藏文件或者文件

访问权限控制

这个就是安全的核心思想,没有这点意识的人,开发的东西是裸的,一攻就破。

文件系统的文件和目录最基本的控制是访问权限控制,对于所有者和用户组可以有不同的权限,具体权限包括读、写、执行。

Java人员开发的程序部署之后,必须确保可执行jar包、配置文件、日志文件等必须在自己的程序账号下,甚至有自己的用户组,并且chmod好对应的读、写、执行权限。这是基本的意识和一个程序员入门的保障。

当然作为大公司并不是每个开发人员亲力亲为,例如HW,BAT等微服务的部署都是容器化,CICD线上化了,但是总离不开VM的手工部署服务,例如tegraf,ansible等一些agent工具。

如果oracle账号,或者一个黑客植入的只读账号都可以读取你的配置信息,influxdb中的数据,那就凉凉了。

大小写敏感

windows往往大小写不敏感,Linux大多数大小写敏感。这个一定要知道,最好面对不同版本的操作系统要做好debug。

不然你配置的nginx,或者写一些脚本如ps1,python等可能出现未知问题。

临时文件概念

操作系统有一定的策略自动清理不用的临时文件

Linux 临时文件目录是 /tmp

Windows cmd命令 echo %tmp%就可以查看

/tmp linux这个临时命令非常有效,尤其一些CICD类的自动化项目,往往会使用临时文件执行一些操作。

如安全自动化,运维自动化,自动化部署,感知等等系统,会通过tmp目录下载执行一些中间的shell,插件或者jobs

1.1.5、文件读写

image

这个是IO的核心了,上面是文件的组成和操作系统的映射关系,基于此基础上才能了解文件读写的正确姿势。

  • 文件是放在硬盘上的
  • 程序要处理文件需要将文件读入内存
  • 处理完后需要写会硬盘
  • 操作系统提供了对文件读写的基本API,不同操作系统的接口和实现不同
  • Java封装了操作系统的功能,提供了统一的API

有个基本意识:硬盘的读写速度,相对于内存是很慢的(SSD磁读写改成了电子闪存颗粒读写得到几何级的提升)

新建议一个文件,输入一个字节的信息,会看到磁盘占用不止1个字节,为什么?

操作系统在和硬盘之间的传输不是按照字节来的,而是按照快批量传输的,为了延迟开销,块大小一般>512字节。

磁盘的文件存储也是以块为单位的。

基本常识:一般读写文件需要两次数据copy。

image-20220918105445930

应用程序所在环境是用户态,操作系统所在环境是内核态,应用程序调用操作系统的功能,需要两次环境的切换,先从用户态切换到内核态,再从内核态切换到用户态。切换是有开销的。

缓冲区:为了提高文件操作效率,应用程序经常使用这种思维模式,引入缓冲区。

缓冲区满了一次性调用操作系统写到硬盘,但是要记住最后一次操作要清空缓冲区,就算不满。内存是稀缺资源。

打开和关闭:这是文件操作的核心概念。

  • 打开:操作系统会建立一个有关该文件的内存结构,这个结构一般通过一个整数索引来引用,这个索引一般称为文件描述符

    这个是消耗内存的,操作系统能同时打开的文件数也是有限的。

    这就是实际项目中经常遇到的open more file的问题,df -h看磁盘没有存储满,但是操作文件一样无法读写,就因为操作系统打开的文件满了。

  • 关闭:

    关闭文件一般会同步缓冲区的内容到硬盘,把并释放占据的内存结构。

内存映射文件:

上面打开关闭文件是标准模型,应为应用程序不能直接读写文件,需要经过操作系统,所以开销都是双倍的。

为了降低开销其实操作系统再不断的迭代过程中引入了预处理等各种机制来包装,

其中内存映射文件就是一种高效的随机读写大文件的方法,将文件直接映射到内存,操作内存就是操作文件。

在内存映射文件中,只有范根到的数据才会被实际复制到内存,且数据只会复制一次,被操作系统以及多个应用程序共享。

1.2、Java文件概述

以上算是操作系统中文件系统的基础概念,作为Java开发程序员,还要了解在此基础上Java对文件的一些基础设计思路。

(离开了计算机原理,所有的高级语言技术都是死记硬背)

Java最核心的思维模式就是抽象:

在Java中处理文件不叫处理文件,而是引入了一些抽象的概念和类,包含流、装饰器设计模式、Reader\Writer、随机读写文件、File、NIO、序列化和反序列化。

Java8 SE 还引入steam的概念,应该和这个归并到一起。

1.2.1、流

高级语言处理文件,一般不是单独处理的,而是视为输入输出(Input/Output,IO)设备的一种。

Java使用基本统一的概念处理所有的IO,包括键盘、显示终端、网络等等。

这个统一的概念就是流

  • 输入流:可以从中获取数据,提供者可以是键盘、文件、网络等等
  • 输出流:可以向其中写入数据,目的可以是显示终端,文件,网络等等。

Java IO的基本类大多位于java.io中。

  • 类InputStream表示输入流
  • OUtputStream表示输出流
  • FileInputStream表示文件输入流
  • FileOutputStream表示文件输出流

以后的编码对应功能的实现,就变成了面向流编程,例如对流进行加密,压缩,计算信息摘要,XSS校验等等。

Java核心是抽象,统一操作,Java确实也是这么干的,把一些类似的操作也向面向流编程进行了靠拢,统一协作。

例如字节数组,也被包装成了流ByteArrayInputStream和ByteArrayOutputSteam。

我们知道HttpRequest对象中的流只能使用一次,例如文件上传,如果我们为了防止入侵,把流读取出来做二进制的校验,可能那么文件上传下载后续操作就null了。所以就可以通过流copy技术。

1.2.2、装饰设计模式

外包无技术还体现在一点,代码面向功能实现而很少有编程思想,而编程思想是技术的核心内功。

基本的流按字节读写(为啥不是按位,位是存储模式而程序数据处理的基本单位,数据在计算的本质往往是01,01往往是通过磁性正负极,高低电压和光信号表示,最小单位就是位,但是程序处理数据一般都是按照数据类型来的,一般都是8位一个字节的逻辑单元起步),没有缓冲区,很不方便使用。

Java为了解决这个问题的方法就是装饰器设计模式,引入比较多的装饰类,对基本的流增加功能,以方便使用。例如:

  • FilterInputStream 过滤器输入流
  • FilterOutputStream 过滤器输出流

过滤器不改变本质,如水管就算你加了净水器输入的是水,输出的还是水,和工厂模式不同,输入的是水出来的是牛肉汤。

看下对应的子类:

  • 对流起缓冲装饰的子类:BufferedInputStream和BufferedOutputStream

  • 可以按照8种基本类型和字符串对流进行读写的子类是DataInputStream和DataOutputSream。

  • 可以对流进行压缩和解压缩的子类有GZIPInputSteam、ZipInputStream、GZIPOutputSteam、OutInputStream

    做ZIP炸弹检测的时候会用到

  • 可以将基本类型、对象输出为其字符串表示的子类有PrintStream

1.2.3、Reader/Writer

流处理大多都是以二进制形式处理数据的,处理文本文件不方便。

InputStream和OutputSream无编码概念,能够方便地按照字符处理文本数据的基本类就是Reader和Writer。

编码不同字符映射成二进制长度是不同的,但是按照字符就处理纯文本文件就方便太多了。

看下这个高效的读写器的子类:

  • FileReader和FileWriter 读写文件
  • BufferedReader和BufferedWriter 起缓冲装饰
  • CharArrayReader和CharArrayWriter字符数组的包装类
  • StringReader和StringWriter 字符串包装类
  • InputStreamReader和InputStreamWriter
  • PrintWriter 将基本类型,对象输出为其字符串表示。

1.2.4、随机文件读写

RandomAccessFile,适用于大小已知的记录组成的文件。

1.2.5、File

以上都是操作文件内容的,但是对于文件自身的操作需要的File类

1.2.6、NIO

除了java.io包,java还有个java.nio包,nio表示New IO。

NIO代表一种不同的看待IO的方式,它有缓冲区和通道的概念。

它们更接近操作系统的概念,某些操作的性能也更高。

例如可以利用DMA机制,不通过CPU直接将数据从硬盘复制到网卡。

NIO还提供了比较底层的功能:

  • 内存映射文件
  • 文件枷锁
  • 自定义文件系统
  • 非租塞式IO
  • 异步IO等

1.2.7、 序列化和反序列化

简单说,序列化就是将内存中的Java对象持久保存到一个流中,反序列化就是从流中恢复Java对象到内存。

Java主要通过接口Serializable和类ObjectInputStream和ObjectOutputStream提供对序列化的支持。

为啥实际项目基本用不到,因为这个玩意Java自己独有,不便于不同语言应用见得通讯,序列化的本质是数据结构。

所以就有了后来的XML和JSON。

posted @ 2022-09-16 22:53  红尘过客2022  阅读(151)  评论(0编辑  收藏  举报