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

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

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

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

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

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

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

前端操作流程详解

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

1. 文件选择与初始化

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

获取用户选择的文件对象

读取文件基本信息(文件名、大小等)

计算整个文件的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; // 2MB

const 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. 分片上传参数实体类

@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 uploadAppendFile(MultipartFileParam multipartFileParam) {

Map map = new HashMap<>();

// 获取分片参数

long chunk = multipartFileParam.getChunkNumber(); // 当前分片位置

long fileSize = multipartFileParam.getFileSize(); // 文件大小(偏移量)

long totalChunks = multipartFileParam.getTotalChunks(); // 分片总数

String taskId = multipartFileParam.getTaskId(); // 任务ID

MultipartFile 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();

// 记录分片数和文件路径到Redis

redisUtil.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();

}

// 更新文件信息到Redis

String md5 = (String) redisUtil.getObject(UpLoadConstant.task + taskId);

HashMap 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 checkMd5(String md5) {

Map map = new HashMap<>();

String fileSize = null;

String taskId = null;

// 计算文件MD5

md5 = 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 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相关信息

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

记录每个文件上传的进度

在上传中断后,能够从断点处继续上传

避免重复上传已经成功上传的分片

3. 提高查询效率

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

实现流程总结

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

用户选择文件,前端读取文件信息

前端计算整个文件的MD5值

前端调用checkMd5接口检查是否已部分上传

如果已部分上传,后端返回已上传的文件大小和任务ID

前端从指定位置开始上传剩余分片

后端接收每个分片并使用RandomAccessFile从指定位置写入临时文件

所有分片上传完成后,将临时文件重命名为正式文件

清理Redis中的临时数据

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

相关数据

365提款一周都没到 黄铜与青铜,有什么区别及其用途?
365提款一周都没到 翅膀隐喻:人类超越性追求的文化解读史
365提款一周都没到 部门评价怎么写

部门评价怎么写

06-14 访问量: 7558