依星源码资源网,依星资源网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

限时开通VIP永久会员,可免费下载所有附件此广告位出租
查看: 13|回复: 0

一个Demo搞定前后端大文件分片上传、断点续传、秒传

[复制链接] 主动推送

9442

主题

9494

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
10275
发表于 4 天前 | 显示全部楼层 |阅读模式
一个Demo搞定前后端大文件分片上传、断点续传、秒传
1前言
文件上传在项目开发中再常见不过了,大多项目都会涉及到图片、音频、视频、文件的上传,通常简单的一个Form表单就可以上传小文件了,但是遇到大文件时比如1GB以上,或者用户网络比较慢时,简单的文件上传就不能适用了,用户辛苦传了好几十分钟,到最后发现上传失败,这样的系统用户体验是非常差的。

或者用户上传到一半时,把应用退出了,下次进来再次上传,如果让他从头开始传也是不合理的。本文主要通过一个Demo从前端、后端用实战代码演示小文件上传、大文件分片上传、断点续传、秒传的开发原理。

2小文件上传
小文件小传非常的简单,本项目后端我们使用SpringBoot 3.1.2 + JDK17,前端我们使用原生的JavaScript+spark-md5.min.js实现。

后端代码
POM.xml使用springboot3.1.2JAVA版本使用JDK17

  1. <parent>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-parent</artifactId>
  4.     <version>3.1.2</version>
  5.     <relativePath/> <!-- lookup parent from repository -->
  6. </parent>
  7. <groupId>com.example</groupId>
  8. <artifactId>uploadDemo</artifactId>
  9. <version>0.0.1-SNAPSHOT</version>
  10. <name>uploadDemo</name>
  11. <description>uploadDemo</description>
  12. <properties>
  13.     <java.version>17</java.version>
  14. </properties>
  15. <dependencies>
  16.     <dependency>
  17.         <groupId>org.springframework.boot</groupId>
  18.         <artifactId>spring-boot-starter-web</artifactId>
  19.     </dependency>
  20. </dependencies>
  21. <build>
  22.     <plugins>
  23.         <plugin>
  24.             <groupId>org.springframework.boot</groupId>
  25.             <artifactId>spring-boot-maven-plugin</artifactId>
  26.         </plugin>
  27.     </plugins>
  28. </build>
复制代码
JAVA接文件接口:
  1. @RestController
  2. public class UploadController {

  3.     public static final String UPLOAD_PATH = "D:\\upload\";

  4.     @RequestMapping("/upload")
  5.     public ResponseEntity<Map<String, String>> upload(@RequestParam MultipartFile file) throws IOException {
  6.         File dstFile = new File(UPLOAD_PATH, String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));
  7.         file.transferTo(dstFile);
  8.         return ResponseEntity.ok(Map.of("path", dstFile.getAbsolutePath()));
  9.     }

  10. }
复制代码
前端代码
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>upload</title>
  6. </head>
  7. <body>
  8. upload

  9. <form enctype="multipart/form-data">
  10.     <input type="file" name="fileInput" id="fileInput">
  11.     <input type="button" value="上传" onclick="uploadFile()">
  12. </form>

  13. 上传结果
  14. <span id="uploadResult"></span>

  15. <script>
  16.     var  uploadResult=document.getElementById("uploadResult")
  17.     function uploadFile() {
  18.         var fileInput = document.getElementById('fileInput');
  19.         var file = fileInput.files[0];
  20.         if (!file) return; // 没有选择文件

  21.         var xhr = new XMLHttpRequest();
  22.         // 处理上传进度
  23.         xhr.upload.onprogress = function(event) {
  24.             var percent = 100 * event.loaded / event.total;
  25.             uploadResult.innerHTML='上传进度:' + percent + '%';
  26.         };
  27.         // 当上传完成时调用
  28.         xhr.onload = function() {
  29.             if (xhr.status === 200) {
  30.                 uploadResult.innerHTML='上传成功'+ xhr.responseText;
  31.             }
  32.         }
  33.         xhr.onerror = function() {
  34.             uploadResult.innerHTML='上传失败';
  35.         }
  36.         // 发送请求
  37.         xhr.open('POST', '/upload', true);
  38.         var formData = new FormData();
  39.         formData.append('file', file);
  40.         xhr.send(formData);
  41.     }
  42. </script>

  43. </body>
  44. </html>
复制代码

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

注意事项
在上传过程会报文件大小限制错误,主要有三个参数需要设置:

  1. org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (46302921) exceeds the configured maximum (10485760)
复制代码
这里需在springboot的application.properties 或者application.yml中添加max-file-size和max-request-size配置项,默认大小分别是1M和10M,肯定不能满足我们上传需求的。
  1. spring.servlet.multipart.max-file-size=1024MB  
  2. spring.servlet.multipart.max-request-size=1024MB
复制代码
如果使用nginx报 413状态码413 Request Entity Too Large,Nginx默认最大上传1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置项:client_max_body_size 1024m。

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

3大文件分片上传
前端
前端上传流程
大文件分片上传前端主要有三步:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

前端上传代码计算文件MD5值用了spark-md5这个库,使用也是比较简单的。这里为什么要计算MD5简单说一下,因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性。
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>分片上传</title>
  6.     <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
  7. </head>
  8. <body>
  9. 分片上传

  10. <form enctype="multipart/form-data">
  11.     <input type="file" name="fileInput" id="fileInput">
  12.     <input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
  13.     <input type="button" value="上传" onclick="uploadFile()">
  14.     <input type="button" value="检测文件完整性" onclick="checkFile()">
  15. </form>

  16. <p>
  17.     文件MD5:
  18.     <span id="fileMd5"></span>
  19. </p>
  20. <p>
  21.     上传结果:
  22.     <span id="uploadResult"></span>
  23. </p>
  24. <p>
  25.     检测文件完整性:
  26.     <span id="checkFileRes"></span>
  27. </p>


  28. <script>
  29.     //每片的大小
  30.     var chunkSize = 1 * 1024 * 1024;
  31.     var uploadResult = document.getElementById("uploadResult")
  32.     var fileMd5Span = document.getElementById("fileMd5")
  33.     var checkFileRes = document.getElementById("checkFileRes")
  34.     var  fileMd5;


  35.     function  calculateFileMD5(){
  36.         var fileInput = document.getElementById('fileInput');
  37.         var file = fileInput.files[0];
  38.         getFileMd5(file).then((md5) => {
  39.             console.info(md5)
  40.             fileMd5=md5;
  41.             fileMd5Span.innerHTML=md5;
  42.         })
  43.     }

  44.     function uploadFile() {
  45.         var fileInput = document.getElementById('fileInput');
  46.         var file = fileInput.files[0];
  47.         if (!file) return;
  48.         if (!fileMd5) return;


  49.         //获取到文件
  50.         let fileArr = this.sliceFile(file);
  51.         //保存文件名称
  52.         let fileName = file.name;

  53.         fileArr.forEach((e, i) => {
  54.             //创建formdata对象
  55.             let data = new FormData();
  56.             data.append("totalNumber", fileArr.length)
  57.             data.append("chunkSize", chunkSize)
  58.             data.append("chunkNumber", i)
  59.             data.append("md5", fileMd5)
  60.             data.append("file", new File([e],fileName));
  61.             upload(data);
  62.         })


  63.     }

  64.     /**
  65.      * 计算文件md5值
  66.      */
  67.     function getFileMd5(file) {
  68.         return new Promise((resolve, reject) => {
  69.             let fileReader = new FileReader()
  70.             fileReader.onload = function (event) {
  71.                 let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
  72.                 resolve(fileMd5)
  73.             }
  74.             fileReader.readAsArrayBuffer(file)
  75.         })
  76.     }


  77.    function upload(data) {
  78.        var xhr = new XMLHttpRequest();
  79.        // 当上传完成时调用
  80.        xhr.onload = function () {
  81.            if (xhr.status === 200) {
  82.                uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
  83.            }
  84.        }
  85.        xhr.onerror = function () {
  86.            uploadResult.innerHTML = '上传失败';
  87.        }
  88.        // 发送请求
  89.        xhr.open('POST', '/uploadBig', true);
  90.        xhr.send(data);
  91.     }

  92.     function checkFile() {
  93.         var xhr = new XMLHttpRequest();
  94.         // 当上传完成时调用
  95.         xhr.onload = function () {
  96.             if (xhr.status === 200) {
  97.                 checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
  98.             }
  99.         }
  100.         xhr.onerror = function () {
  101.             checkFileRes.innerHTML = '检测文件完整性失败';
  102.         }
  103.         // 发送请求
  104.         xhr.open('POST', '/checkFile', true);
  105.         let data = new FormData();
  106.         data.append("md5", fileMd5)
  107.         xhr.send(data);
  108.     }

  109.     function sliceFile(file) {
  110.         const chunks = [];
  111.         let start = 0;
  112.         let end;
  113.         while (start < file.size) {
  114.             end = Math.min(start + chunkSize, file.size);
  115.             chunks.push(file.slice(start, end));
  116.             start = end;
  117.         }
  118.         return chunks;
  119.     }

  120. </script>

  121. </body>
  122. </html>
复制代码
前端注意事项
前端调用uploadBig接口有四个参数:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

计算大文件的MD5可能会比较慢,这个可以从流程上进行优化,比如上传使用异步去计算文件MD5、不计算整个文件MD5而是计算每一片的MD5保证每一片数据的一致性。

后端
后端就两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5。

/uploadBig接口设计思路
接口总体流程:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

这里需要注意的:

MD5.conf每一次检测文件不存在里创建个空文件,使用byte[] bytes = new byte[totalNumber];将每一位状态设置为0,从0位天始,第N位表示第N个分片的上传状态,0-未上传 1-已上传,当每将上传成功后使用randomAccessConfFile.seek(chunkNumber)将对就设置为1。

randomAccessFile.seek(chunkNumber * chunkSize);可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题。

大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的。

MappedByteBuffer写文件的用法:

  1. FileChannel fileChannel = randomAccessFile.getChannel();  
  2. MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkNumber * chunkSize, fileData.length);  
  3. mappedByteBuffer.put(fileData);
复制代码
/checkFile接口设计思路

[color=rgba(0, 0, 0, 0.9)]/checkFile接口流程:

[color=rgba(0, 0, 0, 0.9)]

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

大文件上传完整JAVA代码:

  1. @RestController
  2. public class UploadController {

  3.     public static final String UPLOAD_PATH = "D:\\upload\";

  4.     /**
  5.      * @param chunkSize   每个分片大小
  6.      * @param chunkNumber 当前分片
  7.      * @param md5         文件总MD5
  8.      * @param file        当前分片文件数据
  9.      * @return
  10.      * @throws IOException
  11.      */
  12.     @RequestMapping("/uploadBig")
  13.     public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {
  14.         //文件存放位置
  15.         String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
  16.         //上传分片信息存放位置
  17.         String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
  18.         //第一次创建分片记录文件
  19.         //创建目录
  20.         File dir = new File(dstFile).getParentFile();
  21.         if (!dir.exists()) {
  22.             dir.mkdir();
  23.             //所有分片状态设置为0
  24.             byte[] bytes = new byte[totalNumber];
  25.             Files.write(Path.of(confFile), bytes);
  26.         }
  27.         //随机分片写入文件
  28.         try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
  29.              RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
  30.              InputStream inputStream = file.getInputStream()) {
  31.             //定位到该分片的偏移量
  32.             randomAccessFile.seek(chunkNumber * chunkSize);
  33.             //写入该分片数据
  34.             randomAccessFile.write(inputStream.readAllBytes());
  35.             //定位到当前分片状态位置
  36.             randomAccessConfFile.seek(chunkNumber);
  37.             //设置当前分片上传状态为1
  38.             randomAccessConfFile.write(1);
  39.         }
  40.         return ResponseEntity.ok(Map.of("path", dstFile));
  41.     }


  42.     /**
  43.      * 获取文件分片状态,检测文件MD5合法性
  44.      *
  45.      * @param md5
  46.      * @return
  47.      * @throws Exception
  48.      */
  49.     @RequestMapping("/checkFile")
  50.     public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {
  51.         String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
  52.         Path path = Path.of(uploadPath);
  53.         //MD5目录不存在文件从未上传过
  54.         if (!Files.exists(path.getParent())) {
  55.             return ResponseEntity.ok(Map.of("msg", "文件未上传"));
  56.         }
  57.         //判断文件是否上传成功
  58.         StringBuilder stringBuilder = new StringBuilder();
  59.         byte[] bytes = Files.readAllBytes(path);
  60.         for (byte b : bytes) {
  61.             stringBuilder.append(String.valueOf(b));
  62.         }
  63.         //所有分片上传完成计算文件MD5
  64.         if (!stringBuilder.toString().contains("0")) {
  65.             File file = new File(String.format("%s\\%s\", UPLOAD_PATH, md5));
  66.             File[] files = file.listFiles();
  67.             String filePath = "";
  68.             for (File f : files) {
  69.                 //计算文件MD5是否相等
  70.                 if (!f.getName().contains("conf")) {
  71.                     filePath = f.getAbsolutePath();
  72.                     try (InputStream inputStream = new FileInputStream(f)) {
  73.                         String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
  74.                         if (!md5pwd.equalsIgnoreCase(md5)) {
  75.                             return ResponseEntity.ok(Map.of("msg", "文件上传失败"));
  76.                         }
  77.                     }
  78.                 }
  79.             }
  80.             return ResponseEntity.ok(Map.of("path", filePath));
  81.         } else {
  82.             //文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传
  83.             return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
  84.         }

  85.     }
  86.    
  87. }
复制代码

配合前端上传演示分片上传,依次按如下流程点击按钮:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

断点续传

有了上面的设计做断点续传就比较简单的,后端代码不需要改变,只要修改前端上传流程就好了:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

用/checkFile接口,文件里如果有未完成上传的分片,接口返回chunks字段对就的位置值为0,前端将未上传的分片继续上传,完成后再调用/checkFile就完成了断点续传

  1. {
  2.     "chucks": "111111111100000000001111111111111111111111111"
  3. }
复制代码

秒传

秒传也是比较简单的,只要修改前端代码流程就好了,比如张三上传了一个文件,然后李四又上传了同样内容的文件,同一文件的MD5值可以认为是一样的(虽然会存在不同文件的MD5一样,不过概率很小,可以认为MD5一样文件就是一样),10万不同文件MD5相同概率为110000000000000000000000000000\frac{1}{10000000000000000000000000000}100000000000000000000000000001,福利彩票的中头奖的概率一般为11000000\frac{1}{1000000}10000001,具体计算方法可以参考走近消息摘要--Md5产生重复的概率,所以MD5冲突的概率可以忽略不计。

当李四调用/checkFile接口后,后端直接返回了李四上传的文件路径,李四就完成了秒传。大部分云盘秒传的思路应该也是这样,只不过计算文件HASH算法更为复杂,返回给用户文件路径也更为安全,要防止被别人算出文件路径了。

秒传前端代码流程:

一个Demo搞定前后端大文件分片上传、断点续传、秒传

一个Demo搞定前后端大文件分片上传、断点续传、秒传

4总结

本文从前端和后端两个方面介绍了大文件的分片上传、断点继续、秒传设计思路和实现代码,所有代码都是亲测可以直接使用。



相关帖子

扫码关注微信公众号,及时获取最新资源信息!下载附件优惠VIP会员5折;永久VIP免费
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

免责声明:
1、本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
2、本站所有内容均由互联网收集整理、网友上传,并且以计算机技术研究交流为目的,仅供大家参考、学习,请勿任何商业目的与商业用途。
3、若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
4、论坛的所有内容都不保证其准确性,完整性,有效性,由于源码具有复制性,一经售出,概不退换。阅读本站内容因误导等因素而造成的损失本站不承担连带责任。
5、用户使用本网站必须遵守适用的法律法规,对于用户违法使用本站非法运营而引起的一切责任,由用户自行承担
6、本站所有资源来自互联网转载,版权归原著所有,用户访问和使用本站的条件是必须接受本站“免责声明”,如果不遵守,请勿访问或使用本网站
7、本站使用者因为违反本声明的规定而触犯中华人民共和国法律的,一切后果自己负责,本站不承担任何责任。
8、凡以任何方式登陆本网站或直接、间接使用本网站资料者,视为自愿接受本网站声明的约束。
9、本站以《2013 中华人民共和国计算机软件保护条例》第二章 “软件著作权” 第十七条为原则:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。若有学员需要商用本站资源,请务必联系版权方购买正版授权!
10、本网站如无意中侵犯了某个企业或个人的知识产权,请来信【站长信箱312337667@qq.com】告之,本站将立即删除。
郑重声明:
本站所有资源仅供用户本地电脑学习源代码的内含设计思想和原理,禁止任何其他用途!
本站所有资源、教程来自互联网转载,仅供学习交流,不得商业运营资源,不确保资源完整性,图片和资源仅供参考,不提供任何技术服务。
本站资源仅供本地编辑研究学习参考,禁止未经资源商正版授权参与任何商业行为,违法行为!如需商业请购买各资源商正版授权
本站仅收集资源,提供用户自学研究使用,本站不存在私自接受协助用户架设游戏或资源,非法运营资源行为。
 
在线客服
点击这里给我发消息 点击这里给我发消息 点击这里给我发消息
售前咨询热线
312337667

微信扫一扫,私享最新原创实用干货

QQ|免责声明|小黑屋|依星资源网 ( 鲁ICP备2021043233号-3 )|网站地图

GMT+8, 2024-9-1 08:14

Powered by Net188.com X3.4

邮箱:312337667@qq.com 客服QQ:312337667(工作时间:9:00~21:00)

快速回复 返回顶部 返回列表