如何实现大文件上传:秒传、断点续传、分片上传_hdfs断点续传-程序员宅基地

技术标签: 秒传  断点续传  大文件上传  服务器  数据库  就业规划及面试指导  

前言

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

详细教程

秒传

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  
public class RandomAccessUploadStrategy extends SliceUploadTemplate {  
  
  @Autowired  
  private FilePathUtil filePathUtil;  
  
  @Value("${upload.chunkSize}")  
  private long defaultChunkSize;  
  
  @Override  
  public boolean upload(FileUploadRequestDTO param) {  
    RandomAccessFile accessTmpFile = null;  
    try {  
      String uploadDirPath = filePathUtil.getPath(param);  
      File tmpFile = super.createTmpFile(param);  
      accessTmpFile = new RandomAccessFile(tmpFile, "rw");  
      //这个必须与前端设定的值一致  
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024  
          : param.getChunkSize();  
      long offset = chunkSize * param.getChunk();  
      //定位到该分片的偏移量  
      accessTmpFile.seek(offset);  
      //写入该分片数据  
      accessTmpFile.write(param.getFile().getBytes());  
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);  
      return isOk;  
    } catch (IOException e) {  
      log.error(e.getMessage(), e);  
    } finally {  
      FileUtil.close(accessTmpFile);  
    }  
   return false;  
  }  
  
}  

b、MappedByteBuffer实现方式

@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)  
@Slf4j  
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {  
  
  @Autowired  
  private FilePathUtil filePathUtil;  
  
  @Value("${upload.chunkSize}")  
  private long defaultChunkSize;  
  
  @Override  
  public boolean upload(FileUploadRequestDTO param) {  
  
    RandomAccessFile tempRaf = null;  
    FileChannel fileChannel = null;  
    MappedByteBuffer mappedByteBuffer = null;  
    try {  
      String uploadDirPath = filePathUtil.getPath(param);  
      File tmpFile = super.createTmpFile(param);  
      tempRaf = new RandomAccessFile(tmpFile, "rw");  
      fileChannel = tempRaf.getChannel();  
  
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024  
          : param.getChunkSize();  
      //写入该分片数据  
      long offset = chunkSize * param.getChunk();  
      byte[] fileData = param.getFile().getBytes();  
      mappedByteBuffer = fileChannel  
.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);  
      mappedByteBuffer.put(fileData);  
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);  
      return isOk;  
  
    } catch (IOException e) {  
      log.error(e.getMessage(), e);  
    } finally {  
  
      FileUtil.freedMappedByteBuffer(mappedByteBuffer);  
      FileUtil.close(fileChannel);  
      FileUtil.close(tempRaf);  
  
    }  
  
    return false;  
  }  
  
}  

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

@Slf4j  
public abstract class SliceUploadTemplate implements SliceUploadStrategy {  
  
  public abstract boolean upload(FileUploadRequestDTO param);  
  
  protected File createTmpFile(FileUploadRequestDTO param) {  
  
    FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);  
    param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));  
    String fileName = param.getFile().getOriginalFilename();  
    String uploadDirPath = filePathUtil.getPath(param);  
    String tempFileName = fileName + "_tmp";  
    File tmpDir = new File(uploadDirPath);  
    File tmpFile = new File(uploadDirPath, tempFileName);  
    if (!tmpDir.exists()) {  
      tmpDir.mkdirs();  
    }  
    return tmpFile;  
  }  
  
  @Override  
  public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {  
  
    boolean isOk = this.upload(param);  
    if (isOk) {  
      File tmpFile = this.createTmpFile(param);  
      FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);  
      return fileUploadDTO;  
    }  
    String md5 = FileMD5Util.getFileMD5(param.getFile());  
  
    Map<Integer, String> map = new HashMap<>();  
    map.put(param.getChunk(), md5);  
    return FileUploadDTO.builder().chunkMd5Info(map).build();  
  }  
  
  /**  
   * 检查并修改文件上传进度  
   */  
  public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {  
  
    String fileName = param.getFile().getOriginalFilename();  
    File confFile = new File(uploadDirPath, fileName + ".conf");  
    byte isComplete = 0;  
    RandomAccessFile accessConfFile = null;  
    try {  
      accessConfFile = new RandomAccessFile(confFile, "rw");  
      //把该分段标记为 true 表示完成  
      System.out.println("set part " + param.getChunk() + " complete");  
      //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127  
      accessConfFile.setLength(param.getChunks());  
      accessConfFile.seek(param.getChunk());  
      accessConfFile.write(Byte.MAX_VALUE);  
  
      //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)  
      byte[] completeList = FileUtils.readFileToByteArray(confFile);  
      isComplete = Byte.MAX_VALUE;  
      for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {  
        //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE  
        isComplete = (byte) (isComplete & completeList[i]);  
        System.out.println("check part " + i + " complete?:" + completeList[i]);  
      }  
  
    } catch (IOException e) {  
      log.error(e.getMessage(), e);  
    } finally {  
      FileUtil.close(accessConfFile);  
    }  
 boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);  
    return isOk;  
  }  
  
  /**  
   * 把上传进度信息存进redis  
   */  
  private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,  
      String fileName, File confFile, byte isComplete) {  
  
    RedisUtil redisUtil = 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();  
      return true;  
    } 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");  
      }  
  
      return false;  
    }  
  }  
/**  
   * 保存文件操作  
   */  
  public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {  
  
    FileUploadDTO fileUploadDTO = null;  
  
    try {  
  
      fileUploadDTO = renameFile(tmpFile, fileName);  
      if (fileUploadDTO.isUploadComplete()) {  
        System.out  
            .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);  
        //TODO 保存文件信息到数据库  
  
      }  
  
    } catch (Exception e) {  
      log.error(e.getMessage(), e);  
    } finally {  
  
    }  
    return fileUploadDTO;  
  }  
/**  
   * 文件重命名  
   *  
   * @param toBeRenamed 将要修改名字的文件  
   * @param toFileNewName 新的名字  
   */  
  private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {  
    //检查要重命名的文件是否存在,是否是文件  
    FileUploadDTO fileUploadDTO = new FileUploadDTO();  
    if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {  
      log.info("File does not exist: {}", toBeRenamed.getName());  
      fileUploadDTO.setUploadComplete(false);  
      return fileUploadDTO;  
    }  
    String ext = FileUtil.getExtension(toFileNewName);  
    String p = toBeRenamed.getParent();  
    String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;  
    File newFile = new File(filePath);  
    //修改文件名  
    boolean uploadFlag = toBeRenamed.renameTo(newFile);  
  
    fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());  
    fileUploadDTO.setUploadComplete(uploadFlag);  
    fileUploadDTO.setPath(filePath);  
    fileUploadDTO.setSize(newFile.length());  
    fileUploadDTO.setFileExt(ext);  
    fileUploadDTO.setFileId(toFileNewName);  
  
    return fileUploadDTO;  
  }  
}  

总结

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

本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的md5值计算,后端写入的速度还是比较快。如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网:

https://help.aliyun.com/product/31815.html

阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。

文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器:

https://www.cnblogs.com/ossteam/p/4942227.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/whirlwind526/article/details/128848476

智能推荐

ffmpeg 从avio_read 到 file_read-程序员宅基地

文章浏览阅读1k次,点赞24次,收藏10次。第一层: aviobuf.c中, AVIOContext对象当家, 这就是pReadCtx.第二层: avio.c中, URLContext 当家, 简记为h.第三层: file.c中, FileContext 对象当家,简记为c.

接口相关配置_tvbox黄源接口9月-程序员宅基地

文章浏览阅读7.3k次。1,相关接口系统配置(1)配置应用服务器,选择YES(2)网关配置点击ping网关,显示status:active则网关正常。(3)点击网关设置属性配置peoplesoft节点配置填写正确的应用程序服务器url,用户id为ps,tools发行版本为开发工具版本,可点击开发工具help,about application designer 查看版本..._tvbox黄源接口9月

素数筛_acm素數篩-程序员宅基地

文章浏览阅读110次。We know what a base of a number is and what the properties are. For example, we use decimal number system, where the base is 10 and we use the symbols - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. But in different bases we use different symbols. For example in binary _acm素數篩

Appium 自动化测试详解元素定位方式_appium 根据class定位元素driver.find_element(appiumby.cla-程序员宅基地

文章浏览阅读1.9k次,点赞2次,收藏21次。简介在做UI自动化的时候,有很大一部分精力是在进行定位元素操作,元素定位如果不准确,直接影响自动化的成败和效率环境Appium server :v1.20.2Appium-Python-Client :2.1.2selenium 4.1.0常用的元素定位方式id定位元素class_name定位元素content-desc定位元素name 定位元素 (appium1.5及之后的版本废弃了name属性)xpath定位元素uiautomator定位元素,Android独有id 定_appium 根据class定位元素driver.find_element(appiumby.class_name

Anaconda的python虚拟环境中安装cudatoolkit和cudnn加速tensorflow_conda安装cudatoolkit(1)_虚拟环境中下载cudatoolkit-程序员宅基地

文章浏览阅读811次,点赞25次,收藏11次。大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**_虚拟环境中下载cudatoolkit

uniapp小程序让页面滚动到底部_uni-app开发之使用scroll-view实现QQ小程序自动滑动到底部(二)...-程序员宅基地

文章浏览阅读3k次。版权声明:本文为博主原创文章,如果转载请给出原文链接:http://doofuu.com/article/4156212.html最近在开发一款情侣星座测试类型的QQ小程序。小程序中用到了类似聊天的界面,其中需要每次发完信息自动滑动到底部的需求。小程序滑动不像HTML那么简单,可以直接定位元素位置。不慌! 在小程序中虽然不像HTML中实现自动滑动到底部那么简单,不管是QQ小程序、还是微信小程序都有..._uniapp滚动到底部

随便推点

【Linux命令-转载】nohup 和 & 绝配(让命令在后台执行)-程序员宅基地

文章浏览阅读428次。nohup 的作用可以将程序以忽略挂起信号(SIGHUP)的方式运行。常见的用法是和 & 命令一同使用,将命令放置到后台运行,即使终端挂掉,进程会忽略挂起信号,继续运行。将程序放到后台运行,一般有两种方式:(1)command &:后台运行,关掉终端会停止运行。(2)nohup command & :后台运行,关掉终端也会继续运行。「注意:」(1)如果使用nohup执行程序未显示进行标准输出重定向,则标准输出默认重定向当前工作目录的 nohup.out 文件中。

2003服务器系统QQ安装不了,windows2003server-程序员宅基地

文章浏览阅读957次。windows2003server是一款针对大中型企业而设计的服务器操作系统,拥有32位和64位两种版本,保证了最佳的灵活性和可伸缩性,它支持的应用程序包括:联网、消息传递、清单和顾客服务系统、数据库、电子商务 Web 站点以及文件和打印服务器。有需要的朋友可以到本站进行下载!windows2003server简介:Windows Server 2003 企业版允许通过添加处理器和内存来提高服务器..._qq winserver

最新《市场调研与预测》考试重点_实验内容:根据收集的资料和调查分析的结果,进行战略总体决策,制定预算,运用定位策略,表现策略进行策划-程序员宅基地

文章浏览阅读177次。1.通过实地试验进行调查取得的资料,客观实用,排除人们主观估计的偏差2.调研人员可以针对调研事项的需要进行合理的实验设计,有效的控制实验环境,有意识的使调研对象在相同条件下重复出现,反复进行试验,使调研的结果更加准确3.调研人员可以主动的引起市场因素的变化,并通过控制其变化来研究该因素对市场产生的影响,而不是被动、消极的等待某种现象的发生,这是其他调研法无法做到的。一个实际的市场调研与预测设计的目标并不是一定要形成最精确的信息,而是在一定的成本费用条件下形成最有价值的信息。政府部门的市场调研;_实验内容:根据收集的资料和调查分析的结果,进行战略总体决策,制定预算,运用定位策略,表现策略进行策划,对驴友的需求和特点进行分析,展开创意与文案写作,考虑分析网络新产品和服务项目开发的可能性。

vi一个简单操作_vi a.txt-程序员宅基地

文章浏览阅读399次。进入:打开VIM之后,按一下insert键或者i键就可以进入输入状态了 #vi a.txt退出:退出的时候先按Esc键,出来冒号(:)就可以敲命令, q! 回车 不保存 wq 保存 wq!强制保存并推出(适用与只读文件) x 保存_vi a.txt

vue3 antd pro 框架动态路由_vue3+ant design + ts+pro-程序员宅基地

文章浏览阅读629次。1. 在store/user.ts中,找到 GENERATE_ROUTES_DYNAMIC 方法中调用的方法:generatorDynamicRouter(),此方法在在router/router-guards/router-guards.ts中。2. router/router-guards/router-guards.ts中,通过方法getCurrentUserNav(),获取到个人信息中的菜单,将菜单列表传generator()方法中,此方法将菜单列表处理成路由所需格式的路由数组。_vue3+ant design + ts+pro

【华为云技术分享】敏捷开发落地不实际?原因可能在于你的 IDE 工具_华为云ide问题-程序员宅基地

文章浏览阅读3.5k次。对于企业来说,效率就是一切。开发效率的工程化建设已经开始被各大企业提到技术管理日程中。而且现阶段,无论是框架也好、模板也好,目的都是在为提升代码开发效率而努力。随着云计算的深入,端 + 云的开发模式以及完全云端化的开发模式都先后上线,这些无疑都是在对传统 IDE 开发模式的挑战。云端 IDE,会是未来的趋势吗?云时代下,万物上云正在影响企业研发效率工程化建设万物上云,可以说已经是不可逆..._华为云ide问题

推荐文章

热门文章

相关标签