大家好,我是小悟。
- 什么是大文件上传
大文件上传通常指上传超过几百MB甚至几个GB的文件。与普通文件上传相比,大文件上传面临以下挑战:
- 内存限制 - 一次性加载整个文件到内存会导致内存溢出
- 网络稳定性 - 上传过程中网络中断需要能够断点续传
- 超时问题 - 长时间上传可能导致连接超时
- 进度监控 - 需要实时显示上传进度
- 文件校验 - 确保文件完整性和安全性
解决方案:分片上传
大文件上传的核心思想是将文件分割成多个小块,分别上传,最后在服务器端合并。
前端代码示例 (HTML + JavaScript)
<!DOCTYPE html> <html> <head> <title>大文件上传</title> </head> <body> <input type="file" id="fileInput" /> <button onclick="uploadFile()">开始上传</button> <div id="progress"></div> <script> const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB async function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('请选择文件'); return; } const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const fileMd5 = await calculateFileMD5(file); // 检查文件是否已上传过 const checkResult = await checkFileExists(file.name, fileMd5, file.size); if (checkResult.uploaded) { alert('文件已存在'); return; } let uploadedChunks = checkResult.uploadedChunks || []; for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { // 跳过已上传的分片 if (uploadedChunks.includes(chunkIndex)) { updateProgress(chunkIndex + 1, totalChunks); continue; } const chunk = file.slice(chunkIndex * CHUNK_SIZE, (chunkIndex + 1) * CHUNK_SIZE); const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('fileName', file.name); formData.append('fileMd5', fileMd5); try { await uploadChunk(formData); updateProgress(chunkIndex + 1, totalChunks); } catch (error) { console.error(`分片 ${chunkIndex} 上传失败:`, error); alert('上传失败'); return; } } // 所有分片上传完成,请求合并 await mergeChunks(file.name, fileMd5, totalChunks); alert('上传完成'); } function uploadChunk(formData) { return fetch('/upload/chunk', { method: 'POST', body: formData }).then(response => { if (!response.ok) { throw new Error('上传失败'); } return response.json(); }); } function checkFileExists(fileName, fileMd5, fileSize) { return fetch(`/upload/check?fileName=${fileName}&fileMd5=${fileMd5}&fileSize=${fileSize}`) .then(response => response.json()); } function mergeChunks(fileName, fileMd5, totalChunks) { return fetch('/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName: fileName, fileMd5: fileMd5, totalChunks: totalChunks }) }).then(response => response.json()); } function updateProgress(current, total) { const progress = document.getElementById('progress'); const percentage = Math.round((current / total) * 100); progress.innerHTML = `上传进度: ${percentage}%`; } // 计算文件MD5(简化版,实际应使用更可靠的库) async function calculateFileMD5(file) { // 这里使用简单的文件名+大小模拟MD5 // 实际项目中应使用 spark-md5 等库 return btoa(file.name + file.size).replace(/[^a-zA-Z0-9]/g, ''); } </script> </body> </html>
后端Java代码示例 (Spring Boot)
1. 配置文件上传设置
@Configuration public class UploadConfig { @Bean public MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setMaxFileSize("10GB"); factory.setMaxRequestSize("10GB"); return factory.createMultipartConfig(); } }
2. 文件上传控制器
@RestController @RequestMapping("/upload") public class FileUploadController { @Value("${file.upload-dir:/tmp/uploads}") private String uploadDir; /** * 检查文件是否存在 */ @GetMapping("/check") public ResponseEntity<CheckResult> checkFile( @RequestParam String fileName, @RequestParam String fileMd5, @RequestParam Long fileSize) { String filePath = Paths.get(uploadDir, fileMd5, fileName).toString(); File file = new File(filePath); CheckResult result = new CheckResult(); // 如果文件已存在 if (file.exists() && file.length() == fileSize) { result.setUploaded(true); return ResponseEntity.ok(result); } // 检查已上传的分片 String chunkDir = getChunkDir(fileMd5); File chunkFolder = new File(chunkDir); if (!chunkFolder.exists()) { result.setUploaded(false); result.setUploadedChunks(new ArrayList<>()); return ResponseEntity.ok(result); } List<Integer> uploadedChunks = Arrays.stream(chunkFolder.listFiles()) .map(f -> Integer.parseInt(f.getName())) .collect(Collectors.toList()); result.setUploaded(false); result.setUploadedChunks(uploadedChunks); return ResponseEntity.ok(result); } /** * 上传文件分片 */ @PostMapping("/chunk") public ResponseEntity<UploadResult> uploadChunk( @RequestParam("file") MultipartFile file, @RequestParam Integer chunkIndex, @RequestParam Integer totalChunks, @RequestParam String fileName, @RequestParam String fileMd5) { try { // 创建分片目录 String chunkDir = getChunkDir(fileMd5); File chunkFolder = new File(chunkDir); if (!chunkFolder.exists()) { chunkFolder.mkdirs(); } // 保存分片文件 File chunkFile = new File(chunkDir + File.separator + chunkIndex); file.transferTo(chunkFile); UploadResult result = new UploadResult(); result.setSuccess(true); result.setMessage("分片上传成功"); return ResponseEntity.ok(result); } catch (Exception e) { UploadResult result = new UploadResult(); result.setSuccess(false); result.setMessage("分片上传失败: " + e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); } } /** * 合并文件分片 */ @PostMapping("/merge") public ResponseEntity<MergeResult> mergeChunks(@RequestBody MergeRequest request) { try { String chunkDir = getChunkDir(request.getFileMd5()); String fileName = request.getFileName(); String filePath = Paths.get(uploadDir, request.getFileMd5(), fileName).toString(); // 创建目标文件 File targetFile = new File(filePath); File parentDir = targetFile.getParentFile(); if (!parentDir.exists()) { parentDir.mkdirs(); } // 合并分片 try (FileOutputStream fos = new FileOutputStream(targetFile)) { for (int i = 0; i < request.getTotalChunks(); i++) { File chunkFile = new File(chunkDir + File.separator + i); try (FileInputStream fis = new FileInputStream(chunkFile)) { byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) > 0) { fos.write(buffer, 0, len); } } // 删除分片文件 chunkFile.delete(); } } // 删除分片目录 new File(chunkDir).delete(); MergeResult result = new MergeResult(); result.setSuccess(true); result.setMessage("文件合并成功"); result.setFilePath(filePath); return ResponseEntity.ok(result); } catch (Exception e) { MergeResult result = new MergeResult(); result.setSuccess(false); result.setMessage("文件合并失败: " + e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); } } private String getChunkDir(String fileMd5) { return Paths.get(uploadDir, "chunks", fileMd5).toString(); } }
3. 数据传输对象
@Data public class CheckResult { private boolean uploaded; private List<Integer> uploadedChunks; } @Data public class UploadResult { private boolean success; private String message; } @Data public class MergeRequest { private String fileName; private String fileMd5; private Integer totalChunks; } @Data public class MergeResult { private boolean success; private String message; private String filePath; }
4. 应用配置
# application.properties spring.servlet.multipart.max-file-size=10GB spring.servlet.multipart.max-request-size=10GB file.upload-dir=/data/uploads
关键技术点
- 分片上传:将大文件分割成小块,分别上传
- 断点续传:记录已上传的分片,网络中断后可以从中断处继续
- 文件校验:通过MD5验证文件完整性
- 进度监控:实时显示上传进度
- 内存优化:流式处理,避免内存溢出
优化建议
- 增加重试机制:网络异常时自动重试
- 并行上传:同时上传多个分片提高速度
- 压缩传输:对分片进行压缩减少网络传输量
- 安全验证:添加身份验证和文件类型检查
- 分布式存储:支持分布式文件系统存储
这种方案可以有效解决大文件上传的各种问题,提供稳定可靠的上传体验。
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海