如何实现断点续传和分片上传?

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. 总结

断点续传和分片上传的实现,核心在于将大文件拆分为独立可管理的小分片,并通过任务状态记录实现中断后的精准续传。前端负责切片和调度,后端负责存储、状态管理和合并,两者配合即可显著提升大文件上传的稳定性和用户体验。实际项目中,还可以结合并发控制、完整性校验和秒传机制,进一步优化上传效率。

相关推荐
微软技术分享5 小时前
面向二进制安全的大模型多智能体框架
网络·安全·web安全
搁浅小泽11 小时前
新能源汽车多合一压缩机控制器CAN网络测试规范
网络·汽车
xixiaoyunya13 小时前
虚拟机备份怎么做:镜像级与文件级搭配的完整实操方案
linux·运维·网络
anew___13 小时前
《从零手写操作系统 (30):环境变量与进程上下文——export/unset与继承语义》
java·开发语言·网络·jvm·算法
谢亮_vipxieliang13 小时前
Go GMP调度模型——从原理到实战的完整指南
开发语言·网络·golang
wdfk_prog14 小时前
LWIP教程 03:从 `low_level_input()` 到 `pbuf_free()`——`pbuf` 的数据视图、Chain 与引用计数
运维·网络·笔记·学习
福兮说14 小时前
IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出
javascript·网络·网络协议·tcp/ip·mysql·golang
xiaoye-duck14 小时前
《Linux 网络编程》深入理解 epoll(上):从接口到底层内核工作机制剖析
linux·网络
江曦仁15 小时前
ALV 选择框勾选不更新
状态模式
anew___15 小时前
《从零手写操作系统 (29):管道与重定向进阶——命名管道、Here Document与Shell语法扩展》
java·开发语言·前端·javascript·网络