【SpringBoot】优雅实现超大文件上传

前言

文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。

那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式

秒传

1、什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了。

2、本文实现的秒传核心逻辑

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径

分片上传

1.什么是分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

2.分片上传的场景

  1. 大文件上传
  2. 网络环境环境不好,存在需要重传风险的场景

断点续传

1、什么是断点续传

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

2、应用场景

断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。

3、实现断点续传的核心逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

4、实现流程步骤

a、方案一,常规步骤

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
  • 初始化一个分片上传任务,返回本次分片上传唯一标识;
  • 按照一定的策略(串行或并行)发送各个分片数据块;
  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

b、方案二、本文实现的步骤

  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小
  • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)
  • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

5、分片上传/断点上传代码实现

a、前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接:

  • http://fex.baidu.com/webuploader/getting-started.html

b、后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接:

  • https://blog.csdn.net/dimudan2015/article/details/81910690

另一种是使用MappedByteBuffer,对MappedByteBuffer不熟悉的朋友,可以查看如下链接进行了解:

  • https://www.jianshu.com/p/f90866dcbffc

后端进行写入操作的核心代码

a、RandomAccessFile实现方式

@UploadMode(mode=UploadModeEnum.RANDOM_ACCESS)
@Slf4j
publicclassRandomAccessUploadStrategyextendsSliceUploadTemplate{@AutowiredprivateFilePathUtilfilePathUtil;@Value("${upload.chunkSize}")privatelongdefaultChunkSize;@Overridepublicbooleanupload(FileUploadRequestDTOparam){RandomAccessFileaccessTmpFile=null;try{StringuploadDirPath=filePathUtil.getPath(param);FiletmpFile=super.createTmpFile(param);accessTmpFile=newRandomAccessFile(tmpFile,"rw");//这个必须与前端设定的值一致longchunkSize=Objects.isNull(param.getChunkSize())?defaultChunkSize\*1024\*1024:param.getChunkSize();longoffset=chunkSize\*param.getChunk();//定位到该分片的偏移量accessTmpFile.seek(offset);//写入该分片数据accessTmpFile.write(param.getFile().getBytes());booleanisOk=super.checkAndSetUploadProgress(param,uploadDirPath);returnisOk;}catch(IOExceptione){log.error(e.getMessage(),e);}finally{FileUtil.close(accessTmpFile);}returnfalse;}}

b、MappedByteBuffer实现方式

@UploadMode(mode=UploadModeEnum.MAPPED_BYTEBUFFER)
@Slf4j
publicclassMappedByteBufferUploadStrategyextendsSliceUploadTemplate{@AutowiredprivateFilePathUtilfilePathUtil;@Value("${upload.chunkSize}")privatelongdefaultChunkSize;@Overridepublicbooleanupload(FileUploadRequestDTOparam){RandomAccessFiletempRaf=null;FileChannelfileChannel=null;MappedByteBuffermappedByteBuffer=null;try{StringuploadDirPath=filePathUtil.getPath(param);FiletmpFile=super.createTmpFile(param);tempRaf=newRandomAccessFile(tmpFile,"rw");fileChannel=tempRaf.getChannel();longchunkSize=Objects.isNull(param.getChunkSize())?defaultChunkSize\*1024\*1024:param.getChunkSize();//写入该分片数据longoffset=chunkSize\*param.getChunk();byte\[\]fileData=param.getFile().getBytes();mappedByteBuffer=fileChannel.map(FileChannel.MapMode.READ_WRITE,offset,fileData.length);mappedByteBuffer.put(fileData);booleanisOk=super.checkAndSetUploadProgress(param,uploadDirPath);returnisOk;}catch(IOExceptione){log.error(e.getMessage(),e);}finally{FileUtil.freedMappedByteBuffer(mappedByteBuffer);FileUtil.close(fileChannel);FileUtil.close(tempRaf);}returnfalse;}}

c、文件操作核心模板类代码

@Slf4j
publicabstractclassSliceUploadTemplateimplementsSliceUploadStrategy{publicabstractbooleanupload(FileUploadRequestDTOparam);protectedFilecreateTmpFile(FileUploadRequestDTOparam){FilePathUtilfilePathUtil=SpringContextHolder.getBean(FilePathUtil.class);param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));StringfileName=param.getFile().getOriginalFilename();StringuploadDirPath=filePathUtil.getPath(param);StringtempFileName=fileName+"_tmp";FiletmpDir=newFile(uploadDirPath);FiletmpFile=newFile(uploadDirPath,tempFileName);if(!tmpDir.exists()){tmpDir.mkdirs();}returntmpFile;}@OverridepublicFileUploadDTOsliceUpload(FileUploadRequestDTOparam){booleanisOk=this.upload(param);if(isOk){FiletmpFile=this.createTmpFile(param);FileUploadDTOfileUploadDTO=this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(),tmpFile);returnfileUploadDTO;}Stringmd5=FileMD5Util.getFileMD5(param.getFile());Map\<Integer,String\>map=newHashMap\<\>();map.put(param.getChunk(),md5);returnFileUploadDTO.builder().chunkMd5Info(map).build();}/**
*检查并修改文件上传进度
*/publicbooleancheckAndSetUploadProgress(FileUploadRequestDTOparam,StringuploadDirPath){StringfileName=param.getFile().getOriginalFilename();FileconfFile=newFile(uploadDirPath,fileName+".conf");byteisComplete=0;RandomAccessFileaccessConfFile=null;try{accessConfFile=newRandomAccessFile(confFile,"rw");//把该分段标记为true表示完成System.out.println("setpart"+param.getChunk()+"complete");//创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE127accessConfFile.setLength(param.getChunks());accessConfFile.seek(param.getChunk());accessConfFile.write(Byte.MAX_VALUE);//completeList检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)byte\[\]completeList=FileUtils.readFileToByteArray(confFile);isComplete=Byte.MAX_VALUE;for(inti=0;i\<completeList.length\&\&isComplete==Byte.MAX_VALUE;i++){//与运算,如果有部分没有完成则isComplete不是Byte.MAX_VALUEisComplete=(byte)(isComplete\&completeList\[i\]);System.out.println("checkpart"+i+"complete?:"+completeList\[i\]);}}catch(IOExceptione){log.error(e.getMessage(),e);}finally{FileUtil.close(accessConfFile);}booleanisOk=setUploadProgress2Redis(param,uploadDirPath,fileName,confFile,isComplete);returnisOk;}/**
*把上传进度信息存进redis
*/privatebooleansetUploadProgress2Redis(FileUploadRequestDTOparam,StringuploadDirPath,StringfileName,FileconfFile,byteisComplete){RedisUtilredisUtil=SpringContextHolder.getBean(RedisUtil.class);if(isComplete==Byte.MAX_VALUE){redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS,param.getMd5(),"true");redisUtil.del(FileConstant.FILE_MD5_KEY+param.getMd5());confFile.delete();returntrue;}else{if(!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS,param.getMd5())){redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS,param.getMd5(),"false");redisUtil.set(FileConstant.FILE_MD5_KEY+param.getMd5(),uploadDirPath+FileConstant.FILE_SEPARATORCHAR+fileName+".conf");}returnfalse;}}/**
*保存文件操作
*/publicFileUploadDTOsaveAndFileUploadDTO(StringfileName,FiletmpFile){FileUploadDTOfileUploadDTO=null;try{fileUploadDTO=renameFile(tmpFile,fileName);if(fileUploadDTO.isUploadComplete()){System.out.println("uploadcomplete!!"+fileUploadDTO.isUploadComplete()+"name="+fileName);//TODO保存文件信息到数据库}}catch(Exceptione){log.error(e.getMessage(),e);}finally{}returnfileUploadDTO;}/**
*文件重命名
*
*@paramtoBeRenamed将要修改名字的文件
*@paramtoFileNewName新的名字
*/privateFileUploadDTOrenameFile(FiletoBeRenamed,StringtoFileNewName){//检查要重命名的文件是否存在,是否是文件FileUploadDTOfileUploadDTO=newFileUploadDTO();if(!toBeRenamed.exists()\|\|toBeRenamed.isDirectory()){log.info("Filedoesnotexist:{}",toBeRenamed.getName());fileUploadDTO.setUploadComplete(false);returnfileUploadDTO;}Stringext=FileUtil.getExtension(toFileNewName);Stringp=toBeRenamed.getParent();StringfilePath=p+FileConstant.FILE_SEPARATORCHAR+toFileNewName;FilenewFile=newFile(filePath);//修改文件名booleanuploadFlag=toBeRenamed.renameTo(newFile);fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());fileUploadDTO.setUploadComplete(uploadFlag);fileUploadDTO.setPath(filePath);fileUploadDTO.setSize(newFile.length());fileUploadDTO.setFileExt(ext);fileUploadDTO.setFileId(toFileNewName);returnfileUploadDTO;}
}

总结

在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用fastdfs、hdfs等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/758577.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Http 超文本传输协议基本概念学习摘录

目录 HTTP协议 超文本传输协议 HyperText超文本 HTML超文本标记语言 HTTP协议原理 请求发送 服务器处理 响应发送 连接关闭或保持 HTTP协议版本 HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3 HTTP请求方法 GET POST PUT DELETE HEAD OPTIONS HTTP请求头字…

python中的类与对象

前言 在Python中&#xff0c;类是一种用于创建新类型对象的结构&#xff0c;它允许我们将数据和功能&#xff08;属性和方法&#xff09;封装到一个单独的逻辑单元中。类可以被看作是创建对象&#xff08;实例&#xff09;的蓝图或模板。类&#xff08;Class&#xff09;和对象…

JVM学习-类加载

目录 1.类文件结构 2.类加载器 3.类加载的三个阶段 3.1加载 3.2链接 3.2.1验证 3.2.2准备阶段 3.2.3解析阶段 3.3初始化 4.拓展&#xff1a;反射 4.1获取类对象 4.2创建实例 4.3获取方法 4.4方法调用 1.类文件结构 2.类加载器 类加载器用来将类文件的二进制字节码加载到JV…

猜数字游戏有三变(Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

小白学视觉 | 超详细!Python中 pip 常用命令

本文来源公众号“小白学视觉”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;超详细&#xff01;Python中 pip 常用命令 相信对于大多数熟悉Python的人来说&#xff0c;一定都听说并且使用过pip这个工具&#xff0c;但是对它的了…

中国联通推出了一站式全流程的专业安全服务

中国联通依托多年深耕通信与信息安全领域的丰富经验和专业技术积累&#xff0c;推出了一站式全流程的专业安全服务&#xff0c;从网络环境的前期准备阶段直至正式运行&#xff0c;均提供全面、立体、高效的保障措施&#xff0c;确保各类企事业单位在网络空间的安全稳定。 请点击…

算法打卡day23|回溯法篇03|Leetcode 39. 组合总和、40.组合总和II、131.分割回文串

算法题 Leetcode 39. 组合总和 题目链接:39. 组合总和 大佬视频讲解&#xff1a;组合总和视频讲解 个人思路 这道组合题主要是有总和的限制&#xff0c;当递归和超过了总和就return&#xff0c;递归时加上回溯去遍历数组。 解法 回溯法 把组合问题抽象为如下树形结构 如上…

【通信原理笔记】【二】随机信号分析——2.5 高斯随机过程

文章目录 前言一、高斯分布二、高斯过程三、高斯白噪声四、窄带高斯白噪声的复包络总结 前言 这篇我们来学习通信原理中非常重要的高斯&#xff08;正态&#xff09;随机过程&#xff0c;在之后的内容中会反复使用这个模型 一、高斯分布 首先回顾一下概率论中学过的高斯分布的…

达源电机超高速数码马达震撼来袭

新质生产力是什么? 12万转高速电吹风机马达引领行业技术革新 随着科技的不断进步&#xff0c;电吹风机行业正迎来一场深刻新质生产力技术革新。在这场革新中&#xff0c;达源电机以其独特绕线技术与自适应平衡磁场的马达技术&#xff0c;成功打破了国外高速马达电机悬臂梁专利…

uniapp——第3篇:自定义组件、组件间传数据

前提&#xff0c;建议先学会前端几大基础&#xff1a;HTML、CSS、JS、Ajax&#xff0c;还有一定要会Vue!&#xff08;Vue2\Vue3&#xff09;都要会&#xff01;&#xff01;&#xff01;不然不好懂 一、组件是啥玩意&#xff1f; 我之前讲vue2的文章讲过 Vue全家桶:vue2vue3全…

位图与布隆过滤器

目录 一、位图 1、问题用位图来解决&#xff1a; 二、 布隆过滤器 1、将哈希与位图结合&#xff0c;即布隆过滤器 2.布隆过滤器的查找 3.布隆过滤器的删除 4.布隆过滤器优点 5、布隆过滤器缺陷 三、海量数据处理问题&#xff1a; 一、位图 问题1&#xff1a;给40亿个不…

netron:本地查看服务器端打开的onnx文件

我们开发一般都在服务器中开发&#xff0c;假如我们在服务器端导出了一个onnx文件&#xff0c;不将onnx文件传到本地&#xff0c;如何进行本地查看呢&#xff1f; netron --port 8082 --host 10.75.29.201 model_data/deeplab_0131.onnx--host : 指定服务器的ip, 注意不是本地…

2024/03/19(网络编程·day5)

一、思维导图 二、selec函数实现TCP并发服务器 #include<myhead.h>#define SER_PORT 8888 //服务器端口号 #define SER_IP "192.168.117.116" //服务器IP int main(int argc, const char *argv[]) {//1、创建一个套接字int sfd -1;sfd socket(AF_INET,SOC…

软件设计师:03 - 数据库系统

一、数据模型的分类 1.1、概念数据模型 1.2、结构数据模型 1.3 真题 二、三级模式 概念模式对应的是基本表&#xff0c;概念模式也称为模式 外模式对应的是视图&#xff0c;也称用户模式或者子模式 内模式对应的是数据库里面的存储文件&#xff0c;也称存储模式 真题 三、两级…

VO、RVO、ORCA(动态避障)算法

碰撞锥&#xff08;碰撞区域&#xff09; 上上图中假设B物体处于静止状态&#xff0c;A物体沿着向量v1和v2移动&#xff0c;刚好能和B擦肩而过&#xff0c;不会发生碰撞&#xff1b;若V1和V2的夹角再小一点的话就一定会发生碰撞。此时会产生碰撞区域&#xff1a; 红线画出来…

蓝桥杯练习04学生成绩统计

学生成绩统计 介绍 随着大数据的发展&#xff0c;数据统计在很多应用中显得不可或缺&#xff0c;echarts作为一款基于JavaScript的数据可视化图表库&#xff0c;也成为了前端开发的必备技能&#xff0c;下面我们一起来用echarts开发一个学生数据统计的柱形图。 准备 开始答…

Vmware使用ubuntu报错空间不够

Vmware使用ubuntu报错空间不够 1.vmware修改2.进入镜像进行修改2.1需要下载GParted软件 1.vmware修改 这个需要把硬盘的大小进行扩展 2.进入镜像进行修改 2.1需要下载GParted软件

C++_回文串

目录 回文子串 最长回文子串 分割回文串 IV 分割回文串 II 最长回文子序列 让字符串成为回文串的最少插入次数 回文子串 647. 回文子串 思路&#xff0c;i j表示改范围内是否为回文串&#xff0c; ②倒着遍历是为了取出dp[i 1][j - 1] ③i j 只有一对&#xff0c;不会重复…

C++模版(一)

C++模版 概念函数模版函数模版语法注意事项普通函数和函数模版区别普通函数和函数模版的调用规则模版的局限性具体化模版本质分析概念 模版也可称之为泛型,可以理解为模具,通过模具可以创建多种实现,大大提高了复用性。 实际生活中,有很多模版的例子。比如我们的入职简历,…

harmonyOS简介及背景

harmonyOS的场景模式18n: 1&#xff08;入口手机&#xff09;8&#xff08;电脑、VR、手环、iPad、智慧屏、&#xff09;–wifi—n(车载、智能家居等所有)harmonyOS不需要考虑软硬件的差异&#xff0c;是一个兼容N种的超级终端harmonyOS干了两件事&#xff1a; &#xff08;1&a…