⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 江南一点雨 「江南一点雨」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

文件上传功能几乎是每个 Web 应用不可或缺的一部分。无论是个人博客中的图片上传,还是企业级应用中的文档管理,文件上传都扮演着至关重要的角色。今天,松哥和大家来聊聊文件上传中的几个高级玩法——秒传、断点续传和分片上传。

一 文件上传的常见场景

在日常开发中,文件上传的场景多种多样。比如,在线教育平台上的视频资源上传,社交平台上的图片分享,以及企业内部的知识文档管理等。这些场景对文件上传的要求也各不相同,有的追求速度,有的注重稳定性,还有的需要考虑文件大小和安全性。因此,针对不同需求,我们有了秒传、断点续传和分片上传等解决方案。

二 秒传、断点上传与分片上传

秒传

秒传,顾名思义,就是几乎瞬间完成文件上传的过程。其实现原理是通过计算文件的哈希值(如 MD5 或 SHA-1),然后将这个唯一的标识符发送给服务器。如果服务器上已经存在相同的文件,则直接返回成功信息,避免了重复上传。这种方式不仅节省了带宽,也大大提高了用户体验。

断点续传

断点续传是指在网络不稳定或者用户主动中断上传后,能够从上次中断的地方继续上传,而不需要重新开始整个过程。这对于大文件上传尤为重要,因为它可以有效防止因网络问题导致的上传失败,同时也能节约用户的流量和时间。

分片上传

分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。

三 秒传实战

后端实现

在 SpringBoot 项目中,我们可以使用 MessageDigest 类来计算文件的 MD5 值,然后检查数据库中是否存在该文件。

@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
FileService fileService;

@PostMapping("/upload1")
public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5) {
try {
// 检查数据库中是否已存在该文件
if (fileService.existsByMd5(md5)) {
return ResponseEntity.ok("文件已存在");
}
// 保存文件到服务器
file.transferTo(new File("/path/to/save/" + file.getOriginalFilename()));
// 保存文件信息到数据库
fileService.save(new FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream())));
return ResponseEntity.ok("上传成功");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传失败");
}
}
}

前端调用

前端可以通过 JavaScript 的 FileReader API 读取文件内容,通过 spark-md5 计算 MD5 值,然后发送给后端进行校验。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>秒传</title>
<script src="spark-md5.js"></script>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">开始上传</button>
<hr>
<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const md5 = await calculateMd5(file);
const formData = new FormData();
formData.append('md5', md5);

const response = await fetch('/file/upload1', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
if (result != "文件已存在") {
// 开始上传文件
}
} else {
console.error("上传失败: " + result);
}
}

function calculateMd5(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const spark = new SparkMD5.ArrayBuffer();
spark.append(reader.result);
resolve(spark.end());
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
</script>
</body>
</html>

前端分为两个步骤:

  1. 计算文件的 MD5 值,计算之后发送给服务端确定文件是否存在。
  2. 如果文件已经存在,则不需要继续上传文件;如果文件不存在,则开始上传文件,上传文件和 MD5 校验请求类似,上面的案例代码中我就没有重复演示了,松哥在书里和之前的课程里都多次讲过文件上传,这里不再啰嗦。

四 分片上传实战

分片上传关键是在前端对文件切片,比如一个 10MB 的文件切为 10 份,每份 1MB。每次上传的时候,需要多一个参数记录当前上传的文件切片的起始位置。

比如一个 10MB 的文件,切为 10 份,每份 1MB,那么:

  • 第 0 片,从 0 开始,一共是 1024*1024 个字节。
  • 第 1 片,从 1024*1024 开始,一共是 1024*1024 个字节。
  • 第 2 片…

把这个搞懂,后面的代码就好理解了。

后端实现

private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/";
/**
* 上传文件到指定位置
*
* @param file 上传的文件
* @param start 文件开始上传的位置
* @return ResponseEntity<String> 上传结果
*/
@PostMapping("/upload2")
public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start,@RequestParam("fileName") String fileName) {
try {
File directory = new File(UPLOAD_DIR);
if (!directory.exists()) {
directory.mkdirs();
}
File targetFile = new File(UPLOAD_DIR + fileName);
RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");
FileChannel channel = randomAccessFile.getChannel();
channel.position(start);
channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());
channel.close();
randomAccessFile.close();
return ResponseEntity.ok("上传成功");
} catch (Exception e) {
System.out.println("上传失败: "+e.getMessage());
return ResponseEntity.status(500).body("上传失败");
}
}

后端每次处理的时候,需要先设置文件的起始位置。

前端调用

前端需要将文件切分成多个小块,然后依次上传。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片示例</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">开始上传</button>

<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const filename = file.name;
let start = 0;

uploadFile(file, start);
}

async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每个分片1MB
const total = Math.ceil(file.size / chunkSize);

for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);

const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);

const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上传成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);
break;
}
}
}
</script>
</body>
</html>

五 断点续传实战

断点续传的技术原理类似于分片上传。

当文件已经上传了一部分之后,断了需要重新开始上传。

那么我们的思路是这样的:

  1. 前端先发送一个请求,检查要上传的文件在服务端是否已经存在,如果存在,目前大小是多少。
  2. 前端根据已经存在的大小,继续上传文件即可。

后端案例

先来看后端检查的接口,如下:

@GetMapping("/check")
public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename) {
File file = new File(UPLOAD_DIR + filename);
if (file.exists()) {
return ResponseEntity.ok(file.length());
} else {
return ResponseEntity.ok(0L);
}
}

如果文件存在,则返回已经存在的文件大小。

如果文件不存在,则返回 0,表示前端从头开始上传该文件。

前端调用

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>断点续传示例</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button onclick="startUpload()">开始上传</button>

<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const filename = file.name;
let start = await checkFile(filename);

uploadFile(file, start);
}

async function checkFile(filename) {
const response = await fetch(`/file/check?filename=${filename}`);
const start = await response.json();
return start;
}

async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每个分片1MB
const total = Math.ceil((file.size - start) / chunkSize);

for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);

const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);

const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上传成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);
break;
}
}
}
</script>
</body>
</html>

这个案例实际上是一个断点续传+分片上传的案例,相关知识点并不难,小伙伴们可以自行体会下。

六 总结

好了,以上就是关于文件上传中秒传、断点续传和分片上传的实战分享。通过这些技术的应用,我们可以极大地提升文件上传的效率和稳定性,改善用户体验。希望各位小伙伴在自己的项目中也能灵活运用这些技巧,解决实际问题。

文章目录
  1. 1. 一 文件上传的常见场景
  2. 2. 二 秒传、断点上传与分片上传
    1. 2.1. 秒传
    2. 2.2. 断点续传
    3. 2.3. 分片上传
  3. 3. 三 秒传实战
    1. 3.1. 后端实现
    2. 3.2. 前端调用
  4. 4. 四 分片上传实战
    1. 4.1. 后端实现
    2. 4.2. 前端调用
  5. 5. 五 断点续传实战
    1. 5.1. 后端案例
    2. 5.2. 前端调用
  6. 6. 六 总结