导读:分片上传、断点续传,这两个名词对于做过或者熟悉文件上传的朋友来说应该不会陌生,总结本篇文章希望对从事相关工作的同学能够有所帮助或者启发。
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。分片上传不仅可以避免因网络环境不好导致的一直需要从文件起始位置还是上传的问题,还能使用多线程对不同分块数据进行并发发送,提高发送效率,降低发送时间。
一、背景
对于大文件分片处理前面已经对文件拆分细节
- 微服务架构 | 怎样解决超大附件分片上传?
以及对于RandomAccessFile支持“随机访问”的方式的详细介绍
[向文件中追加内容]
[向文件指定位置插入内容]
- RandomAccessFile 解决多线程下载及断点续传
二、分片上传
梳理整个逻辑,对于超大文件分片上传的整个流程大致如下:
- 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
- 初始化一个分片上传任务,返回本次分片上传唯一标识;
- 按照一定的策略(串行或并行)发送各个分片数据块;
- 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。
在整个数据上传的过程中当然还涉及数据的签名校验,防止数据被恶意篡改。整个上传流程图如下所示。
基于上面的原理介绍,基于微服务架构 | 怎样解决超大附件分片上传?中的案例继续
上传文件详细
- 总文件大小 37.877 KB
- 分片大小 20L* 1024 = 20.480 KB
计算可以得到
- 分片数量2个
- 分片大小 18.939 KB
计算逻辑 怎样解决超大附件分片上传?有详细介绍
▐ 单个分片上传
设置默认临时分片文件存储在本地磁盘,文件夹已日期为时间分割
代码语言:javascript复制/Data/attach/
读取每个分片大小
代码语言:javascript复制try (InputStream inputStream = new FileInputStream(uploadVO.getFile())) {
for (int i = 0; i < sliceBytesVO.getFdTotalSlices(); i ) {
// 读取每个分片的数据字节
this.readSliceBytes(i, inputStream, sliceBytesVO);
// 调用分片上传API的函数
String result = sliceApiCallFunction.apply(sliceBytesVO);
if (StringUtils.isEmpty(result)) {
continue;
}
return result;
}
} catch (IOException e) {
throw e;
}
执行分片上传函数
代码语言:javascript复制 public SysAttachUploadResultVO upload(InputStream inputStream, SysAttachUploadSliceBaseVO sliceBaseVO) {
// 根据文件信息查询是否存在分片概要
SysAttachFileSliceSummary sliceSummary = this.checkExistedSummary(sliceBaseVO);
// 如果不存在分片概要,则保存分片概要
if (sliceSummary == null) {
sliceSummary = this.saveSliceSummary(sliceBaseVO);
}
// 查询当前分片是否存在,若存在则不需要上传当前分片
SysAttachFileSliceVO currentSliceExisted = this.checkSliceExited(sliceSummary.getFdId(),
sliceBaseVO.getFdSliceIndex());
if (currentSliceExisted != null) {
return null;
}
// 根据分片概要查询上一个分片碎片
SysAttachFileSliceVO previousSlice = this.findPreviousSliceBySummaryId(sliceSummary.getFdId());
// 普通写入实现
sliceMergeUploader.uploadSlice(inputStream, sliceBaseVO, sliceSummary.getFdId(), previousSlice);
List<SysAttachFileSlice> sliceList = sysAttachFileSliceService.findBySummaryId(sliceSummary.getFdId());
// 判断所有分片是否都已经上传
if (this.checkAllSliceUploaded(sliceList, sliceBaseVO)) {
// 创建完整附件,并返回临时附件文件ID
return sliceMergeUploader.createFullAttach(sliceBaseVO, sliceList, sliceSummary.getFdId());
}
return null;
}
生成分片文件和保存分片记录
代码语言:javascript复制public String writeFile(InputStream inputStream, String fileExtName, long fileSize, String filePath,
String encryptMethod, Map<String, String> header) {
// 当服务器上面不存在附件文件时候,分别构建输入,输出流
File file = new File(filePath);
File pfile = file.getParentFile();
if (!pfile.exists()) {
pfile.mkdirs();
}
if (file.exists()) {
file.delete();
}
FileOutputStream fileOutputStream = null;
InputStream encryptionInputStream = null;
try {
file.createNewFile();
fileOutputStream = new FileOutputStream(file);
// 进行附件文件写操作
encryptionInputStream = getEncryptService(encryptMethod).initEncryptInputStream(inputStream);
IOUtils.copy(encryptionInputStream, fileOutputStream);
return filePath;
} catch (IOException e) {
throw new KmssRuntimeException("attach.msg.error.SysAttachWriteFailed", e);
} finally {
// 附件写操作完成后,依次关闭输出流和输入流,释放输出,输入流所在内存空间
IOUtils.closeQuietly(fileOutputStream);
IOUtils.closeQuietly(encryptionInputStream);
IOUtils.closeQuietly(inputStream);
}
}
第一个分片
[分片详情]
[本地磁盘分片文件]
同理继续上传第二个分片,然后对所有附件进行顺序合并处理
三、断点续传
由于分片上传的数据是永久性的,因此可以很容易的基于分片上传来实现断点续传。
在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。
为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。
详细实现细节可以参考分片上传实现。
四、总结
由于RandomAccessFile可以自由访问文件的任意位置,所以如果需要访问文件的部分内容,而不是把文件从头读到尾,因此RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。
实现分片上传和断点续传的核心是巧用RandomAccessFile读和写内容,其次就是如何记录分片的摘要信息,用户比较和分析上传进度。