1. 为什么需要分片上传与断点续传
在 Web 应用中,上传大文件(如视频、安装包、数据集)时,如果采用传统的单次上传方式,往往会遇到两个突出问题:一是网络波动导致上传中断后必须从头重传,浪费时间和带宽;二是单个请求体过大,容易触发网关超时或服务器内存压力。分片上传和断点续传正是为了解决这两个问题而设计的。
分片上传的核心思路是把一个大文件切成多个较小的分片,逐个上传到服务器;断点续传则是在上传中断后,能够从已上传的分片之后继续,而不是重新上传整个文件。两者通常配合使用,共同提升大文件上传的稳定性和效率。
2. 整体流程设计
一个完整的断点续传与分片上传流程,通常包含以下几个阶段:
- 初始化上传:前端向后端发起创建上传任务的请求,携带文件名、文件大小、分片大小等信息,后端返回一个唯一的上传任务 ID。
- 计算分片信息:前端根据分片大小将文件切分为多个分片,并为每个分片生成序号和校验值(如 MD5)。
- 逐片上传:前端按顺序(或并发)将每个分片上传到后端,后端保存分片并记录已上传状态。
- 查询已上传分片:上传中断后,前端向后端查询该任务已成功上传的分片列表,据此跳过已完成的分片。
- 合并文件:所有分片上传完成后,前端通知后端执行合并操作,后端将分片按顺序拼接为完整文件。
3. 前端实现要点
前端主要负责文件切片、上传调度、进度展示和断点恢复。下面以 JavaScript 为例,说明核心实现思路。
3.1 文件切片
利用 HTML5 的 File API,可以通过 slice 方法将文件切分为多个 Blob 分片:
javascript
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片 5MB
function createChunks(file) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + CHUNK_SIZE, file.size);
chunks.push(file.slice(start, end));
start = end;
}
return chunks;
}
3.2 上传分片
每个分片通过独立的请求上传,并携带任务 ID 和分片序号,便于后端识别和排序:
javascript
async function uploadChunk(taskId, chunk, index) {
const formData = new FormData();
formData.append('taskId', taskId);
formData.append('index', index);
formData.append('chunk', chunk);
const response = await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
return response.json();
}
3.3 断点续传
上传中断后,重新开始时先查询已上传的分片,只上传缺失的部分:
javascript
async function resumeUpload(taskId, chunks) {
const res = await fetch(`/api/upload/status?taskId=${taskId}`);
const { uploadedIndexes } = await res.json();
const tasks = chunks.map((chunk, index) => {
if (uploadedIndexes.includes(index)) {
return Promise.resolve({ index, skipped: true });
}
return uploadChunk(taskId, chunk, index).then(() => ({ index, skipped: false }));
});
await Promise.all(tasks);
await mergeFile(taskId);
}
4. 后端实现要点
后端负责接收分片、记录上传状态、校验完整性以及合并文件。下面以 Java Spring Boot 为例说明核心接口设计。
4.1 初始化上传任务
java
@PostMapping("/api/upload/init")
public Map<String, Object> initUpload(@RequestBody InitUploadRequest request) {
String taskId = UUID.randomUUID().toString();
// 保存任务信息:文件名、总大小、分片大小、分片总数
uploadTaskService.createTask(taskId, request);
return Map.of("taskId", taskId, "chunkSize", request.getChunkSize());
}
4.2 接收分片
java
@PostMapping("/api/upload/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam String taskId,
@RequestParam int index,
@RequestParam MultipartFile chunk) {
// 校验任务存在,保存分片到临时目录
uploadTaskService.saveChunk(taskId, index, chunk);
return ResponseEntity.ok().build();
}
4.3 查询已上传分片
java
@GetMapping("/api/upload/status")
public Map<String, Object> getUploadStatus(@RequestParam String taskId) {
List<Integer> uploadedIndexes = uploadTaskService.getUploadedIndexes(taskId);
return Map.of("uploadedIndexes", uploadedIndexes);
}
4.4 合并分片
java
@PostMapping("/api/upload/merge")
public ResponseEntity<?> merge(@RequestParam String taskId) {
uploadTaskService.mergeChunks(taskId);
return ResponseEntity.ok().build();
}
5. 关键细节与注意事项
- 分片大小选择:分片过小会导致请求数量过多,增加网络开销;分片过大会失去分片上传的优势。一般建议 1MB 到 10MB 之间,可根据网络环境动态调整。
- 并发控制:并发上传多个分片可以提升速度,但需要控制并发数,避免占用过多连接或触发服务端限流。
- 完整性校验:每个分片上传时可携带 MD5 值,服务端校验后存储;合并后也可对整体文件做一次校验,确保数据完整。
- 任务过期清理:长时间未完成的上传任务应设置过期时间,定期清理临时分片,避免占用磁盘空间。
- 秒传支持:在初始化时计算整个文件的 MD5,若服务端已存在相同文件,可直接返回秒传结果,跳过实际上传。
6. 总结
断点续传和分片上传的实现,核心在于将大文件拆分为独立可管理的小分片,并通过任务状态记录实现中断后的精准续传。前端负责切片和调度,后端负责存储、状态管理和合并,两者配合即可显著提升大文件上传的稳定性和用户体验。实际项目中,还可以结合并发控制、完整性校验和秒传机制,进一步优化上传效率。