断点续传
1、 什么是断点续传
通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。
什么是断点续传:
引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
断点续传流程如下图:
流程如下:
1、前端上传前先把文件分成块
2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传
3、各分块上传完成最后在服务端合并文件
2、断点续传实现
1.前端对文件进行分块
2.前端使用多线程上传分片,上传前给服务器发送消息验证当前分片是否已经上传。
3.所有分片上传完毕后,发送合并分片请求,校验文件的完整性。 (上传的分片应该具备顺序标记)
4.前端给服务器传一个MD5值,服务器合并文件后,利用MD5值计算是否与源文件一致。如果不一致,说明文件需要重新上传。
分片文件清理问题:
- 在数据库中有一张文件表记录minIo中存储的文件信息
- 文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成
- 当一个文件传了一半不再上传了,说明该文件没有上传完成,通过定时任务去查询文件表中的记录,如果文件距离上次上传结束超过24小时,则可以考虑清除MinIo中相关的分片数据
3、分块与合并测试
为了更好的理解文件分块上传的原理,下边用java代码测试文件的分块与合并。
文件分块的流程如下:
1、获取源文件长度
2、根据设定的分块文件的大小计算出块数
3、从源文件读数据依次向每一个块文件写数据。
测试代码如下:
package com.xuecheng.media; import org.apache.commons.codec.digest.DigestUtils; import org.junit.jupiter.api.Test; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.*; /** * @program: xuecheng-plus-project148 * @description: TODO大文件分块合并 * @author: Mr.Zhang * @create: 2023-02-27 08:54 **/ public class BigFileTest { //分块测试 @Test public void testChunk() throws IOException { //源文件 File sourceFile = new File("D:\\software\\Test\\xuecheng\\video\\1.mp4"); //分块文件存储路径 File chunkFolderPath = new File("D:\\software\\Test\\xuecheng\\chunk\\"); if (!chunkFolderPath.exists()) { chunkFolderPath.mkdir(); } //分块的大小 1mb int chunkSize = 1024 * 1024 * 1; //分块数量 long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize); //思路,使用流对象读取文件,向分块文件写数据,达到分块大小不再写 RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r"); //缓冲区 byte[] b = new byte[1024]; for (long i = 0; i < chunkNum; i++) { //分块文件 File file = new File("D:\\software\\Test\\xuecheng\\chunk\\" + i); //如果分块文件存在了,则删除 if (file.exists()) { file.delete(); } //创建文件 boolean newFile = file.createNewFile(); if (newFile) { //向分块文件写数据流对象 RandomAccessFile raf_write = new RandomAccessFile(file, "rw"); int len = -1; while ((len = raf_read.read(b)) != -1) { //向文件中写数据 raf_write.write(b, 0, len); //达到分块大小不在写了 if (file.length() >= chunkSize) { break; } } raf_write.close(); } } raf_read.close(); } //测试合并 @Test public void testMerge() throws IOException { //源文件 File sourceFile = new File("D:\\software\\Test\\xuecheng\\video\\1.mp4"); //分块文件存储路径 File chunkFolderPath = new File("D:\\software\\Test\\xuecheng\\chunk\\"); if (!chunkFolderPath.exists()) { chunkFolderPath.mkdir(); } //合并后的文件 File mergeFile = new File("D:\\software\\Test\\xuecheng\\video\\1_01.mp4"); boolean newFile1 = 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"); //缓冲区 byte[] b = new byte[1024]; for (File file : chunkFileList) { //读取分块文件的流对象 RandomAccessFile raf_read = new RandomAccessFile(file, "r"); int len = -1; while ((len = raf_read.read(b))!=-1){ //向合并文件写数据 raf_write.write(b,0,len); } } //校验合并后的文件是否正确 FileInputStream sourceFileStream = new FileInputStream(sourceFile); FileInputStream mergeFileStream = new FileInputStream(mergeFile); //源文件 String sourceMd5Hex = DigestUtils.md5Hex(sourceFileStream); //合并后的文件 String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream); if (sourceMd5Hex.equals(mergeMd5Hex)){ System.out.println("合并成功"); } } }