Java IO 之 InputStream OutputStream & BufferedReader BufferedWriter & FSDataInputStream FSDataOutputStream

一、InputStream和OutputStream

(一)说明

InputStreamOutputStream是Java标准库中最基本的IO流,它们都位于java.io包中,该包提供了所有同步IO的功能。

  • java.io.InputStream 、 java.io.OutputStream

(二)用法

1.read&write

InputStream和OutputStream都不是接口,而是抽象类,它们分别是所有输入流和输出流的超类。这两个抽象类定义了两个最重要的方法:

  • int read():读取输入流的下一个byte,返回该byte表示的int值(0~255)。读到末尾,返回-1表示不能继续读了。
  • void write( int b ):写一个byte到输出流。虽然传入的是int,但只会写入1 byte,即该int的最低8位表示字节的部分( b & 0xff )
public abstract int read() throws IOException;
public abstract void write(int b) throws IOException;

2.实现类

(1)FileInputStream:从文件流中读取数据

下面的代码演示了如何完整地读取一个FileInputStream的所有字节:

public void readFile throws IOException{
    //创建一个FileInputStream对象
    InputStream input = new FileInputStream("src/readme.txt");
    for(;;){
        int n = input.read(); //反复调用read(),直到返回-1
        if(n==-1){
            break;
        }
        System.out.println(n);//打印byte值
    }
    input.close();
}

在计算机中,应用程序如果打开了有一个文件进行读写,完成后要及时关闭,以便OS把资源释放。

(2)FileOutputStream:将若干字节写入文件流

下面代码演示了如何将多个bytes写入文件流:

public void writeFile() throws IOException{
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write(72);//H
    ...
    output.write(111);//o
    output.close();
}

每次写入1byte太麻烦,更常见的是一次性写入多个bytes。可以通过OutputStream提供的重载方法void write( byte[ ] )来实现:

public void writeFile() throws IOException{
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write("Hello".getBytes("UTF-8"));
    output.close();
}

3.close

在操作完之后,InputStream和OutputStream都需要通过close()来关闭流,之后OS会释放底层资源。

4.flush

只用于OutputStream,目的是将buffer内容真正输出到目的地

因为写的时候,出于效率考虑,OS不是每次直接把1 byte写入文件或发送到网络,而是先放到内存buffer(本质上是byte [ ]数组),等到buffer写满,再一次性写入文件或网络。

对所有IO设备来说,一次写1B或1000B,花费的时间几乎相同,所以OutputStream有flush()方法能强制把buffer内容输出

(1)调用时机

通常情况下,我们不需要调用这个flush(),因为

  1. buffer写满了OutputStream会自动调用它;
  2. 调用close()关闭OutputStream之前,也会自动调用flush()方法。

但是某些时机我们需要手动调用flush(),比如在实现一个即时通讯软件时,用户发一句就要flush一句,而不能等待buffer满。

(2)InputStream buffer

InputStream也有buffer。当从FileInputStream读取1 byte时,OS往往会一次性读取多个 bytes到buffer,read时直接返回buffer中的byte,而不是每次都IO 1 byte。

5.IOException

读写IO流时,可能发生错误,例如文件不存在、权限异常,它们由JVM封装为IOException抛出。

因此,所有与IO相关的代码都必须处理IOException

实际使用时,为了避免读取时发生IO错误,IO流无法正确关闭,资源也无法及时释放,所以我们要用try...finally来保证IO流正确关闭:

public void readFile() throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("src/readme.txt");
            int n;
            while ((n = input.read()) != -1) {
                System.out.println(n);
            }
        } finally {
           if (input != null) {
                input.close();
            }
        }
    }

用try...finally来编写会感觉复杂,更好的写法是利用Java 7引入新的try(resource)语法,只需要编写try语法,就能让编译器自动为我们关闭资源。推荐写法如下:

//InputStream
public void readFile() throws IOException{
    try(InputStream input = new FileInputStream("src/readme.txt")){
        int n;
        while((n=input.read())!=-1)
            System.out.println(n);
    }//编译器在此自动为我们写入finally并调用close()
}

//OutputStream
public void writeFile() throws IOException{
    try(OutputStream output = new FileOutputStream("out/readme.txt")){
      output.write("Hello".getBytes("UTF-8"));//Hello
    }
}

同时操作多个资源时,在try(resource){ ... }中可以同时写出多个资源,用;隔开。例如同时读写两个文件:

        //读取input.txt,写入output.txt
        try(InputStream input = new FileInputStream("input.txt");
        OutputStream output = new FileOutputStream("output.txt")){
            input.transferTo(output);
        }

不过实际上,编译器并不会特别为InputStream加上自动关闭。只看resource是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally并调用close()方法。InputStream、OutputStream都实现了这个接口,因此都可以用在try( resoucrce )中。

(三)缓冲

IO流中,每次用read()、write()读写1 byte未免太麻烦了。IO流各提供了两个重载方法来支持读写多个字节:

  • read( byte[ ] b ):从输入流中读取一定数量的bytes,并将之存入缓冲数组b中;
  • read( byte[ ] b , int off , int len ):从输出流中off byte处开始读取len bytes到缓冲数组b中;
  • write( byte[ ] b ):将b中的所有byte写入输出流中;
  • write( byte[ ] b , int off , int len ):从b中的off处开始,写入len bytes到输出流中。

利用read方法一次读取多个bytes时,需要先定义一个byte[ ]作为buffer,read会尽可能多的读取byte到buffer,但不会超过buffer大小。read返回实际读取的byte数,如果返回-1,表示没有更多的数据了

read

用buffer一次读取多个bytes的代码如下:

public void readFile() throws IOException{
    try(InputStream input = new FileInputStream("src/readme.txt")){
        byte[] buffer = new byte[1000];
        int n;
        while((n=input.read(buffer))!=-1)//读取到buffer
            System.out.println("read "+n+" bytes.");
    }
}

(四)阻塞

之前我们说过java.io是同步io在进行IO时,会中断程序运行,直到数据IO完毕——此之谓阻塞

(五)其他InputStream实现类

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。

1.ByteArrayInputStream

在内存中模拟一个InputStream:

public class Main {
    public static void main(String[] args) throws IOException {
        byte [] data ={ 72, 101, 108, 108, 111, 33 };
        try(InputStream input = new ByteArrayInputStream(data)){
            int n;
            while((n=input.read())!=-1)
                System.out.println((char)n);
        }
    }
}

ByteArrayInputStream实际上是把一个byte[ ]数组在内存中变成一个InputStream,虽然应用不多,但测试时可以用来构造一个InputStream。

(六)其他OutputStream实现类

1.ByteArrayOutputStream

在内存中模拟一个OutputStream:

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data;
        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            output.write("Hello".getBytes("UTF-8"));
            output.write("world!".getBytes("UTF-8"));
            data = output.toByteArray();
        }
        System.out.println(new String(data, "UTF-8"));
    }
}

(七)总结

1、InputStream、OutputStream是最基本的IO流,它传输的数据基本单位是byte

常用的方法有:

  • int read():读取输入流的下一个byte,返回该byte表示的int值(0~255)。读到末尾,返回-1表示不能继续读了。
  • read( byte[ ] b ):从输入流中读取多个bytes,并将之存入缓冲数组b[ ]中;
  • read( byte[ ] b , int off , int len ):从输出流中off byte处开始读取len bytes到缓冲数组b中;
  • void write( int b ):写一个byte到输出流。虽然传入的是int,但只会写入1 byte,即该int的最低8位表示字节的部分( b & 0xff )
  • write( byte[ ] b ):将b中的所有byte写入输出流中;
  • write( byte[ ] b , int off , int len ):从b中的off处开始,写入len bytes到输出流中。

常见实现类有:

  • FileXXXStream:文件读写;
  • ByteArrayXXXStream:在内存中模拟IO流。

2、用法

//InputStreampublic 
void readFile() throws IOException{
    try(InputStream input = new FileInputStream("src/readme.txt")){
        byte[] buffer = new byte[1000];
        int n;
        while((n=input.read(buffer))!=-1)//读取到buffer
            System.out.println("read "+n+" bytes.");
    }
}
//OutputStream
public void writeFile() throws IOException{
    try(OutputStream output = new FileOutputStream("out/readme.txt")){
      output.write("Hello".getBytes("UTF-8"));//Hello
    }
}

也可以把InputStream和OutputStream资源的创建放在一起:

//读取input.txt,写入output.txt
try(InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt")){
    input.transferTo(output);
}

二、BufferedReader BufferedWriter

(一)介绍

  1. InputStream、OutputStream
    • 处理字节流的抽象类
    • InputStream字节输入流的所有类的超类,一般我们使用它的子类,如FileInputStream等.
    • OutputStream字节输出流的所有类的超类,一般我们使用它的子类,如FileOutputStream等.
  2. InputStreamReader  OutputStreamWriter
    • 处理字符流的抽象类
    • InputStreamReader 是字节流通向字符流的桥梁,它将字节流转换为字符流.
    • OutputStreamWriter是字符流通向字节流的桥梁,它将字符流转换为字节流.
  3. BufferedReader BufferedWriter
    • BufferedReader 由Reader类扩展而来,提供通用的缓冲方式文本读取,readLine读取一个文本行,
    • 字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取。
    • BufferedWriter  由Writer 类扩展而来,提供通用的缓冲方式文本写入, newLine使用平台自己的行分隔符,
    • 将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。

InputStream能从来源处读取一个一个byte,所以它是最低级的,

InputStreamReader封裝了InputStream在里头,它以较高级的方式,一次读取一个一个字符,

BufferedReader则是比InputStreamReader更高级,它封裝了StreamReader类,一次读取取一行的字符

  1. public BufferedReader(Reader in) 构造方法 接收一个Reader类的实例
  2. public String readLine() throws IOException 一次性从缓冲区中将内容全部读取进来。

(二)实例

BufferedReader接收键盘输入时实例化如下:

此时就可以给出键盘输入数据的标准格式。

import java.io.* ;
public class BufferedReaderDemo01{
    public static void main(String args[]){
        BufferedReader buf = null ;        // 声明对象
        buf = new BufferedReader(new InputStreamReader(System.in)) ;    // 将字节流变为字符流
        String str = null ;    // 接收输入内容
        System.out.print("请输入内容:") ;
        try{
            str = buf.readLine() ;    // 读取一行数据
        }catch(IOException e){
            e.printStackTrace() ;    // 输出信息
        }
        System.out.println("输入的内容为:" + str) ;
    }
};

此时,没有任何长度限制,可以输入很多的内容,每次都以回车结束。

需要注意的是,如果从文件中读取的话readLine一次只能读取一行的数据。编辑test.txt 如下所示:

import java.io.* ;
public class BufferedReaderDemo01{
    public static void main(String args[])throws IOException{
        BufferedReader buf = null ;        // 声明对象
        buf = new BufferedReader(new FileReader(new File("D:"+File.separator+"test.txt"))) ;    // 将字节流变为字符流
        String str1 = null ;    // 接收输入内容
        String str2 = null ;    // 接收输入内容
        try{
            str1 = buf.readLine() ;    // 读取一行数据
            str2 = buf.readLine() ;    // 读取一行数据
        }catch(IOException e){
            e.printStackTrace() ;    // 输出信息
        }
        System.out.println("读取第一行的内容为:" + str1) ;
        System.out.println("读取第二行的内容为:" + str2) ;
    }
};

可以发现:从文件中使用readLine()方法读取行内容时,会自动接着上次在流中的位置进行读取。

如果要全部读取文件的中的内容有如下两种方法:

方法一:使用StringBuffer类不停的连接readLine()从每次读取的一行内容,直至读取的为null为止。然后进行输出。

方法二:使用StringBuffer类不停的连接read()方法读取到的每一个数字转化后的字符。然后进行输出。

三、HDFS读写过程

(一)HDFS相关类说明

FileSystem:通用文件系统的抽象基类,可以被分布式文件系统继承,所有可能使用Hadoop文件系统的代码都要使用到这个类

DistributedFileSystem:Hadoop为FileSystem这个抽象类提供了多种具体的实现,DistributedFileSystem就是FileSystem在HDFS文件系统中的实现。

FSDataInputStream:FileSystem的open()方法返回的是一个输入流FSDataInputStream对象,在HDSF文件系统中具体的输入流就是DFSInputStream。

FSDataOutputStream:FileSystem的create()方法返回的是一个输出流FSDataOutputStream对象,在HDFS文件系统中具体的输出流就是DFSOutputStream。

(二)读数据的过程

 客户端连续调用open()、read()、close()读取数据时,HDFS内部执行流程如下:

  1. 客户端通过FileSystem.open()打开文件,相应的,在HDFS文件系统中DistributedFileSystem具体实现了FileSystem。因此,调用open()方法后,DistributedFileSystem会创建输入流FSDataInputStream,对于HDFS而言,具体的输入流就是DFSInputStream。
  2. 在DFSInputStream的构造函数中,输入流通过ClienProtocal.getBlockLocations()远程调用名称节点获得文件开始部分数据块的保存位置。对于该数据块,名称节点返回保存该数据块的所有数据节点的地址,同时根据距离客户端的远近对数据节点进行排序;然后,DistributedFileSystem会利用DFSInputStream来实例化FSDataInputStream,返回给客户端,同时返回了数据块的数据节点地址。
  3. 获得输入流FSDataInputStream后,客户端调用read()函数开始读取数据。输入流根据前面的排序结果,选择距离客户端最近的数据节点建立连接并读取数据。
  4. 数据从该数据节点读到客户端;当该数据块读取完毕时,FSDataInputStream关闭和该数据节点的连接。
  5. 输入流通过getBlockLocations()方法查找下一个数据块(如果客户端缓存中已经包含了该数据块的位置信息,就不需要调用该方法)。
  6. 找到该数据块的最佳数据节点,读取数据。
  7. 当客户端读取完毕数据的时候,调用FSDataInputStream的close()函数,关闭输入流。

ps:在读取数据的过程中,如果客户端与数据节点通信时出现错误,就会尝试连接包含此数据块的下一个数据节点

代码举例:

import java.io.BufferedReader;
import java.io.InputStreamReader; 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;
 
public class Hdfsread {
        public static void main(String[] args) {
                try {
                        Configuration conf = new Configuration();
                        conf.set("fs.defaultFS","hdfs://localhost:9000"); 
                        conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
                        FileSystem fs = FileSystem.get(conf);
                        Path file = new Path("test"); 
                        FSDataInputStream getIt = fs.open(file);
                        BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
                        String content = d.readLine(); //读取文件一行
                        System.out.println(content);
                        d.close(); //关闭文件
                        fs.close(); //关闭hdfs
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
} 

(三)写数据过程

客户端向HDFS写数据是一个复杂的过程,客户端连续调用create()、write()和close()时,HDFS内部执行过程如下:(ps:不发生任何异常情况)

  1. 客户端通过FileSystem.create()创建文件,相应的,在HDFS文件系统中DistributedFileSystem具体实现了FileSystem。因此,调用create()方法后,DistributedFileSystem会创建输出流FSDataOutputStream,对于HDFS而言,具体的输出流就是DFSOutputStream。
  2. DistributedFileSystem通过RPC远程调用名称节点,在文件系统的命名空间中创建一个新的文件。名称节点会执行一些检查,比如文件是否已经存在、客户端是否有权限创建文件等。检查通过之后,名称节点会构造一个新文件,并添加文件信息。远程方法调用结束后,DistributedFileSystem会利用DFSOutputStream来实例化FSDataOutputStream,返回给客户端,客户端使用这个输入流写入数据。
  3. 获得输出流FSDataOutputStream以后,客户端调用输出流的write()方法向HDFS中对应的文件写入数据。
  4. 客户端向输出流FSDataOutputStream中写入的数据会首先被分成一个个的分包,这些分包被放入DFSOutputStream对象的内部队列。输出流FSDataOutputStream会向名称节点申请保存文件和副本数据块的若干个数据节点,这些数据节点形成一个数据流通道。队列中的分包最后被打包成数据包,发往数据流管道中的第一个数据节点,第一个数据节点将数据包发送给第二个数据节点,第二个数据节点将数据包发送给第三个数据节点,这样,数据包会流经管道上的各个数据节点(流水线复制策略)。
  5. 因为各个数据节点位于不同的机器上,数据需要通过网络发送。因此,为了保证所有的数据节点的数据都是准确的,接收到数据的数据节点要向发送者发送“确认包”(ACK Packet)。确认包沿着数据流管道逆流而上,从数据流管道依次通过各个数据节点并最终发往客户端,当客户端收到应答时,它将对应的分包从内部队列移除。不断执行3~5步骤,直到数据全部写完。
  6. 客户端调用close()方法关闭输出流,此时开始,客户端不会再向输出流写入数据。
  7. 当DFSOutputStream对象内部队列的分包都收到应答后,就可以使用ClientProtocol.complete()方法通知名称节点关闭文件,完成一次正常的写文件过程。

代码举例:

import org.apache.hadoop.conf.Configuration;  
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path; 
public class Hdfswrite {    
        public static void main(String[] args) { 
                try {
                        Configuration conf = new Configuration(); //Hadoop集群的配置信息 
                        conf.set("fs.defaultFS","hdfs://localhost:9000");//设置Hadoop集群的默认文件系统URI的属性
                        conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");//设置HDFS实现类的属性
                        FileSystem fs = FileSystem.get(conf);
                        byte[] buff = "Hello world".getBytes(); // 要写入的内容
                        String filename = "test"; //要写入的文件名
                        FSDataOutputStream os = fs.create(new Path(filename));
                        os.write(buff,0,buff.length);
                        System.out.println("Create:"+ filename);
                        os.close();
                        fs.close();
                } catch (Exception e) {  
                        e.printStackTrace();  
                }  
        }  
}

(四)总结

读的过程:

  1. 客户端访问名称节点,查询并获取文件的数据块位置列表,返回输入流对象。
  2. 就近挑选一台数据节点服务器,请求建立输入流 。
  3. 数据节点向输入流中中写数据。
  4. 关闭输入流。

写的过程:

  1. 客户端向名称发出写文件请求。
  2. 检查是否已存在文件、检查权限。若通过检查,返回输出流对象。
  3. 客户端按128MB的块切分文件。
  4. 客户端将名称节点返回的分配的可写的数据节点列表和Data数据一同发送给最近的第一个数据节点,此后客户端和名称节点分配的多个数据节点构成pipeline管道,客户端向输出流对象中写数据。客户端每向第一个写入一个packet,这个packet便会直接在pipeline里传给第二个、第三个…数据节点。
  5. 每个数据节点写完一个块后,会返回确认信息。
  6. 写完数据,关闭输输出流。
  7. 发送完成信号给名称节点。

若通过检查,直接先将操作写入EditLog,WAL(write aheadlog)操作,先写log在写内存,写入失败通过EditLog记录校验。
packet默认64k。
写完一个block块后汇总确认,不会每个packet确认。
HDFS一般情况下都是强调强一致性,即所有数据节点写完后才向名称节点汇报。

 

posted @ 2023-06-24 20:42  ImreW  阅读(588)  评论(0编辑  收藏  举报