首页 > 编程语言 >Java 小文件上传、大文件分片上传、断点续传、秒传的开发原理

Java 小文件上传、大文件分片上传、断点续传、秒传的开发原理

时间:2023-11-13 14:34:38浏览次数:42  
标签:文件 Java xhr file 分片 上传 MD5

1、前言

 

文件上传在项目开发中再常见不过了,大多项目都会涉及到图片、音频、视频、文件的上传,通常简单的一个Form表单就可以上传小文件了,但是遇到大文件时比如1GB以上,或者用户网络比较慢时,简单的文件上传就不能适用了,用户辛苦传了好几十分钟,到最后发现上传失败,这样的系统用户体验是非常差的。

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

 

2、小文件上传

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

后端代码

POM.xml使用springboot3.1.2JAVA版本使用JDK17

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>uploadDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>uploadDemo</name>
<description>uploadDemo</description>
<properties>
    <java.version>17</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

JAVA接文件接口:

@RestController
public class UploadController {

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

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

}

 

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</head>
<body>
upload

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

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

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

        var xhr = new XMLHttpRequest();
        // 处理上传进度
        xhr.upload.onprogress = function(event) {
            var percent = 100 * event.loaded / event.total;
            uploadResult.innerHTML='上传进度:' + percent + '%';
        };
        // 当上传完成时调用
        xhr.onload = function() {
            if (xhr.status === 200) {
                uploadResult.innerHTML='上传成功'+ xhr.responseText;
            }
        }
        xhr.onerror = function() {
            uploadResult.innerHTML='上传失败';
        }
        // 发送请求
        xhr.open('POST', '/upload', true);
        var formData = new FormData();
        formData.append('file', file);
        xhr.send(formData);
    }
</script>

</body>
</html>
图片

 

注意事项

 

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

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-sizemax-request-size配置项,默认大小分别是1M和10M,肯定不能满足我们上传需求的。

spring.servlet.multipart.max-file-size=1024MB  
spring.servlet.multipart.max-request-size=1024MB

如果使用nginx报 413状态码413 Request Entity Too Large,Nginx默认最大上传1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置项:client_max_body_size 1024m

图片

 

3、大文件分片上传

 

前端

前端上传流程

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

图片

前端上传代码计算文件MD5值用了spark-md5这个库,使用也是比较简单的。这里为什么要计算MD5简单说一下,因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>分片上传</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
分片上传

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

<p>
    文件MD5:
    <span id="fileMd5"></span>
</p>
<p>
    上传结果:
    <span id="uploadResult"></span>
</p>
<p>
    检测文件完整性:
    <span id="checkFileRes"></span>
</p>


<script>
    //每片的大小
    var chunkSize = 1 * 1024 * 1024;
    var uploadResult = document.getElementById("uploadResult")
    var fileMd5Span = document.getElementById("fileMd5")
    var checkFileRes = document.getElementById("checkFileRes")
    var  fileMd5;


    function  calculateFileMD5(){
        var fileInput = document.getElementById('fileInput');
        var file = fileInput.files[0];
        getFileMd5(file).then((md5) => {
            console.info(md5)
            fileMd5=md5;
            fileMd5Span.innerHTML=md5;
        })
    }

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


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

        fileArr.forEach((e, i) => {
            //创建formdata对象
            let data = new FormData();
            data.append("totalNumber", fileArr.length)
            data.append("chunkSize", chunkSize)
            data.append("chunkNumber", i)
            data.append("md5", fileMd5)
            data.append("file", new File([e],fileName));
            upload(data);
        })


    }

    /**
     * 计算文件md5值
     */
    function getFileMd5(file) {
        return new Promise((resolve, reject) => {
            let fileReader = new FileReader()
            fileReader.onload = function (event) {
                let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
                resolve(fileMd5)
            }
            fileReader.readAsArrayBuffer(file)
        })
    }


   function upload(data) {
       var xhr = new XMLHttpRequest();
       // 当上传完成时调用
       xhr.onload = function () {
           if (xhr.status === 200) {
               uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
           }
       }
       xhr.onerror = function () {
           uploadResult.innerHTML = '上传失败';
       }
       // 发送请求
       xhr.open('POST', '/uploadBig', true);
       xhr.send(data);
    }

    function checkFile() {
        var xhr = new XMLHttpRequest();
        // 当上传完成时调用
        xhr.onload = function () {
            if (xhr.status === 200) {
                checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
            }
        }
        xhr.onerror = function () {
            checkFileRes.innerHTML = '检测文件完整性失败';
        }
        // 发送请求
        xhr.open('POST', '/checkFile', true);
        let data = new FormData();
        data.append("md5", fileMd5)
        xhr.send(data);
    }

    function sliceFile(file) {
        const chunks = [];
        let start = 0;
        let end;
        while (start < file.size) {
            end = Math.min(start + chunkSize, file.size);
            chunks.push(file.slice(start, end));
            start = end;
        }
        return chunks;
    }

</script>

</body>
</html>
前端注意事项

前端调用uploadBig接口有四个参数:

图片

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

 

后端

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

/uploadBig接口设计思路

接口总体流程:

图片

这里需要注意的:

  • 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写文件的用法:

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

/checkFile接口流程:

图片

大文件上传完整JAVA代码:

@RestController
public class UploadController {

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

    /**
     * @param chunkSize   每个分片大小
     * @param chunkNumber 当前分片
     * @param md5         文件总MD5
     * @param file        当前分片文件数据
     * @return
     * @throws IOException
     */
    @RequestMapping("/uploadBig")
    public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {
        //文件存放位置
        String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
        //上传分片信息存放位置
        String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        //第一次创建分片记录文件
        //创建目录
        File dir = new File(dstFile).getParentFile();
        if (!dir.exists()) {
            dir.mkdir();
            //所有分片状态设置为0
            byte[] bytes = new byte[totalNumber];
            Files.write(Path.of(confFile), bytes);
        }
        //随机分片写入文件
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
             RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
             InputStream inputStream = file.getInputStream()) {
            //定位到该分片的偏移量
            randomAccessFile.seek(chunkNumber * chunkSize);
            //写入该分片数据
            randomAccessFile.write(inputStream.readAllBytes());
            //定位到当前分片状态位置
            randomAccessConfFile.seek(chunkNumber);
            //设置当前分片上传状态为1
            randomAccessConfFile.write(1);
        }
        return ResponseEntity.ok(Map.of("path", dstFile));
    }


    /**
     * 获取文件分片状态,检测文件MD5合法性
     *
     * @param md5
     * @return
     * @throws Exception
     */
    @RequestMapping("/checkFile")
    public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {
        String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        Path path = Path.of(uploadPath);
        //MD5目录不存在文件从未上传过
        if (!Files.exists(path.getParent())) {
            return ResponseEntity.ok(Map.of("msg", "文件未上传"));
        }
        //判断文件是否上传成功
        StringBuilder stringBuilder = new StringBuilder();
        byte[] bytes = Files.readAllBytes(path);
        for (byte b : bytes) {
            stringBuilder.append(String.valueOf(b));
        }
        //所有分片上传完成计算文件MD5
        if (!stringBuilder.toString().contains("0")) {
            File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
            File[] files = file.listFiles();
            String filePath = "";
            for (File f : files) {
                //计算文件MD5是否相等
                if (!f.getName().contains("conf")) {
                    filePath = f.getAbsolutePath();
                    try (InputStream inputStream = new FileInputStream(f)) {
                        String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
                        if (!md5pwd.equalsIgnoreCase(md5)) {
                            return ResponseEntity.ok(Map.of("msg", "文件上传失败"));
                        }
                    }
                }
            }
            return ResponseEntity.ok(Map.of("path", filePath));
        } else {
            //文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传
            return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
        }

    }
    
}

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

图片图片

 

断点续传

 

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

图片图片

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

{
    "chucks": "111111111100000000001111111111111111111111111"
}

 

秒传

 

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

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

秒传前端代码流程:

图片图片

 

4、总结

 

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

 

 

非常感谢原文作者,源地址连接:https://mp.weixin.qq.com/s/JF6RPZ-aYHSdov4G4q81IQ

标签:文件,Java,xhr,file,分片,上传,MD5
From: https://www.cnblogs.com/sguozeng/p/17829002.html

相关文章

  • python 脚本打包成exe可运行文件
    在Python 3中使用Tkinter编写GUI应用程序既简单又有趣。然而,如果你想与其他人分享你的应用程序,那么你需要将源代码和必要的库文件一起打包成一个可执行文件。本文将介绍如何使用pyinstaller将Python 3脚本打包成一个.exe文件并将Tkinter应用程序部署到其他计算机上。安装pyinstal......
  • ubuntu开发之安装QT creater出现文本文件忙解决办法
    问题:问题解析:其实就跟windows下面,该文件已被打开,无法删除是一样的道理。解决办法:①找到该文件被那个进程所进行着指令如下:【可知被2537进程使用】sudofuser文件名②杀死该进程sudokill-9进程号现象:......
  • Linux修改文件名命令是什么?
    Linux命令是用于在Linux操作系统中执行各种任务和操作的指令。在Linux中,提供了很多命令可以帮助我们完成各种各样的操作,比如重启网卡、修改文件名、复制目录或文件等,那么Linux修改文件名命令是什么?我们简单来介绍一下。在Linux系统中,有多种命令可以用来修改文件名。以下是......
  • [EFI]技嘉 Z490 VISION G i5-10500 电脑 Hackintosh 黑苹果引导文件
    硬件配置硬件型号驱动情况主板技嘉Z490VISIONGCLPCcontrollerZ490芯片组)处理器英特尔[email protected]六核已驱动内存16GB(威到DDR42655MHz8GBx2〕已驱动硬盘SSDSC2BB150G7R(150GB/国态硬盘)已驱动显卡AMDRadeonRX580(华硕)已驱动声卡瑞昱@英特尔HighDefini......
  • java中String、StringBuffer和StringBuilder的区别(简单介绍)
    简单介绍java中用于处理字符串常用的有三个类:1、java.lang.String2、java.lang.StringBuffer3、java.lang.StrungBuilder三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被使用着,且考虑到防止其中的参数被参数修改影响到其他的应用......
  • java(StringBuilder)练习
    需求:键盘录入一个九位数以下的数字将该数字各数字变为罗马数字。importjava.util.Scanner;publicclassBoke{publicstaticvoidmain(String[]args){ScannerSc=newScanner(System.in);Stringx;while(true){System.out.......
  • 问题:类文件具有错误的版本 61.0, 应为 52.0
    1.问题在配置SpringBoot项目时,使用了SpringBoot3,jdk版本为jdk1.8,报错:java:无法访问org.springframework.boot.SpringApplication错误的类文件:/G:/tools/Maven/maven-repository/org/springframework/boot/spring-boot/3.1.2/spring-boot-3.1.2.jar!/org/springframework......
  • 如何将视频上传到可用于HTML5 <video> 标签的 'src' 属性的平台?
    要将视频上传到可用于HTML5<video>标签的'src'属性的平台,您需要遵循以下步骤:1.选择一个支持视频上传的平台,例如YouTube、Vimeo或自建服务器。2.注册并登录到所选平台。3.创建一个新的视频项目或上传您的视频文件。4.获取视频的嵌入代码或URL。这通常可以在平台的管理页......
  • Java项目配置Maven依赖时不知需要的最低jdk版本?(报错java: 错误: 无效的目标发行版:17)
    1.问题在配置SpringBoot项目依赖时,使用了最新的spring-boot-starter-parent3.1.5,但是出现了java:错误:无效的目标发行版:17的报错2.解决经过查阅资料后得知是jdk版本不匹配导致的错误,那我们如何得知相应依赖需要的最低版本需求,并进行配置呢?Maven依赖仓库1.登陆进这个网站后......
  • JavaScript 基础知识
    运营商 运算符对数据进行操作。它们有多种形式,包括算术运算符(+、-、*、/)、比较运算符(==、!=、<、>)和逻辑运算符(&&、||、!) 控制流语句 控制流语句根据特定条件控制代码块的执行顺序。它们包括if-else语句、switch语句和循环。数组 数组是数据项的有序集合。它们允许您......