文件上传功能几乎是每个 Web 应用不可或缺的一部分。无论是个人博客中的图片上传,还是企业级应用中的文档管理,文件上传都扮演着至关重要的角色。今天,松哥和大家来聊聊文件上传中的几个高级玩法——秒传、断点续传和分片上传。
一 文件上传的常见场景
在日常开发中,文件上传的场景多种多样。比如,在线教育平台上的视频资源上传,社交平台上的图片分享,以及企业内部的知识文档管理等。这些场景对文件上传的要求也各不相同,有的追求速度,有的注重稳定性,还有的需要考虑文件大小和安全性。因此,针对不同需求,我们有了秒传、断点续传和分片上传等解决方案。
二 秒传、断点上传与分片上传
秒传
秒传,顾名思义,就是几乎瞬间完成文件上传的过程。其实现原理是通过计算文件的哈希值(如 MD5 或 SHA-1),然后将这个唯一的标识符发送给服务器。如果服务器上已经存在相同的文件,则直接返回成功信息,避免了重复上传。这种方式不仅节省了带宽,也大大提高了用户体验。
断点续传
断点续传是指在网络不稳定或者用户主动中断上传后,能够从上次中断的地方继续上传,而不需要重新开始整个过程。这对于大文件上传尤为重要,因为它可以有效防止因网络问题导致的上传失败,同时也能节约用户的流量和时间。
分片上传
分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。
三 秒传实战
后端实现
在 SpringBoot 项目中,我们可以使用 MessageDigest
类来计算文件的 MD5 值,然后检查数据库中是否存在该文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @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 值,然后发送给后端进行校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <!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>
|
前端分为两个步骤:
- 计算文件的 MD5 值,计算之后发送给服务端确定文件是否存在。
- 如果文件已经存在,则不需要继续上传文件;如果文件不存在,则开始上传文件,上传文件和 MD5 校验请求类似,上面的案例代码中我就没有重复演示了,松哥在书里和之前的课程里都多次讲过文件上传,这里不再啰嗦。
四 分片上传实战
分片上传关键是在前端对文件切片,比如一个 10MB 的文件切为 10 份,每份 1MB。每次上传的时候,需要多一个参数记录当前上传的文件切片的起始位置。
比如一个 10MB 的文件,切为 10 份,每份 1MB,那么:
- 第 0 片,从 0 开始,一共是
1024*1024
个字节。
- 第 1 片,从
1024*1024
开始,一共是 1024*1024
个字节。
- 第 2 片…
把这个搞懂,后面的代码就好理解了。
后端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/";
@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("上传失败"); } }
|
后端每次处理的时候,需要先设置文件的起始位置。
前端调用
前端需要将文件切分成多个小块,然后依次上传。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <!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; 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 3 4 5 6 7 8 9
| @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,表示前端从头开始上传该文件。
前端调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <!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; 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>
|
这个案例实际上是一个断点续传+分片上传的案例,相关知识点并不难,小伙伴们可以自行体会下。
六 总结
好了,以上就是关于文件上传中秒传、断点续传和分片上传的实战分享。通过这些技术的应用,我们可以极大地提升文件上传的效率和稳定性,改善用户体验。希望各位小伙伴在自己的项目中也能灵活运用这些技巧,解决实际问题。
公众号后台回复 fileupload,可以下载本文完整案例。