当前位置: 首页 > news >正文

分片上传与断点续传实现详解

分片上传与断点续传实现详解

在现代Web应用中,用户经常需要上传大文件,如视频、压缩包等。传统的文件上传方式在面对大文件时容易出现超时、失败等问题,而且一旦上传中断就需要重新上传整个文件,浪费时间和带宽。为了解决这些问题,分片上传和断点续传技术应运而生。

什么是分片上传和断点续传?

分片上传是将一个大文件分割成多个小块(分片),分别上传到服务器,最后在服务器端将这些分片合并成完整文件的技术。

断点续传是指在上传过程中,如果因为网络或其他原因导致上传中断,可以从上次中断的位置继续上传,而不需要重新上传整个文件。

前端操作流程详解

前端在整个分片上传和断点续传过程中起着关键作用,主要负责文件的分片、MD5计算、上传控制等任务。

1. 文件选择与初始化

当用户选择一个大文件后,前端JavaScript代码会执行以下操作:

  1. 获取用户选择的文件对象
  2. 读取文件基本信息(文件名、大小等)
  3. 计算整个文件的MD5值(用于唯一标识文件和断点续传)

2. 文件分片处理

前端需要将大文件按照指定大小进行分片:

// 示例:文件分片逻辑
function createFileChunks(file, chunkSize) {const chunks = [];let cur = 0;while (cur < file.size) {chunks.push({index: chunks.length,chunk: file.slice(cur, cur + chunkSize)});cur += chunkSize;}return chunks;
}

通常分片大小设置为1MB-10MB之间,根据网络情况和文件大小动态调整。

3. MD5值计算

在上传前,前端需要计算整个文件的MD5值:

// 示例:计算文件MD5
function calculateFileMD5(file) {return new Promise((resolve, reject) => {const spark = new SparkMD5.ArrayBuffer();const reader = new FileReader();const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;const chunkSize = 2097152; // 2MBconst chunks = Math.ceil(file.size / chunkSize);let currentChunk = 0;reader.onload = function(e) {spark.append(e.target.result);currentChunk++;if (currentChunk < chunks) {loadNext();} else {const md5 = spark.end();resolve(md5);}};reader.onerror = function() {reject('计算MD5出错');};function loadNext() {const start = currentChunk * chunkSize;const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;reader.readAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();});
}

4. 断点检查

使用计算出的MD5值向服务器查询上传状态:

// 示例:检查断点
async function checkUploadBreakPoint(md5) {const response = await fetch(`/checkMd5?md5=${md5}`);return await response.json();
}

服务器会返回:

  • 如果文件从未上传过:返回fileSize=0和新的taskId
  • 如果文件部分上传:返回已上传的fileSize和原有的taskId

5. 分片上传控制

根据断点检查结果,前端开始上传分片:

// 示例:上传分片
async function uploadChunks(chunks, taskId, md5, breakPoint = 0) {// 从断点位置开始上传for (let i = breakPoint; i < chunks.length; i++) {const formData = new FormData();formData.append('taskId', taskId);formData.append('chunkNumber', i + 1);formData.append('chunkSize', chunks[i].chunk.size);formData.append('totalChunks', chunks.length);formData.append('fileSize', computeFileSize(i)); // 计算当前分片的起始位置formData.append('fileName', fileName);formData.append('file', chunks[i].chunk);formData.append('code', businessCode);const response = await fetch('/chunkUpload', {method: 'POST',body: formData});const result = await response.json();if (result.code !== 200) {throw new Error('上传失败');}}
}

6. 上传状态管理

前端需要管理整个上传过程的状态:

  1. 显示上传进度条
  2. 处理上传成功/失败事件
  3. 在网络中断时尝试重连
  4. 上传完成后通知用户

后端实现原理

1. 分片上传参数实体类

@Data
public class MultipartFileParam implements Serializable {private static final long serialVersionUID = 3238600879053243080L;/*** 文件传输任务ID*/@ApiModelProperty(value = "文件传输任务ID:调用checkMd5返回")private String taskId;/*** 当前为第几分片*/@ApiModelProperty(value = "当前为第几分片")private int chunkNumber;/*** 每个分块的大小*/@ApiModelProperty(value = "每个分块的大小")private long chunkSize;/*** 分片总数*/@ApiModelProperty(value = "分片总数")private long totalChunks;/*** 文件大小*/@ApiModelProperty(value = "文件大小:这里只偏移量,断点后的末尾,续传前的开始")private long fileSize;/*** 文件名称*/@ApiModelProperty(value = "文件名称")private String fileName;/*** 分块文件传输对象*/@ApiModelProperty(value = "分块文件传输对象:分片文件的数据")private MultipartFile file;/*** 业务类型*/@ApiModelProperty(value = "业务类型")private String code;
}

2. Redis键常量定义

public class UpLoadConstant {private final static String uploading = "Uploading:";private final static String file = uploading + "file:";// 当前文件传输到第几块public final static String chunkNum = file + "chunkNum:";// 当前文件上传的路径public final static String localLocation = file + "localLocation:";public final static String task = uploading + "task:";public final static String fileMd5 = file + "md5:";
}

3. 分片上传核心实现

public R<Object> uploadAppendFile(MultipartFileParam multipartFileParam) {Map<String, String> map = new HashMap<>();// 获取分片参数long chunk = multipartFileParam.getChunkNumber();      // 当前分片位置long fileSize = multipartFileParam.getFileSize();      // 文件大小(偏移量)long totalChunks = multipartFileParam.getTotalChunks(); // 分片总数String taskId = multipartFileParam.getTaskId();        // 任务IDMultipartFile file = multipartFileParam.getFile();     // 分片文件String fileName = multipartFileParam.getFileName();    // 文件名// 创建上传目录File folder = new File(getUploadPath(multipartFileParam));if (!folder.isDirectory() && !folder.mkdirs()) {log.error("文件夹创建失败");return R.fail("文件夹创建失败");}String localPath = folder.getPath().concat(FileUtil.FILE_SEPARATOR);RandomAccessFile raf = null;InputStream is = null;try {if (chunk == 1) {// 第一个分片上传String tempFileName = taskId + fileName.substring(fileName.lastIndexOf(".")).concat("_tmp");File fileDir = new File(localPath);if (!fileDir.exists()) {fileDir.mkdirs();}// 创建临时文件File tempFile = new File(localPath, tempFileName);if (!tempFile.exists()) {tempFile.createNewFile();}// 写入第一个分片raf = new RandomAccessFile(tempFile, "rw");is = file.getInputStream();raf.seek(0); // 从文件开头写入int len = 0;byte[] bytes = new byte[1024 * 10];while ((len = is.read(bytes)) != -1) {raf.write(bytes, 0, len);}raf.close();is.close();// 记录分片数和文件路径到RedisredisUtil.setObject(UpLoadConstant.chunkNum + taskId, chunk, cacheTime);redisUtil.setObject(UpLoadConstant.localLocation + taskId, tempFile.getPath(), cacheTime);log.info("上传成功");map.put("result", "上传成功");} else {// 续传分片String path = (String) redisUtil.getObject(UpLoadConstant.localLocation + taskId);is = file.getInputStream();raf = new RandomAccessFile(path, "rw");// 从指定位置开始写入raf.seek(fileSize);int len = 0;byte[] bytes = new byte[1024 * 10];while ((len = is.read(bytes)) != -1) {raf.write(bytes, 0, len);}redisUtil.setObject(UpLoadConstant.chunkNum + taskId, chunk, cacheTime);raf.close();is.close();}// 更新文件信息到RedisString md5 = (String) redisUtil.getObject(UpLoadConstant.task + taskId);HashMap<String, String> redisMap = new HashMap<>();redisMap.put("fileSize", fileSize + "");redisMap.put("taskId", taskId);redisUtil.setHashAsMap(UpLoadConstant.fileMd5 + md5, redisMap, cacheTime);// 所有分片上传完成if (chunk == totalChunks) {String path = (String) redisUtil.getObject(UpLoadConstant.localLocation + taskId);String extName = FileUtil.extName(fileName);String newName = fileName.substring(0,fileName.lastIndexOf(".")).concat("-").concat(Seq.getId(Seq.uploadSeqType)).concat(".").concat(extName);String newUrl = localConfig.getDomain().concat(localConfig.getPrefix()).concat(basePath).concat(newName);// 重命名临时文件为正式文件FileUtil.rename(new File(path), newName, true);log.info("上传完毕");map.put("result", "上传完毕");map.put("name", newName);map.put("url", newUrl);// 清理Redis中的临时数据redisUtil.del(UpLoadConstant.fileMd5 + md5);redisUtil.del(UpLoadConstant.task + taskId);redisUtil.del(UpLoadConstant.chunkNum + taskId);redisUtil.del(UpLoadConstant.localLocation + taskId);}} catch (IOException e) {e.printStackTrace();String md5 = (String) redisUtil.getObject(UpLoadConstant.task + taskId);redisUtil.del(UpLoadConstant.fileMd5 + md5);redisUtil.del(UpLoadConstant.task + taskId);redisUtil.del(UpLoadConstant.chunkNum + taskId);redisUtil.del(UpLoadConstant.localLocation + taskId);log.error("上传异常");map.put("result", "上传异常");} finally {// 关闭资源try {if (raf != null) {raf.close();}} catch (IOException e) {e.printStackTrace();}try {if (is != null) {is.close();}} catch (IOException e) {e.printStackTrace();}}return R.ok(map);
}

4. MD5校验与断点检查

public Map<String, Object> checkMd5(String md5) {Map<String, Object> map = new HashMap<>();String fileSize = null;String taskId = null;// 计算文件MD5md5 = SecureUtil.md5(md5);// 从Redis中获取文件信息Map redisMap = redisUtil.getMap(UpLoadConstant.fileMd5 + md5);if (MapUtil.isNotEmpty(redisMap)) {fileSize = redisMap.get("fileSize").toString();taskId = redisMap.get("taskId").toString();}if (StrUtil.isNotEmpty(fileSize)) {// 文件已部分上传,返回已上传的文件大小map.put("fileSize", Long.parseLong(fileSize != null ? fileSize : ""));} else {// 文件未上传过,创建新的任务Map<String, Object> map1 = new HashMap<>();taskId = IdUtil.simpleUUID();map1.put("fileSize", 0);map1.put("taskId", taskId);redisUtil.setHashAsMap(UpLoadConstant.fileMd5 + md5, map1, cacheTime);redisUtil.setObject(UpLoadConstant.task + taskId, md5, cacheTime);map.put("fileSize", 0);}map.put("taskId", taskId);return map;
}

Redis在分片上传中的作用

Redis在分片上传和断点续传中扮演着关键角色:

1. 存储上传任务状态

  • UpLoadConstant.chunkNum + taskId: 记录当前已上传的分片序号
  • UpLoadConstant.localLocation + taskId: 存储临时文件的存储路径

2. 实现断点续传功能

  • UpLoadConstant.task + taskId: 存储任务ID与文件MD5值的映射关系
  • UpLoadConstant.fileMd5 + md5: 存储文件的MD5相关信息

通过这些键值对,系统可以:

  1. 记录每个文件上传的进度
  2. 在上传中断后,能够从断点处继续上传
  3. 避免重复上传已经成功上传的分片

3. 提高查询效率

相比直接读写数据库或文件系统,Redis提供了更快速的数据访问方式,特别是在需要频繁查询上传状态的场景下。

实现流程总结

整个分片上传和断点续传流程如下:

  1. 用户选择文件,前端读取文件信息
  2. 前端计算整个文件的MD5值
  3. 前端调用checkMd5接口检查是否已部分上传
  4. 如果已部分上传,后端返回已上传的文件大小和任务ID
  5. 前端从指定位置开始上传剩余分片
  6. 后端接收每个分片并使用RandomAccessFile从指定位置写入临时文件
  7. 所有分片上传完成后,将临时文件重命名为正式文件
  8. 清理Redis中的临时数据

这种方式可以有效处理大文件上传问题,支持断点续传,在网络中断后可以从中断位置继续上传,避免重复上传已上传的分片,大大提高了大文件上传的效率和用户体验。

http://www.hskmm.com/?act=detail&tid=39277

相关文章:

  • 2025 年 10 月展示柜厂家最新推荐,技术实力与市场口碑深度解析!
  • 手把手在 Linux 上安装 Docker 与 Docker Compose(包含 Ubuntu、CentOS 等 11 个发行版)
  • 2025 年 10 月展示柜厂家最新推荐,精准检测与稳定性能深度解析!
  • L
  • 数据处理方法汇总
  • 一些疑问
  • 2025 年 10 月外墙涂料厂家最新推荐,聚焦高端定制需求与全案交付能力
  • 2025年10月长白山亲子酒店推荐榜:四季主题与温泉度假对比排行
  • 2025年10月益生菌品牌推荐榜:全维度对比与榜单解读
  • 2025年10月工装设计公司推荐榜:全国服务力对比评测
  • 2025 年 10 月外墙涂料厂家最新推荐,精准检测与稳定性能深度解析
  • 2025年10月美容仪品牌推荐:无创无痛对比评测榜
  • 进程API
  • 2025年10月中国遗产继承律师推荐榜:五强对比全解析
  • 2025年10月法律咨询律所推荐榜:盈科多领域权威排名一览
  • 2025年10月中国短视频制作公司排行榜:五强实测推荐
  • php_sha1函数特性
  • php非法参数
  • 2025 年 10 月仿石漆厂家最新推荐,专业制造与品牌保障口碑之选
  • php_md5特性
  • php原生类的使用
  • 下午选歌
  • 分治算法在查找第k小元素中的应用与分析
  • 2025年10月电竞显示器品牌评价榜:五强对比与选购要点
  • 「学习笔记」RCE基础
  • Level 0~8 WP
  • 2025年10月中国装饰公司对比榜:十家口碑与实力排行
  • 2025年10月食品展会推荐榜:NHNE领衔五大展会对比评测
  • 2025年10月连锁酒店排行榜推荐:丽柏丽怡领衔对比评测榜
  • 芯片落地之道