大文件分片上传前后端实现

最近在做公司的视频业务,涉及到大视频的上传。
之前的图片、Excel等上传做的很简单,直接表单提交后端用MultipartFile接收保存到磁盘就行了。
但是针对大文件的上传,需要做额外的处理,否则可能会遇到如下问题:

  1. 文件过大,超出服务端的请求大小限制(如SpringMVC,默认文件上传最大1MB)。
  2. 请求的时间过长,请求超时。
  3. 客户端网络不好的话,容易传输中断,必须整个文件重传。

为了解决这些问题,笔者研究了一下,发现可以用分片上传的方式来解决。

前端处理

大文件分片上传,是需要前端和后端配合操作的。
整体流程是:前端将大文件进行分片,例如一个50MB的文件,分成10片,每个片5MB。然后发10个HTTP请求,将这10个分片数据发送给后端,后端根据分片的下标和Size来往磁盘文件的不同位置写入分片数据,10个分片全部写完后即得到一个完整的文件。

前端的处理流程如下:
在这里插入图片描述

前端实战代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.js"></script>
</head>
<body>
    <input type="file" name="file" id="file">
    <button id="upload" onClick="upload()">upload</button>
</body>
</html>

<script>
    const sliceSize = 5 * 1024 * 1024; // 每个文件切片大小定为5MB
    //发送请求
    function upload() {
        const blob = document.getElementById("file").files[0];
        const fileSize = blob.size;// 文件大小
        const fileName = blob.name;// 文件名

        //计算文件切片总数
        const totalSlice = Math.ceil(fileSize / sliceSize);
        // 循环上传
        for (let i = 1; i <= totalSlice; i++) {
            let chunk;
            if (i == totalSlice) {
                // 最后一片
                chunk = blob.slice((i - 1) * sliceSize, fileSize - 1);//切割文件
            } else {
                chunk = blob.slice((i - 1) * sliceSize, i * sliceSize);
            }
            const formData = new FormData();
            formData.append("file", chunk);
            formData.append("md5", md5(blob));
            formData.append("name", fileName);
            formData.append("size", fileSize);
            formData.append("chunks", totalSlice);
            formData.append("chunk", i);
            $.ajax({
                url: 'http://localhost:8080/chunk/upload',
                type: 'POST',
                cache: false,
                data: formData,
                processData: false,
                contentType: false,
                async: false
            });
        }
    }
</script>

笔者这里写了一个比较粗糙的前端例子,市面上有很多优秀的分片上传插件,例如:webuploader


后端处理

后端接收到分片数据后,要根据分片的下标和分片的大小来往文件的指定位置写入分片数据。
例如:分片大小为1MB,第一个分片就要往文件的第0个字节开始,写入1048576字节的数据。第二个分片就要往文件的第1048576个字节开始,写入1048576字节的数据,以此类推。待所有的分片数据全部写入完成后,即得到一个完整的文件。

后端处理流程如下:
在这里插入图片描述

RandomAccessFile

分片数据的写入,需要对文件进行定位,移动访问指针。
JDK提供了java.io.RandomAccessFile类,支持对文件进行随机的读写操作。
在Linux平台上,所有打开的文件都有一个文件描述符(FD),文件描述符自身维护了一个文件偏移量(current file offset),简称cfo,通过lseek函数可以移动文件的读写指针,RandomAccessFile的seek()方法就是调用了Linux的lseek系统函数来实现的。

通过RandomAccessFile.seek()移动访问指针,然后写入分片数据。

后端处理代码如下:

@RestController
public class FileController {
	// 存放文件的临时目录
	private static final String DATA_DIR = System.getProperty("user.dir") + "/temp/";
	// 文件MD5的缓存容器
	private static final ConcurrentMap<String, File> MD5_CACHE = new ConcurrentHashMap<>();

	/**
	 * 大文件分片上传
	 * @param name 文件名
	 * @param md5 文件MD5值
	 * @param size 文件大小
	 * @param chunks 总的分片数
	 * @param chunk 当前分片数
	 * @param multipartFile 分片流
	 * @throws IOException
	 */
	@PostMapping("/chunk/upload")
	public void chunkUpload(String name,
							String md5,
							Long size,
							Integer chunks,
							Integer chunk,
							@RequestParam("file") MultipartFile multipartFile) throws IOException {
		// 是否生成了文件???
		File targetFile = MD5_CACHE.get(md5);
		if (targetFile == null) {
			// 没有生成的话就生成一个新的文件,没有做并发控制,多线程上传会出问题
			targetFile = new File(DATA_DIR, UUID.randomUUID().toString(true) + "." + FileNameUtil.extName(name));
			targetFile.getParentFile().mkdirs();
			MD5_CACHE.put(md5, targetFile);
		}

		// 可以对文件的任意位置进行读写
		RandomAccessFile accessFile = new RandomAccessFile(targetFile, "rw");
		boolean finished = chunk == chunks;//是否最后一片
		if (finished) {
			// 移动指针到指定位置
			accessFile.seek(size - multipartFile.getSize());
		}else {
			accessFile.seek((chunk - 1) * multipartFile.getSize());
		}
		// 写入分片的数据
		accessFile.write(multipartFile.getBytes());
		accessFile.close();

		if (finished) {
			System.out.println("success.");
			// 上传成功
			MD5_CACHE.remove(md5);
		}
	}
}

文件分片上传的所有请求中,必须有一个唯一标识可以将所有的分片数据关联起来,笔者这里用的是文件的MD5值。


你可能感兴趣的文章:

文件分片上传前后端可以通过MD5加密来保证文件传输的完整性和准确性。 前端上传文件之前,可以通过如下代码计算文件的MD5值: ```javascript function getFileMD5(file) { return new Promise((resolve, reject) => { const fileReader = new FileReader() const spark = new SparkMD5.ArrayBuffer() const chunkSize = 2097152 // 每个分片的大小为2MB let currentChunk = 0 fileReader.onload = e => { spark.append(e.target.result) // 对每个分片进行MD5计算 currentChunk++ if (currentChunk < chunks) { loadNext() } else { const result = spark.end() resolve(result) } } fileReader.onerror = () => { reject('文件读取失败') } const chunks = Math.ceil(file.size / chunkSize) function loadNext() { const start = currentChunk * chunkSize const end = start + chunkSize > file.size ? file.size : start + chunkSize const blob = file.slice(start, end) fileReader.readAsArrayBuffer(blob) } loadNext() }) } ``` 后端在接收到文件分片之后,可以对每个分片进行MD5计算,最后将所有分片的MD5值进行合并,得到整个文件的MD5值,如下所示: ```python import hashlib def get_file_md5(file): md5_obj = hashlib.md5() chunk_size = 2097152 # 每个分片的大小为2MB current_chunk = 0 while True: chunk = file.read(chunk_size) if not chunk: break md5_obj.update(chunk) current_chunk += 1 file.seek(0) return md5_obj.hexdigest() def merge_md5(md5_list): md5_obj = hashlib.md5() md5_list.sort() for md5 in md5_list: md5_obj.update(md5.encode()) return md5_obj.hexdigest() ``` 前端文件的MD5值和文件分片一起上传后端后端在接收到所有分片之后,将所有分片的MD5值进行合并,得到整个文件的MD5值,并与前端传过来的MD5值进行比较,如果相同,则说明文件传输完整无误。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值