大文件的断点续传

325 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情

试想一下,当我们在下载将近10个G的王者荣耀时,突然出现网络环境异常导致下载失败,需要从头下载,你说气不气?

image.png

断点续传就是来解决这种问题的。出现因网络异常等导致文件传输失败时,引入了断点续传后,就不需要从头进行了,可以从已经上传、下载的部分继续未完成的上传下载任务。

image.png 对于大文件来讲,断点续传会将大文件拆分成多个小文件进行传输,即拆分成固定大小的块。接着,将这些小文件进行合并。

那具体来讲,断点续传到底怎么做呢?

大文件拆分成块

  • 导入源文件,并获取源文件长度。
  • 定义分的块的大小,通过源文件大小与单个块的大小确定块的数量。
  • 通过流对象来读取源文件,向分块文件写数据,达到分块的大小后就不再写了。

具体代码如下,注释已经写的非常全了,我就不一块一块拿出来去介绍了。

public void testChunk() throws IOException{
    //源文件
    File sourceFile = new File("/Users/laoli/Desktop/project2022/testvideo/nacos.mov");
    sourceFile.createNewFile();

    //定义分块文件存储路径
    File chunkFolderPath = new File("/Users/laoli/Desktop/project2022/testvideo/chunk/");
    if(!chunkFolderPath.exists()){
        chunkFolderPath.mkdirs();//不存在分块目录就进行创建
    }

    //确定分块大小:1M
    int chunkSize = 1024*1024*1;
    //分块数量,需要向上转型,即数量在0-1之间就是1
    long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);

    //思路:通过流对象读取源文件,向分块文件去写数据,达到分块的大小就不再写了.
    //- 读需要建一个输入流:随机读文件的内容
    RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//r是可读
    //建立读写的缓冲取
    byte[] b= new byte[1024];

    for (int i = 0; i < chunkNum; i++) {
        //分块文件,命名
        File file = new File("/Users/laoli/Desktop/project2022/testvideo/chunk/" + i);
        //如果这个分块文件已经存在,我们就删掉
        if(file.exists()){
            file.delete();
        }
        //先将文件创建出来,刚创建出来的话在磁盘上就存在了,只不过是个空的
        boolean newFile = file.createNewFile();
        //若文件创建成功,就可以向文件中去写数据了
        if (newFile){
            //向分块文件写数据的流对象
            RandomAccessFile raf_write = new RandomAccessFile(file, "rw");//r是可读
            //向文件写数据:读写用流需要建一个缓冲区byte[]
            int len = -1; //-1就是读到末尾
            //先读再写。读到缓冲区。
            //没读到末尾就继续往里面写
            while ((len = raf_read.read(b))!=-1){
                raf_write.write(b,0,len); //从0写,写一个字节:就是把缓冲区的全部写进去
                if(file.length()>=chunkSize){
                    //达到了分块的大小就不写了
                    break;
                }
            }
            //写完关写流
            raf_write.close();
        }
    }
    //所有的都弄完,关读流
    raf_read.close();
}

文件拆分的运行结果如下:

image.png

文件合并

小文件合并的思路与大文件拆块异曲同工。这里我们仍需要源文件(文件拆分前的那个文件),另外需要额外创建合并后的文件。

小文件合并成大文件后,还需要对文件进行校验,通过md5值来校验源文件和合并后的文件,以保证传来的是同一个文件。不校验的话肯定会存在很多问题的,比如突然遭到网络攻击,黑客用病毒替换掉了一个小文件,你根本察觉不到。相信点到这里,jym也明白了为什么源文件仍然需要了。

文件合并还需要注意是小文件的顺序问题:我在对大文件进行拆分的时候,是将拆出的小文件通过数字递增(1,2,3,4...)来命名的,所以在顺序问题上,先拿到分块文件列表并按文件名递增进行排序来处理顺序问题。详细代码如下:

public void testMerge() throws IOException{
    //源文件
    File sourceFile = new File("/Users/laoli/Desktop/project2022/testvideo/nacos.mov");
    sourceFile.createNewFile();
    //定义分块文件存储路径
    File chunkFolderPath = new File("/Users/laoli/Desktop/project2022/testvideo/chunk/");
    if(!chunkFolderPath.exists()){
        chunkFolderPath.mkdirs();//不存在分块目录就进行创建
    }
    //合并后的文件
    File mergeFile = new File("/Users/laoli/Desktop/project2022/testvideo/nacos_01.mov");
    mergeFile.createNewFile(); //把合并文件先创建出来
    //思路:通过流对象读取分块文件,向合并文件去写数据,将所有分块文件依次写入.
    // -获取分块列表:该列表是有顺序的,我们可以按文件名升序
    File[] chunkFiles = chunkFolderPath.listFiles();
    List<File> chunkFileList = Arrays.asList(chunkFiles);
    Collections.sort(chunkFileList, new Comparator<File>() {
        //比较器
        @Override
        public int compare(File o1, File o2) {
            //转数字是因为我们前面拆文件,命名成数字了
            return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
        }
    });
    //依次读取分块文件
    // -创建合并文件的流对象,写流
    RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");//r是可读
    //建立读写的缓冲取
    byte[] b= new byte[1024];
    for (File file : chunkFileList) {
        //读取:我们需要建输入流来读
        RandomAccessFile raf_read = new RandomAccessFile(file, "r");//r是可读
        //向合并文件去写
        int len = -1;
        while ((len = raf_read.read(b))!=-1){
            //向合并文件写数据
            raf_write.write(b,0,len);
        }
    }
    //写完之后还需要校验一下是否与原始文件一样
    //比较两者的md5值就行
    FileInputStream sourceFileStream = new FileInputStream(sourceFile);//源文件的流对象
    FileInputStream mergeFileStream = new FileInputStream(mergeFile);//合并后的文件的流对象
    String sourceFileMD5Hex = DigestUtils.md5Hex(sourceFileStream);//源文件的md5值
    String mergeFileMD5Hex = DigestUtils.md5Hex(mergeFileStream); //合并后文件的md5值
    if (sourceFileMD5Hex.equals(mergeFileMD5Hex)){
        System.out.println("合并成功"); //两者md5相同就合并成功
    }

}

最后是文件合并的运行结果:

image.png

相信讲到这里,jym已经掌握断点续传的原理与实现了。