首页 教程 开发语言 如何使用Java实现文件的断点续传功能?

如何使用Java实现文件的断点续传功能?

什么是断点续传

用户上传大文件,网络差点的需要历时数小时,万一线路中断,不具备断点续传的服务器就只能从头重传,而断点续传就是,允许用户从上传断线的地方继续传送,这样大大减少了用户的烦恼。

解决上传大文件服务器内存不够的问题

解决如果因为其他因素导致上传终止的问题,并且刷新浏览器后仍然能够续传,重启浏览器(关闭浏览器后再打开)仍然能够继续上传,重启电脑后仍然能够上传

检测上传过程中因网络波动导致文件出现了内容丢失那么需要自动检测并且从新上传

解决方案

前端

需要进行分割上传的文件

需要对上传的分片文件进行指定文件序号

需要监控上传进度,控制进度条

上传完毕后需要发送合并请求

Blob 对象,操作文件

后端

上传分片的接口

合并分片的接口

获取分片的接口

其他工具方法,用于辅助

前端端需要注意的就是: 文件的切割,和进度条

后端需要注意的就是: 分片存储的地方和如何进行合并分片

效果演示

先找到需要上传的文件

如何使用Java实现文件的断点续传功能?

当我们开始上传进度条就会发生变化,当我们点击停止上传那么进度条就会停止

如何使用Java实现文件的断点续传功能?

我们后端会通过文件名+文件大小进行MD5生成对应的目录结果如下:

如何使用Java实现文件的断点续传功能?

当前端上传文件达到100%时候就会发送文件合并请求,然后我们后端这些分片都将被合并成一个文件

如何使用Java实现文件的断点续传功能?

通过下图可以看到所有分片都没有了,从而合并出来一个文件

如何使用Java实现文件的断点续传功能?

文件上传过程中网络波动导致流丢失一部分(比对大小)

文件上传过程中,服务器丢失分片 (比对分片的连续度)

文件被篡改内容(比对大小)

效验核心代

如何使用Java实现文件的断点续传功能?

参考代码

前端

<!DOCTYPE html> <html>   <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>       <h2>html5大文件断点切割上传</h2>     <div id="progressBar"></div>       <input id="file" name="mov" type="file" />     <input id="btn" type="button" value="点我上传" />     <input id="btn1" type="button" value="点我停止上传" />       <script type="module">         import FileSliceUpload  from '../jsutils/FileSliceUpload.js'         let testingUrl="http://localhost:7003/fileslice/testing"         let uploadUrl="http://localhost:7003/fileslice/uploads"         let margeUrl="http://localhost:7003/fileslice/merge-file-slice"         let progressUrl="http://localhost:7003/fileslice/progress"          let fileSliceUpload=  new FileSliceUpload(testingUrl,uploadUrl,margeUrl,progressUrl,"#file")          fileSliceUpload.addProgress("#progressBar")           let btn=  document.querySelector("#btn")           let btn1=  document.querySelector("#btn1")         btn.addEventListener("click",function () {             fileSliceUpload.startUploadFile()         })         btn1.addEventListener("click",function () {             fileSliceUpload.stopUploadFile()         })       </script>     </body>   </html>

//大文件分片上传,比如10G的压缩包,或者视频等,这些文件太大了  (需要后端配合进行) class FileSliceUpload{            constructor(testingUrl, uploadUrl, margeUrl,progressUrl, fileSelect) {             this.testingUrl = testingUrl; // 检测文件上传的url             this.uploadUrl = uploadUrl;//文件上传接口             this.margeUrl = margeUrl; // 合并文件接口             this.progressUrl = progressUrl; //进度接口             this.fileSelect = fileSelect;             this.fileObj = null;             this.totalize = null;             this.blockSize = 1024 * 1024; //每次上传多少字节1mb(最佳)             this.sta = 0; //起始位置             this.end =  this.sta +  this.blockSize; //结束位置             this.count = 0; //分片个数             this.barId = "bar"; //进度条id             this.progressId = "progress";//进度数值ID             this.fileSliceName = ""; //分片文件名称             this.fileName = "";             this.uploadFileInterval = null;  //上传文件定时器       }       /**      *  样式可以进行修改      * @param {*} progressId   需要将进度条添加到那个元素下面      */     addProgress (progressSelect) {         let bar = document.createElement("div")         bar.setAttribute("id", this.barId);         let num = document.createElement("div")         num.setAttribute("id", this.progressId);         num.innerText = "0%"         bar.appendChild(num);         document.querySelector(progressSelect).appendChild(bar)            }     //续传  在上传前先去服务器检测之前是否有上传过这个文件,如果还有返回上传的的分片,那么进行续传     // 将当前服务器上传的最后一个分片会从新上传, 避免因为网络的原因导致分片损坏      sequelFile () {         if (this.fileName) {             var xhr = new XMLHttpRequest();             //同步             xhr.open('GET', this.testingUrl + "/" + this.fileName+ "/" + this.blockSize+ "/" + this.totalize, false);             xhr.send();             if (xhr.readyState === 4 && xhr.status === 200) {                 let ret = JSON.parse(xhr.response)                 if (ret.code == 20000) {                    let data= ret.data                     this.count = data.code;                     this.fileSliceName = data.fileSliceName                     //计算起始位置和结束位置                     this.sta = this.blockSize * this.count                     //计算结束位置                     this.end = this.sta + this.blockSize                 } else {                     this.sta = 0; //从头开始                     this.end = this.sta + this.blockSize;                     this.count = 0; //分片个数                 }             }         }     }       stopUploadFile () {         clearInterval(this.uploadFileInterval)     }       // 文件上传(单文件)     startUploadFile () {           // 进度条          let bar = document.getElementById(this.barId)          let progressEl = document.getElementById(this.progressId)         this.fileObj = document.querySelector(this.fileSelect).files[0];         this.totalize = this.fileObj.size;         this.fileName = this.fileObj.name;            //查询是否存在之前上传过此文件,然后继续         this.sequelFile()         let ref = this; //拿到当前对象的引用,因为是在异步中使用this就是他本身而不是class         this.uploadFileInterval = setInterval(function () {                 if (ref.sta > ref.totalize) {                     //上传完毕后结束定时器                     clearInterval(ref.uploadFileInterval)                     //发送合并请求                     ref.margeUploadFile ()                     console.log("stop" + ref.sta);                     return;                 };                 //分片名称                 ref.fileSliceName = ref.fileName + "-slice-" + ref.count++                 //分割文件 ,                 var blob1 =  ref.fileObj.slice(ref.sta, ref.end);                 var fd = new FormData();                 fd.append('part', blob1);                 fd.append('fileSliceName', ref.fileSliceName);                 fd.append('fileSize', ref.totalize);                 var xhr = new XMLHttpRequest();                 xhr.open('POST',  ref.uploadUrl, true);                 xhr.send(fd); //异步发送文件,不管是否成功, 会定期检测                   xhr.onreadystatechange = function () {                     if (xhr.readyState === 4 && xhr.status === 200) {                         let ret = JSON.parse(xhr.response)                         if (ret.code == 20000) {                             //计算进度                             let percent =  Math.ceil((ret.data*ref.blockSize/ ref.totalize) * 100)                             if (percent > 100) {                                 percent=100                                                               }                             bar.style.width = percent + '%';                             bar.style.backgroundColor = 'red';                             progressEl.innerHTML = percent + '%'                         }                     }             }                 //起始位置等于上次上传的结束位置             ref.sta =  ref.end;             //结束位置等于上次上传的结束位置+每次上传的字节             ref.end = ref.sta + ref.blockSize;                   }, 5)       }       margeUploadFile () {             console.log("检测上传的文件完整性..........");             var xhr = new XMLHttpRequest();             //文件分片的名称/分片大小/总大小             xhr.open('GET', this.margeUrl+ "/" + this.fileSliceName + "/" + this.blockSize + "/" + this.totalize, true);             xhr.send(); //发送请求             xhr.onreadystatechange = function () {                 if (xhr.readyState === 4 && xhr.status === 200) {                     let ret = JSON.parse(xhr.response)                     if (ret.code == 20000) {                         console.log("文件上传完毕");                     } else {                         console.log("上传完毕但是文件上传过程中出现了异常", ret);                     }                 }             }           }   } export default FileSliceUpload;

后端

因为代码内部使用较多自己封装的工具类的原因,以下代码只提供原理的参考

package com.controller.commontools.fIleupload;   import com.alibaba.fastjson.JSON; import com.application.Result; import com.container.ArrayByteUtil; import com.encryption.hash.HashUtil; import com.file.FileUtils; import com.file.FileWebUpload; import com.file.ReadWriteFileUtils; import com.function.impl.ExecutorUtils; import com.path.ResourceFileUtil; import com.string.PatternCommon; import org.springframework.web.bind.annotation.*;   import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors;   @RestController @RequestMapping("/fileslice") public class FIleSliceUploadController {       private  final  String identification="-slice-";     private  final  String uploadslicedir="uploads"+File.separator+"slice"+File.separator;//分片目录     private  final  String uploaddir="uploads"+File.separator+"real"+File.separator;//实际文件目录     //获取分片     @GetMapping("/testing/{fileName}/{fileSlicSize}/{fileSize}")     public Result testing(@PathVariable String fileName,@PathVariable long fileSlicSize,@PathVariable long fileSize  ) throws Exception {         String dir = fileNameMd5Dir(fileName,fileSize);         String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir)+File.separator+dir;         File file = new File(absoluteFilePathAndCreate);         if (file.exists()) {             List<String> filesAll = FileUtils.getFilesAll(file.getAbsolutePath());               if (filesAll.size()<2){                 //分片缺少 删除全部分片文件 ,从新上传                 FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);                 return Result.Error();             }               //从小到大文件进行按照序号排序,和判断分片是否损坏             List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);             //获取最后一个分片             String fileSliceName = collect.get(collect.size() - 1);             fileSliceName = new File(fileSliceName).getName();             int code = fileId(fileSliceName);             //服务器的分片总大小必须小于或者等于文件的总大小             if ((code*fileSlicSize)<=fileSize) {                 Result result = new Result();                 String finalFileSliceName = fileSliceName;                 String str = PatternCommon.renderString("{\"code\":\"$[code]\",\"fileSliceName\":\"${fileSliceName}\"}", new HashMap<String, String>() {{                     put("code", String.valueOf(code));                     put("fileSliceName", finalFileSliceName);                 }});                 result.setData(JSON.parse(str));                 return result;             }else {                 //分片异常 ,删除全部分片文件,从新上传                 FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);                 return Result.Error();             }         }         //不存在        return Result.Error();     }         @PostMapping(value = "/uploads")     public Result uploads(HttpServletRequest request)  {         String fileSliceName = request.getParameter("fileSliceName");         long fileSize = Long.parseLong(request.getParameter("fileSize")); //文件大小         String dir = fileSliceMd5Dir(fileSliceName,fileSize);         String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);         FileWebUpload.fileUpload(absoluteFilePathAndCreate,fileSliceName,request);         int i = fileId(fileSliceName); //返回上传成功的文件id,用于前端计算进度         Result result=new Result();         result.setData(i);         return result;     }         // 合并分片     @GetMapping(value = "/merge-file-slice/{fileSlicNamee}/{fileSlicSize}/{fileSize}")     public Result mergeFileSlice(@PathVariable String fileSlicNamee,@PathVariable long fileSlicSize,@PathVariable long fileSize ) throws Exception {         int l =(int) Math.ceil((double) fileSize / fileSlicSize); //有多少个分片         String dir = fileSliceMd5Dir(fileSlicNamee,fileSize); //分片所在的目录        String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);         File file=new File(absoluteFilePathAndCreate);         if (file.exists()){             List<String> filesAll = FileUtils.getFilesAll(file.getAbsolutePath());               //阻塞循环判断是否还在上传  ,解决前端进行ajax异步上传的问题             int beforeSize=filesAll.size();               while (true){                  Thread.sleep(1000);                  //之前分片数量和现在分片数据只差,如果大于1那么就在上传,那么继续                  filesAll = FileUtils.getFilesAll(file.getAbsolutePath());                 if (filesAll.size()-beforeSize>=1){                     beforeSize=filesAll.size();                     //继续检测                     continue;                 }                 //如果是之前分片和现在的分片相等的,那么在阻塞2秒后检测是否发生变化,如果还没变化那么上传全部完成,可以进行合并了                 //当然这不是绝对的,只能解决网络短暂的波动,因为有可能发生断网很长时间,网络恢复后文件恢复上传, 这个问题是避免不了的,所以我们在下面的代码进行数量的效验                 // 因为我们不可能一直等着他网好,所以如果1~3秒内没有上传新的内容,那么我们默认判定上传完毕                 if (beforeSize==filesAll.size()){                     Thread.sleep(2000);                     filesAll = FileUtils.getFilesAll(file.getAbsolutePath());                     if (beforeSize==filesAll.size()){                         break;                     }                 }             }             //分片数量效验             if (filesAll.size()!=l){                 //分片缺少 ,删除全部分片文件,从新上传                 FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);                 return Result.Error();             }             //获取实际的文件名称,组装路径             String realFileName = realFileName(fileSlicNamee);             String realFileNamePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir+ realFileName);             //从小到大文件进行按照序号排序 ,和检查分片文件是否有问题             List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);             int fileSliceSize = collect.size();               List<Future<?>> futures = new ArrayList<>();             // 将文件按照序号进行合并 ,算出Runtime.getRuntime().availableProcessors()个线程 ,每个线程需要读取多少分片, 和每个线程需要读取多少字节大小             //有人会说一个分片一个线程不行吗,你想想如果上千或者上万分片的话,你创建这么多的线程需要多少时间,以及线程切换上下文切换和销毁需要多少时间?              // 就算使用线程池,也顶不住啊,你内存又有多大,能存下多少队列?,并发高的话直接怼爆             int availableProcessors = Runtime.getRuntime().availableProcessors();             //每个线程读取多少文件             int readFileSize = (int)Math.ceil((double)fileSliceSize / availableProcessors);             //每个线程需要读取的文件大小             long readSliceSize = readFileSize * fileSlicSize;             for (int i = 0; i < availableProcessors; i++) {                 int finalI = i;                 Future<?> future =   ExecutorUtils.createFuture("FIleSliceUploadController",()->{                     //每个线程需要读取多少字节                     byte[] bytes=new byte[(int) readSliceSize];                     int index=0;                     for (int i1 = finalI *readFileSize,i2 = readFileSize*(finalI+1)>fileSliceSize?fileSliceSize:readFileSize*(finalI+1); i1 < i2; i1++) {                         try ( RandomAccessFile r = new RandomAccessFile(collect.get(i1), "r");){                             r.read(bytes, (int)(index*fileSlicSize),(int)fileSlicSize);                         } catch (IOException e) {                             e.printStackTrace();                         }                         index++;                     }                       if(finalI==availableProcessors-1){                         //需要调整数组                         bytes = ArrayByteUtil.getActualBytes(bytes);                     }                       try ( RandomAccessFile w = new RandomAccessFile(realFileNamePath, "rw");){                         //当前文件写入的位置                         w.seek(finalI*readSliceSize);                         w.write(bytes);                     } catch (IOException e) {                         e.printStackTrace();                     }                 });                 futures.add(future);             }             //阻塞到全部线程执行完毕后             ExecutorUtils.waitComplete(futures);             //删除全部分片文件             FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);         }else {             //没有这个分片相关的的目录             return Result.Error();         }           return Result.Ok();           }                 //获取分片文件的目录     private String fileSliceMd5Dir(String fileSliceName,long fileSize){         int i = fileSliceName.indexOf(identification) ;         String substring = fileSliceName.substring(0, i);         String dir = HashUtil.md5(substring+fileSize);         return dir;     }     //通过文件名称获取文件目录     private String fileNameMd5Dir(String fileName,long fileSize){         return HashUtil.md5(fileName+fileSize);     }     //获取分片的实际文件名     private String realFileName(String fileSliceName){         int i = fileSliceName.indexOf(identification) ;         String substring = fileSliceName.substring(0, i);         return substring;       }     //获取文件序号     private  int fileId(String fileSliceName){         int i = fileSliceName.indexOf(identification)+identification.length() ;         String fileId = fileSliceName.substring(i);         return Integer.parseInt(fileId);     }         //判断是否损坏   private List<String>  fileSliceIsbadAndSort(File file,long fileSlicSize) throws Exception {         String absolutePath = file.getAbsolutePath();         List<String> filesAll = FileUtils.getFilesAll(absolutePath);         if (filesAll.size()<1){             //分片缺少,删除全部分片文件 ,从新上传             FileUtils.delFilesAllReview(absolutePath,true);             throw  new Exception("分片损坏");         }         //从小到大文件进行按照序号排序         List<String> collect = filesAll.stream().sorted((a, b) -> fileId(a) - fileId(b)).collect(Collectors.toList());         //判断文件是否损坏,将文件排序后,进行前后序号相差大于1那么就代表少分片了         for (int i = 0; i < collect.size()-1; i++) {             //检测分片的连续度             if (fileId(collect.get(i)) - fileId(collect.get(i+1))!=-1) {                 //分片损坏 删除全部分片文件 ,从新上传                 FileUtils.delFilesAllReview(absolutePath,true);                 throw  new Exception("分片损坏");             }             //检测分片的完整度             if (new File(collect.get(i)).length()!=fileSlicSize) {                 //分片损坏 删除全部分片文件 ,从新上传                 FileUtils.delFilesAllReview(absolutePath,true);                 throw  new Exception("分片损坏");             }         }         return  collect;     } }

评论(0)条

提示:请勿发布广告垃圾评论,否则封号处理!!

    猜你喜欢
    【MySQL】用户管理

    【MySQL】用户管理

     服务器/数据库  2个月前  2.15k

    我们推荐使用普通用户对数据的访问。而root作为管理员可以对普通用户对应的权限进行设置和管理。如给张三和李四这样的普通用户权限设定后。就只能操作给你权限的库了。

    Cursor Rules 让开发效率变成10倍速

    Cursor Rules 让开发效率变成10倍速

     服务器/数据库  2个月前  1.21k

    在AI与编程的交汇点上,awesome-cursorrules项目犹如一座灯塔,指引着开发者们驶向更高效、更智能的编程未来。无论你是经验丰富的老手,还是刚入行的新人,这个项目都能为你的编程之旅增添一抹亮色。这些规则文件就像是你私人定制的AI助手,能够根据你的项目需求和个人偏好,精确地调教AI的行为。突然间,你会发现AI不仅能理解Next.js的最佳实践,还能自动应用TypeScript的类型检查,甚至主动提供Tailwind CSS的类名建议。探索新的应用场景,推动AI辅助编程的边界。

    探索Django 5: 从零开始,打造你的第一个Web应用

    探索Django 5: 从零开始,打造你的第一个Web应用

     服务器/数据库  2个月前  1.13k

    Django 是一个开放源代码的 Web 应用程序框架,由 Python 写成。它遵循 MVT(Model-View-Template)的设计模式,旨在帮助开发者高效地构建复杂且功能丰富的 Web 应用程序。随着每个版本的升级,Django 不断演变,提供更多功能和改进,让开发变得更加便捷。《Django 5 Web应用开发实战》集Django架站基础、项目实践、开发经验于一体,是一本从零基础到精通Django Web企业级开发技术的实战指南《Django 5 Web应用开发实战》内容以。

    MySQL 的mysql_secure_installation安全脚本执行过程介绍

    MySQL 的mysql_secure_installation安全脚本执行过程介绍

     服务器/数据库  2个月前  1.08k

    mysql_secure_installation 是 MySQL 提供的一个安全脚本,用于提高数据库服务器的安全性

    【MySQL基础篇】概述及SQL指令:DDL及DML

    【MySQL基础篇】概述及SQL指令:DDL及DML

     服务器/数据库  2个月前  483

    数据库是长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。数据库不仅仅是数据的简单堆积,而是遵循一定的规则和模式进行组织和管理的。数据库中的数据可以包括文本、数字、图像、音频等各种类型的信息。

    Redis中的哨兵(Sentinel)

    Redis中的哨兵(Sentinel)

     服务器/数据库  2个月前  309

    ​ 上篇文章我们讲述了Redis中的主从复制(Redis分布式系统中的主从复制-CSDN博客),本篇文章针对主从复制中的问题引出Redis中的哨兵,希望本篇文章会对你有所帮助。