前端 + 后端 实现分片上传(断点续传/极速秒传)

Posted ps酷教程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端 + 后端 实现分片上传(断点续传/极速秒传)相关的知识,希望对你有一定的参考价值。

先记录下,后面有时间再去实现
可参考链接:vue上传大文件/视频前后端(java)代码

前端 + 后端 实现分片上传(断点续传/极速秒传)

前端slice分片上传,后端用表记录分片索引和分片大小和分片总数,当接受完最后一个分片(分片索引等于分片总数,分片索引从1开始),就合并分片成完成的文件。前端需要递归上传,并显示加载动画和根据分片完成数量显示进度条

临时demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="http://www.baidu.com/a">
        <input type="file" type="hidden" id="file"><!-- 隐藏这个原生的上传文件按钮 -->
        <button type="button" id="btn">触发上传</button></button><!-- 使用它来触发选择图片动作 -->
    </form>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>
    /* 监听选择图片的事件 */
    document.querySelector('#file').onchange =  (e)=>
        console.log('改变了');
        console.log(this); // 这里的this变成了Window, 因为写成了箭头函数。
        console.dir(e.target); 

        // 选择了一个文件,所以数组只有一个元素
        console.log(e.target.files); // FileList 0: File, length: 1

        console.log(e.target.files[0]); // File name: 'GIF 2023-4-1 18-14-01.gif', lastModified: 1680344051705, lastModifiedDate: Sat Apr 01 2023 18:14:11 GMT+0800 (中国标准时间), webkitRelativePath: '', size: 242914, …

        upload(e.target.files[0])

        document.querySelector('#file').value = '' // 让下次即使选择同一个文件仍能触发onchange事件
    

    function upload(file) 
        console.log(file instanceof Blob); // true, 而Blob中有个slice方法,可以对文件进行分片
        let formData = new FormData()

        let shardSize = 10 * 1024 * 1024
        let shardIndex = 1
        let start = shardSize * shardIndex
        let end = Math.min(file.size, start + shardSize)
        console.log(start,end);
        formData.append('mfile', file.slice(start,end))

        // 携带数据请求后台
        $.ajax(
            url: 'http://127.0.0.1:8083/article/uploadImg',
            type: 'POST',
            data: formData,
            contentType: false,
            processData: false,
            cache: false,
            success: function (data) 
                if (data.success) 
                    alert('添加成功');
                 else 
                    alert('添加失败');
                
            
        );
    

    /* 点的是#btn,但是我们要触发#file文件上传 */
    document.querySelector('#btn').onclick = function()
        document.querySelector('#file').click()
    
</script>
</html>
@PostMapping("uploadImg")
public Result uploadImg(@RequestParam("mfile") MultipartFile mfile) throws IOException 
    String filename = mfile.getOriginalFilename();
    mfile.transferTo(new File("D:\\\\Projects\\\\vue-springboot\\\\src\\\\main\\\\resources\\\\static\\\\img\\\\"+filename));
    return Result.ok(filename);

spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB

oss 将前面的分片上传改为oss里的追加上传

public static void main(String[] args) throws IOException 
        
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "xxx";
        String accessKeySecret = "yyy";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "test-zzhua";

        String objectName = "video/juc.mp4";

        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        ObjectMetadata meta = new ObjectMetadata();
        meta.setObjectAcl(CannedAccessControlList.PublicRead);


        RandomAccessFile raFile = new RandomAccessFile(new File("D:\\\\Projects\\\\vue-springboot\\\\src\\\\main\\\\resources\\\\static\\\\img\\\\juc.mp4"), "r");

        long totalLen = raFile.length();

        // 定义每次追加上传的大小 3M
        long everyLen = 3 * 1024 * 1024;

        long accLen = 0;


        byte[] bytes = new byte[5 * 1024]; // 缓冲数组5k

        while (true) 

            // 找到上次读取的位置
            raFile.seek(accLen);

            boolean finish = false;

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 当前读取累积3M, 或不够3M就读完了
            int currLen = 0;

            while (true) 
                int readLen = raFile.read(bytes);
                if (readLen == -1) 
                    finish = true;
                    break;
                
                currLen += readLen;
                baos.write(bytes, 0, readLen);
                if (currLen >= everyLen) 
                    break;
                
            

            // 发起追加请求
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucketName, objectName, bais,meta);
            appendObjectRequest.setPosition(accLen);
            ossClient.appendObject(appendObjectRequest);

            if (finish) 
                break;
            

            accLen += currLen;

        


    

md5大文件计算

javascript实现

参考:SpringBoot大文件上传–前端计算文件的MD5

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js"></script>
</head>
<body>
    <form id="from" method="post" action="/upload" enctype="multipart/form-data">
        <table>
            <tr>
                <td>
                    <input id="md5" name="md5">
                    <input id="file" name="upload" type="file">
                    <input id="submit" type="submit" value="上传">
                </td>
            </tr>
        </table>
    </form>

</body>
<script>
    //注意此方法引用了SparkMD5库 library:https://github.com/satazor/SparkMD5
    //监听文本框变化
    document.getElementById("file").addEventListener("change", function() 
        //声明必要的变量
        chunks=0;
        currentChunk=0;
        var fileReader = new FileReader();//一个用来读取文件的对象
        //文件分割方法(注意兼容性)
        blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
            file = document.getElementById("file").files[0],
            //文件每块分割2M,计算分割详情
            chunkSize = 2097152,
            chunks = Math.ceil(file.size / chunkSize),//文件分成了几块
            currentChunk = 0,//当前处理的第几块
            
            spark = new SparkMD5();//创建md5对象(基于SparkMD5)

       //每块文件读取完毕之后的处理
        fileReader.onload = function(e) 
            console.log("读取文件", currentChunk + 1, "/", chunks);
            //每块交由sparkMD5进行计算
            spark.appendBinary(e.target.result);
            currentChunk++;
            //如果文件处理完成计算MD5,如果还有分片继续处理
            if (currentChunk < chunks) 
                loadNext();
             else 
                md5=spark.end();//最终的MD5
                console.log("MD5:"+md5);
            
        ;

        //处理单片文件的上传
        function loadNext() 
            var start = currentChunk * chunkSize,
                end = start + chunkSize >= file.size ? file.size : start + chunkSize;
            fileReader.readAsBinaryString(blobSlice.call(file, start, end));
            //blobSlice.call(file, start, end)每次执行到blobSlice的时候就会跳转到blobSlice定义的地方,可以理解为一个循环
        
        loadNext();
    );
</script>

</html>

java实现

参考:详解JAVA中获取文件MD5值的四种方法
须引入commons-codec包

String s = DigestUtils.md5Hex(new FileInputStream(new File("D:\\\\documents\\\\尚硅谷谷粒学院项目视频教程\\\\6 - What If I Want to Move Faster.mp4")));
ystem.out.println(s);

10G大文件上传最全方案:秒传断点续传分片上传,包教会!

上一篇:麻了!Fastjson 再曝反序列化漏洞。。

前言

文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式

详细教程

秒传

1、什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.

2、本文实现的秒传核心逻辑

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径

分片上传

1.什么是分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

2.分片上传的场景

1.大文件上传

2.网络环境环境不好,存在需要重传风险的场景

断点续传

1、什么是断点续传

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

2、应用场景

断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。

3、实现断点续传的核心逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

4、实现流程步骤

a、方案一,常规步骤

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;

  • 初始化一个分片上传任务,返回本次分片上传唯一标识;

  • 按照一定的策略(串行或并行)发送各个分片数据块;

  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

b、方案二、本文实现的步骤

  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小

  • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)

  • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

5、分片上传/断点上传代码实现

a、前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接:

“http://fex.baidu.com/webuploader/getting-started.html

b、后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接:

https://blog.csdn.net/dimudan2015/article/details/81910690

另一种是使用MappedByteBuffer,对MappedByteBuffer不熟悉的朋友,可以查看如下链接进行了解:

https://www.jianshu.com/p/f90866dcbffc

后端进行写入操作的核心代码

a、RandomAccessFile实现方式

@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)  
@Slf4j  
public class RandomAccessUploadStrategy extends SliceUploadTemplate   
  
  @Autowired  
  private FilePathUtil filePathUtil;  
  
  @Value("$upload.chunkSize")  
  private long defaultChunkSize;  
  
  @Override  
  public boolean upload(FileUploadRequestDTO param)   
    RandomAccessFile accessTmpFile = null;  
    try   
      String uploadDirPath = filePathUtil.getPath(param);  
      File tmpFile = super.createTmpFile(param);  
      accessTmpFile = new RandomAccessFile(tmpFile, "rw");  
      //这个必须与前端设定的值一致  
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024  
          : param.getChunkSize();  
      long offset = chunkSize * param.getChunk();  
      //定位到该分片的偏移量  
      accessTmpFile.seek(offset);  
      //写入该分片数据  
      accessTmpFile.write(param.getFile().getBytes());  
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);  
      return isOk;  
     catch (IOException e)   
      log.error(e.getMessage(), e);  
     finally   
      FileUtil.close(accessTmpFile);  
      
   return false;  
    
  

b、MappedByteBuffer实现方式

@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)  
@Slf4j  
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate   
  
  @Autowired  
  private FilePathUtil filePathUtil;  
  
  @Value("$upload.chunkSize")  
  private long defaultChunkSize;  
  
  @Override  
  public boolean upload(FileUploadRequestDTO param)   
  
    RandomAccessFile tempRaf = null;  
    FileChannel fileChannel = null;  
    MappedByteBuffer mappedByteBuffer = null;  
    try   
      String uploadDirPath = filePathUtil.getPath(param);  
      File tmpFile = super.createTmpFile(param);  
      tempRaf = new RandomAccessFile(tmpFile, "rw");  
      fileChannel = tempRaf.getChannel();  
  
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024  
          : param.getChunkSize();  
      //写入该分片数据  
      long offset = chunkSize * param.getChunk();  
      byte[] fileData = param.getFile().getBytes();  
      mappedByteBuffer = fileChannel  
.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);  
      mappedByteBuffer.put(fileData);  
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);  
      return isOk;  
  
     catch (IOException e)   
      log.error(e.getMessage(), e);  
     finally   
  
      FileUtil.freedMappedByteBuffer(mappedByteBuffer);  
      FileUtil.close(fileChannel);  
      FileUtil.close(tempRaf);  
  
      
  
    return false;  
    
  

c、文件操作核心模板类代码

@Slf4j  
public abstract class SliceUploadTemplate implements SliceUploadStrategy   
  
  public abstract boolean upload(FileUploadRequestDTO param);  
  
  protected File createTmpFile(FileUploadRequestDTO param)   
  
    FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);  
    param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));  
    String fileName = param.getFile().getOriginalFilename();  
    String uploadDirPath = filePathUtil.getPath(param);  
    String tempFileName = fileName + "_tmp";  
    File tmpDir = new File(uploadDirPath);  
    File tmpFile = new File(uploadDirPath, tempFileName);  
    if (!tmpDir.exists())   
      tmpDir.mkdirs();  
      
    return tmpFile;  
    
  
  @Override  
  public FileUploadDTO sliceUpload(FileUploadRequestDTO param)   
  
    boolean isOk = this.upload(param);  
    if (isOk)   
      File tmpFile = this.createTmpFile(param);  
      FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);  
      return fileUploadDTO;  
      
    String md5 = FileMD5Util.getFileMD5(param.getFile());  
  
    Map<Integer, String> map = new HashMap<>();  
    map.put(param.getChunk(), md5);  
    return FileUploadDTO.builder().chunkMd5Info(map).build();  
    
  
  /**  
   * 检查并修改文件上传进度  
   */  
  public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath)   
  
    String fileName = param.getFile().getOriginalFilename();  
    File confFile = new File(uploadDirPath, fileName + ".conf");  
    byte isComplete = 0;  
    RandomAccessFile accessConfFile = null;  
    try   
      accessConfFile = new RandomAccessFile(confFile, "rw");  
      //把该分段标记为 true 表示完成  
      System.out.println("set part " + param.getChunk() + " complete");  
      //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127  
      accessConfFile.setLength(param.getChunks());  
      accessConfFile.seek(param.getChunk());  
      accessConfFile.write(Byte.MAX_VALUE);  
  
      //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)  
      byte[] completeList = FileUtils.readFileToByteArray(confFile);  
      isComplete = Byte.MAX_VALUE;  
      for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++)   
        //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE  
        isComplete = (byte) (isComplete & completeList[i]);  
        System.out.println("check part " + i + " complete?:" + completeList[i]);  
        
  
     catch (IOException e)   
      log.error(e.getMessage(), e);  
     finally   
      FileUtil.close(accessConfFile);  
      
 boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);  
    return isOk;  
    
  
  /**  
   * 把上传进度信息存进redis  
   */  
  private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,  
      String fileName, File confFile, byte isComplete)   
  
    RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);  
    if (isComplete == Byte.MAX_VALUE)   
      redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");  
      redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());  
      confFile.delete();  
      return true;  
     else   
      if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5()))   
        redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");  
        redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),  
            uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");  
        
  
      return false;  
      
    
/**  
   * 保存文件操作  
   */  
  public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile)   
  
    FileUploadDTO fileUploadDTO = null;  
  
    try   
  
      fileUploadDTO = renameFile(tmpFile, fileName);  
      if (fileUploadDTO.isUploadComplete())   
        System.out  
            .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);  
        //TODO 保存文件信息到数据库  
  
        
  
     catch (Exception e)   
      log.error(e.getMessage(), e);  
     finally   
  
      
    return fileUploadDTO;  
    
/**  
   * 文件重命名  
   *  
   * @param toBeRenamed 将要修改名字的文件  
   * @param toFileNewName 新的名字  
   */  
  private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName)   
    //检查要重命名的文件是否存在,是否是文件  
    FileUploadDTO fileUploadDTO = new FileUploadDTO();  
    if (!toBeRenamed.exists() || toBeRenamed.isDirectory())   
      log.info("File does not exist: ", toBeRenamed.getName());  
      fileUploadDTO.setUploadComplete(false);  
      return fileUploadDTO;  
      
    String ext = FileUtil.getExtension(toFileNewName);  
    String p = toBeRenamed.getParent();  
    String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;  
    File newFile = new File(filePath);  
    //修改文件名  
    boolean uploadFlag = toBeRenamed.renameTo(newFile);  
  
    fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());  
    fileUploadDTO.setUploadComplete(uploadFlag);  
    fileUploadDTO.setPath(filePath);  
    fileUploadDTO.setSize(newFile.length());  
    fileUploadDTO.setFileExt(ext);  
    fileUploadDTO.setFileId(toFileNewName);  
  
    return fileUploadDTO;  
    

总结

在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用fastdfs、hdfs等。

本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的md5值计算,后端写入的速度还是比较快。如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网:

https://help.aliyun.com/product/31815.html

阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。

文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器:

https://www.cnblogs.com/ossteam/p/4942227.html

来源:已赋值

参考:https://blog.csdn.net/java_mindmap/article/details/113667621

THE END

热门推荐:
热议!互联网大厂46分钟裁员内部录音曝光!“制定一个完不成的目标”、“明确他是能力不行!”
原来 00 后真的有在整顿职场

阿里第二波裁员进行中,涉及近万人,赔偿N+3;鹅厂近千人被裁......
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。

以上是关于前端 + 后端 实现分片上传(断点续传/极速秒传)的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot分片上传断点续传大文件极速秒传功能,这篇都帮你搞定!(典藏版)...

Java如何实现大文件分片上传,断点续传和秒传

大文件分片上传,断点续传,秒传 实现

webuploader+php如何实现分片+断点续传

如何实现大文件上传:秒传断点续传分片上传

如何实现大文件上传:秒传断点续传分片上传