Java 困扰三周の问题:使用 byte[] 或 skip() 方法读取字节流 Stream 文件尾部多 / 少 / 缺字节解决方法

本贴最后更新于 1654 天前,其中的信息可能已经渤澥桑田

前言

最近在造一个最强兼容性的 FTP 服务端轮子,但在使用 InputStreamOutputStream 及它的子类时,我遇到了很奇怪也很严重的问题:数据尾部随机缺少/多出数据。这个问题简直太致命了。

问题复现

先出一段代码,我之前操作数据流的步骤如下(请注意看注释):

// 获取文件输出流FileOutputStream和网络输入流InputStream
FileOutputStream fileOutputStream = new FileOutputStream(file);
InputStream inputStream = socket.getInputStream();
// 新建一个字节数组
byte[] bytes = new byte[8192];
// 从网络输入流inputStream,读取字节并写入到字节数组byte[] bytes
while ((inputStream.read(bytes)) != -1) {
    // 写入到本地文件中
    fileOutputStream.write(bytes);
}
// 以防万一,刷新一下缓存
fileOutputStream.flush();
// 关闭流
inputStream.close();
fileOutputStream.close();

无论使用何种字节流类,我使用 new byte[8192]这种字节数组读取数据时,最终的结果总会多出来或者少出来很多字节,非常奇怪,但在逻辑上,我以防万一,还将 fileOutputStream 中的缓存刷新了出来,按理来讲应该没有问题了。

这个问题在我尝试了 BufferedXxxStream、FileXxxStream、XxxStream 了以后还是没有得到解决,在无数次的尝试之后,浪费了我三个星期的时间去解决。但在之后的一次偶然 write()方法的查阅中,我找到了解决问题的线索。

问题解决过程

首先,我查阅了 InputStream 的 read 方法:

方法 用法
read() 从输入流中读取数据的下一个字节,返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回-1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。
read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。以整数形式返回实际读取的字节数。

解决思路

注:下面的解题思路中,read 方法我只调用了一次,忽略了 while,方便阅读理解。

  1. 我使用 read()方法逐个读取字节写入文件是没有问题的,但是这样逐个读取速度太慢、CPU 爆满
int data;
data = inputStream.read();
// 此时data中应存储了输入流中的一个字节
  1. 使用 read(byte[] b)的方法,出现了字节错乱的问题,方法也是我一开始认为正确的:
byte[] bytes = new byte[8192];
inputStream.read(bytes);
// 此时bytes中应存储了输入流中的前8192字节(这里是我之前的误解,下面有解释)
  1. 然后突然想到了一个问题:如果网络不好的话,输入流真的每次都能存满 8192 字节吗?
  2. 果然老师说的没错,不把题读完整,坑的一定是自己。看 read(byte[] b)方法解释的后半段,以整数形式返回实际读取的字节数,也就是说,如果我用 int 接住它的返回值,就能得到本次 read 方法读取到的真实长度?试试看:
byte[] bytes = new byte[8192];
int len = -1;
len = inputStream.read(bytes);
// 此时bytes中应存储了相应大小的字节,而len中存储的应是读取到的真实字节长度。
// 注意,len中存储的是长度,而不是read()不传参方法中的字节。
// 由于我省略掉了while,实际上下面的方法应该打印了很多次。
System.out.println("Length: " + len);

运行以后,我获得到的部分结果:

Length: 8192
Length: 8192
Length: 7000
Length: 8192
Length: 8092

果不其然啊,在第三次 read 方法时,实际上只有 7000 个字节被写入了,还剩下 1192 个字节直接被填满了 0,在多出了一堆数据的同时,文件数据也变得不连贯,直接导致损坏。

解决方案

问题找到了,其它的就简单了,只要我们不把它额外填充的字节写入就可以了。查阅一下 OutputStream.write 方法的使用:

write(byte[] b, int off, int len)
从指定的字节数组写入len个字节,从偏移off开始输出到此输出流。

Wow, AMAZING.

这就简单了,在已知长度 n 的条件下,我们只需要使用 write(bytes, 0, n)就可以了:

byte[] bytes = new byte[8192];
int len = -1;
len = inputStream.read(bytes);
outputStream.write(bytes, 0, len);

从第 0 个字节开始,写入到 len 中指定的下标,OK。
至于为什么从 0 开始?别闹。

后语

在出现问题时,一定要仔细翻阅官方文档!这是次教训,也是个经验。

  • 日志
    44 引用 • 105 回帖
  • 字节流
    1 引用 • 7 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone 1

    huaji 仰望高端玩家

  • InkDP

    huaji 仰望高端玩家

  • someone

    ??

  • someone 1
    作者

    哪里有疑问,还是说我写的比较复杂呢 😄 欢迎指教!