IO Stream byte[]

【问题】

  在服务器上的两个模块,一个专门处理文件(阿里云OSS)下载的模块A,另一个拿这些文件去处理业务需求的模块B。于是A模块先把文件下载下来,放到服务器上,然后B模块再根据所传过来的文件对象转换成流去处理业务。但是这样一来,服务器上就会多出来很多文件。如图:

如果请求用户一多,文件岂不是要大量积压在这里?怎么办呢?

【思考】
  首先,能不能业务模块B处理完了业务之后把文件都删了呢?但是这样的话,还是很麻烦,而且还要考虑用不同的文件路径来存放不同的用户文件,万一文件名称相同,可能每个请求都要创建一个单独的文件路径来存放,这种方式的代价还是代价太大了......
  或者可不可以直接不生成文件返回一个二进制的数组,比如byte[],又或者返回一个数据流对象?
  说起来,对于Java IO相关的内容还是理解地不透彻。为什么Java中都是用各种InputStream和OutputStream来处理流,而不是直接把文件转换为完整的二进制数组来传输呢?使用流对象的过程中为什么总是用:

while((length=inputStream.read(byteArray))!=-1)
{
    ...
}

来处理流之间的转换,这个字节数组为什么长度很多时候都定义为1024?我们在程序中经常使用的System.out.print到底是个啥?可能甚至见过在Servlet中,使用了out.println将html写入了容器的响应对象response......

【查阅资料】

  • IO这个概念,就是输入和输出的缩写,凡是接触到计算机这个体系的都不陌生。首先,要明确一点,对于小文件,直接返回一组byte[]是可行的,此时我们只需要将不同的输入流转换为字节数组即可。如代码所示:
public class FileIOTransferTest {

    public static void main(String[] args) throws IOException {
        testByteToFile();
    }

    private static void testByteToFile() throws IOException {
        File file = new File("C:\Users\Administrator\Desktop\1123123.png");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        byte[] byteArrayFromFile = new FileIOTransferTest().getByteArrayFromFile();
        fileOutputStream.write(byteArrayFromFile, 0, byteArrayFromFile.length);
        fileOutputStream.flush();
        fileOutputStream.close();
    }

    public byte[] getByteArrayFromFile() {
        File file = new File("C:\Users\Administrator\Desktop\123.png");
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

            int len;
            byte[] buffer = new byte[1024];
            while ((len = fileInputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }

            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }
}

  但是,为什么要用流来处理而不是用一个完整的字节数组?我们可以想一下:

  1.如果文件很大,而内存设置很小,一个完整的字节数组可能会比整个内存空间都大,那这种情况岂不是处理不了?尤其是一些嵌入式的设备,内存空间是很宝贵的;

  2.对于网络传输的字节流,本来就是分批传的,一个完整的字节组,可能会根据协议分好几批传入到系统中,所以必须用这种方式来进行处理;

  3.Java的流处理,实际上是基于操作系统内核提供的api进行的,所以很大程度上也是会根据OS的api特性来进行设计。如linux的IO流,read、write等等。

  • 而1024这个问题,其实是为了设置一个合适的缓冲区,避免每次只去读取一个字节而频繁使用IO(一个很耗时的操作)。我们可以看看对read的描述
    /**
     * Reads a byte of data from this input stream. This method blocks
     * if no input is yet available.
     *
     * @return     the next byte of data, or <code>-1</code> if the end of the
     *             file is reached.
     * @exception  IOException  if an I/O error occurs.
     */
    public int read() throws IOException {
        return read0();
    }

    private native int read0() throws IOException;

  这里是一个字节一个字节去读取的,所以这个read()方法,如果是一般的场景千万不要直接使用,而是去设置一个缓冲区,一批一批地拿数据。这里我们可以参考BufferedInputStream和BufferedOutputStream的设计,它在内部设置的默认缓冲区的大小是8192byte。这里就是一个平衡,设置太小,那么IO会特别频繁,肯定性能是不好的,如果设置地过大,很多时候会考虑一些场景的限制,比如Dubbo的接口限制数据量大小为8M,所以这里可以根据场景来进行设置。就像HashMap中可以自己定义一个初始的容量大小,避免在数据装填过程中频繁的扩展数组...

  • 至于不同的方法将数据输出到了不同的位置,也是可以直接溯源这部分代码的源头,看看这个流的destination设置在了哪儿。

【扩展体系】

  在对以上问题梳理的过程中,其实应该搞明白的是,这一块儿到底包含了哪些比较重要的内容,我认为比较重要的是以下几个部分:

  • JDK体系中的字节流和字符流体系是很么样子的,首先是4个抽象基类:

    字节输入流:InputStream 字符输入流:Reader
    字节输出流:OutputStream 字符输出流:Writer

  然后是整个体系结构:

  

分类

字节输入流

字节输出流

字符输入流

字符输出流

抽象基类

InputStream

OutputStream

Reader

Writer

访问文件

FileInputStream

FileOutputStream

FileReader

FileWriter

访问数组

ByteArrayInputStream

ByteArrayOutputStream

CharArrayReader

CharArrayWriter

访问管道

PipedInputStream

PipedOutputStream

PipedReader

PipedWriter

缓冲流

BufferedInputStream

BufferedOutputStream

BufferedReader

BufferedWriter

转换流

   

InputStreamReader

OutputStreamWriter

对象流

ObjectInputStream

ObjectOutputStream

   

抽象基类

FilterInputStream

FilterOutputStream

FilterReader

FilterWriter

打印流

 

PrintStream

 

PrintWriter

特殊流

DataInputStream

DataOutputStream

   

  这里,我们需要知道的是每一个重要的分支为什么会存在,有什么不一样的特性和使用场景。囿于篇幅,这里不做展开。

  • 由于Java API封装到了底层其实是native方法,比如read,write,open等,为了深入了解其中的原理,我们还需要去了解操作系统的IO处理流程,如Linux的IO流是如何处理的,内核中的缓冲区怎么处理,到了JVM中(进程级别)的缓冲区又是怎么处理的。知道大体的处理流程。
  • 其实传统的IO处理,一直是阻塞的模式,直到NIO的出现,所以,NIO也要着重去理解。

  以上基本上就是比较完整的理论体系了。如果对以上比较清楚,掌握原理,也就可以说对于IO这一块儿基本OK了。但是对于系统的性能分析,我们可能还需要多一些实践的工具,比如Linux上去分析IO的性能瓶颈在哪里,怎么查询,思路等等。理论和实践基本搞定的话,我们整个知识体系就立起来了,后续的复杂的问题,只是在这里体系上不断地扩展,开枝散叶。

原文地址:https://www.cnblogs.com/bruceChan0018/p/15085484.html