大文件上传如何做断点续传?

2023-11-17

是什么

不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂

文件上传简单,文件变大就复杂

上传大文件时,以下几个变量会影响我们的用户体验

  • 服务器处理数据的能力
  • 请求超时
  • 网络波动

上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等

为了解决上述问题,我们需要对大文件上传单独处理

这里涉及到分片上传及断点续传两个概念

#分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传

如下图

上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

大致流程如下:

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

#断点续传

断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可

如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

#实现思路

整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕

image-20220803184035358

下面的内容都是伪代码

读取文件内容:

handleFileChange(e){
	const [file] = e.target.files
  if(!file) return 
  this.file = file
}

核心代码

async uploadFile(){

      if(!this.file){
        return 
      }
		  // todos: 判断文件类别
      
      // 1. 文件拆片
      const chunks = this.createFileChunk(this.file)
      // const hash = await this.calculateHashWorker()
      // const hash1 = await this.calculateHashIdle()
      // console.log('文件hash',hash)
      // console.log('文件hash1',hash1)
      
      // 2. 计算文件的hash值。这hash将用来唯一标识这个文件
      const hash = await this.calculateHashSample()
      this.hash = hash

      // 3. 尝试上传
  		// 3.1 文件是否上传过? 如果上传过,就秒传成功
      // 3.2 如果没有,是否有存在的切片?
      const {data:{uploaded, uploadedList}} = await this.$http.post('/checkfile',{
        hash:this.hash,
        ext:this.file.name.split('.').pop()
      })
      if(uploaded){
        // 秒传
        return this.$message.success('秒传成功')
      }
      // 4. 上传
      await this.uploadChunks(uploadedList, chunks)

    },

createFileChunk

// 创建分片
    createFileChunk(file,size=CHUNK_SIZE){
      const chunks = [] 
      let cur = 0
      while(cur<this.file.size){
        chunks.push({index:cur, file:this.file.slice(cur,cur+size)})
        cur+=size
      }
      return chunks
    },

uploadChunks

async uploadChunks(uploadedList=[], chunks){

      this.chunks = chunks.map((chunk,index)=>{
        // 切片的名字 hash+index
        const name = hash +'-'+ index
        return {
          hash,
          name,
          index,
          chunk:chunk.file,
          // 设置进度条,已经上传的,设为100
          progress:uploadedList.indexOf(name)>-1 ?100:0
        }
      })

      const requests = this.chunks
        .filter(chunk=>uploadedList.indexOf(chunk.name)==-1)
        .map((chunk,index)=>{
          // 转成promise
          const form = new FormData()
          form.append('chunk',chunk.chunk)
          form.append('hash',chunk.hash)
          form.append('name',chunk.name)
          // form.append('index',chunk.index)
          return {form, index:chunk.index,error:0}
        })
        .map(({form,index})=> this.$http.post('/uploadfile',form,{
          onUploadProgress:progress=>{
            // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
            this.chunks[index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
          }
        }))
      // @todo 并发量控制 
      // 尝试申请tcp链接过多,也会造成卡顿
      // 异步的并阿叔控制,
      // await Promise.all(requests)

      // await this.sendRequest(requests)
      await Promise.all(requests)
      await this.mergeRequest()

    }

合并文件

async mergeRequest(){
      const ret = await this.$http.post('/mergefile',{
        ext:this.file.name.split('.').pop(),
        size:CHUNK_SIZE,
        hash:this.hash
      })
    },

可以使用md5实现文件的唯一性

import sparkMD5 from 'spark-md5'

// 计算当前文件的hash
    async calculateHashSample(file){

      // 布隆过滤器  判断一个数据存在与否
      // 1个G的文件,抽样后5M以内
      // hash一样,文件不一定一样
      // hash不一样,文件一定不一样
      return new Promise(resolve=>{
        const spark = new sparkMD5.ArrayBuffer()
        const reader = new FileReader()

        const size = file.size
        const offset = 2*1024*1024
        // 第一个2M,最后一个区块数据全要
        let chunks = [file.slice(0,offset)]

        let cur = offset
        while(cur<size){
          if(cur+offset>=size){
            // 最后一个区快
            chunks.push(file.slice(cur, cur+offset))

          }else{
            // 中间的区块
            const mid = cur+offset/2
            const end = cur+offset
            chunks.push(file.slice(cur, cur+2))
            chunks.push(file.slice(mid, mid+2))
            chunks.push(file.slice(end-2, end))
          }
          cur+=offset
        }
        // 中间的,取前中后各2各字节
        reader.readAsArrayBuffer(new Blob(chunks))
        reader.onload = e=>{
          spark.append(e.target.result)
          this.hashProgress = 100
          resolve(spark.end())
        }
      })
    },

然后开始对文件进行分割

var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
    //每10M切割一段,这里只做一个切割演示,实际切割需要循环切割,
    var slice = e.target.result.slice(0, 10*1024*1024);
});

h5上传一个(一片)

const formdata = new FormData();
formdata.append('0', slice);
//这里是有一个坑的,部分设备无法获取文件名称,和文件类型,这个在最后给出解决方案
formdata.append('filename', file.filename);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
    //xhr.responseText
});
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);

function updateProgress(event) {
    if (event.lengthComputable) {
        //进度条
    }
}


7
 

这里给出常见的图片和视频的文件类型判断

function checkFileType(type, file, back) {
/**
* type png jpg mp4 ...
* file input.change=> this.files[0]
* back callback(boolean)
*/
    var args = arguments;
    if (args.length != 3) {
        back(0);
    }
    var type = args[0]; // type = '(png|jpg)' , 'png'
    var file = args[1];
    var back = typeof args[2] == 'function' ? args[2] : function() {};
    if (file.type == '') {
        // 如果系统无法获取文件类型,则读取二进制流,对二进制进行解析文件类型
        var imgType = [
            'ff d8 ff', //jpg
            '89 50 4e', //png

            '0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
            '0 0 0 18 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
            '0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4

            '0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
            '0 0 0 0 66 74 79 70 6D 70 34 32', //m4v

            '0 0 0 14 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 6D 6F 6F 76', //mov

            '4F 67 67 53 0 02', //ogg
            '1A 45 DF A3', //ogg

            '52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)
        ];
        var typeName = [
            'jpg',
            'png',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'm4v',
            'm4v',
            'mov',
            'mov',
            'mov',
            'ogg',
            'ogg',
            'avi',
        ];
        var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
        var reader = new FileReader();
        reader.readAsArrayBuffer(file);
        reader.addEventListener("load", function(e) {
            var slice = e.target.result.slice(0, sliceSize);
            reader = null;
            if (slice && slice.byteLength == sliceSize) {
                var view = new Uint8Array(slice);
                var arr = [];
                view.forEach(function(v) {
                    arr.push(v.toString(16));
                });
                view = null;
                var idx = arr.join(' ').indexOf(imgType);
                if (idx > -1) {
                    back(typeName[idx]);
                } else {
                    arr = arr.map(function(v) {
                        if (i > 3 && i < 8) {
                            return 'x';
                        }
                        return v;
                    });
                    var idx = arr.join(' ').indexOf(imgType);
                    if (idx > -1) {
                        back(typeName[idx]);
                    } else {
                        back(false);
                    }

                }
            } else {
                back(false);
            }

        });
    } else {
        var type = file.name.match(/\.(\w+)$/)[1];
        back(type);
    }
}

调用方法如下

checkFileType('(mov|mp4|avi)',file,function(fileType){
    // fileType = mp4,
    // 如果file的类型不在枚举之列,则返回false
});

上面上传文件的一步,可以改成:

formdata.append('filename', md5code+'.'+fileType);

有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断

后端主要做的内容为:根据前端传给后台的md5值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传

如果想要暂停切片的上传,可以使用XMLHttpRequest的 abort方法

#使用场景

  • 大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度
  • 网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
  • 流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见

#小结

当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如

  • 切片上传失败怎么办
  • 上传过程中刷新页面怎么办
  • 如何进行并行上传
  • 切片什么时候按数量切,什么时候按大小切
  • 如何结合 Web Worker 处理大文件上传
  • 如何实现秒传

#参考文献

  • https://gitee.com/fanyouf/nb/commit/80813904c644f4ed088968a32b81ebda068af345

  • https://segmentfault.com/a/1190000009448892

  • https://baike.baidu.com/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

大文件上传如何做断点续传? 的相关文章

  • 阿里云2核4G服务器优惠价格30元3个月?小心坑

    2024年阿里云2核4G服务器优惠价格30元3个月 活动 https t aliyun com U bLynLC 配置为云服务器ECS经济型e实例ecs e c1m2 large 3M固定带宽 系统盘为40GB ESSD Entry 活动打
  • 实时获取建材网商品数据:API实现详解与代码示例

    一 引言 随着电子商务的快速发展 实时获取商品数据对于企业决策 市场分析以及数据驱动的营销策略至关重要 建材网作为国内知名的建材信息平台 提供了API接口 使得第三方开发者可以方便地获取商品数据 本文将详细介绍如何使用 建材网的API接口
  • Kali Linux 安全渗透核心总结,444页核心知识点

    就像IT人离不开Linux系统一样 网安人也离不开Kali Linux 作为攻击性防御和渗透测试的代名词 越来越多的人开始学习Kali 如果你也对kali感兴趣 又想深入了解这方面内容 不妨收藏一下这份Kali Linux安全渗透教程 共4
  • 在阿里云ECS云服务器上部署和使用开源的应用程序容器引擎Docker

    Docker 是一个开源的应用程序容器引擎 具有可移植性 可扩展性 高安全性和可管理性等优势 它允许开发人员将应用程序和依赖项打包到可移植容器中 从而在 Linux 机器上高效构建 部署和管理应用程序 阿里云提供Docker镜像仓库 用于快
  • 网络基础面试题(二)

    11 什么是网桥 防火墙的端口防护是指什么 网桥是一种网络设备 用于连接两个或多个局域网 LAN 并转发数据包 它能够根据MAC地址来识别和转发数据 提高网络的传输效率和安全性 防火墙的端口防护是指对防火墙上的各个端口进行保护和限制 只允许
  • 如何使用内网穿透实现iStoreOS软路由公网远程访问局域网电脑桌面

    文章目录 简介 一 配置远程桌面公网地址 二 家中使用永久固定地址 访问公司电脑 具体操作方法是 简介 软路由 是PC的硬件加上路由系统来实现路由器
  • “性能压测揭密:关键指标分析!“

    在进行全链路压测和性能测试时 需要关注多个关键性能指标 KPIs 来评估系统的性能表现 以下是一些常见的性能测试指标 1 吞吐量 Throughput 系统在单位时间内能够处理的请求数量或事务数量 通常以每秒请求数 RPS TPS 来衡量
  • 如何查看崩溃日志

    目录 描述 思路 查看ipa包崩溃日志 简单查看手机崩溃信息几种方式 方式1 手机设置查看崩溃日志 方式2 Xocde工具 方式3 第三方软件克魔助手 环境配置 实时日志 奔溃日志分析 方式四 控制台资源库 线上崩溃日志 线上监听crash
  • 改善python程序的91建议记录

    使用else子句简化循环 异常处理 案例1 执行sql异常时处理 def save db obj try save attr1 db execute a sql stmt obj attr1 save attr2 db execute an
  • 如何解读服务器的配置和架构?

    在当今数字化时代 服务器作为企业或组织的重要基础设施 其配置和架构对于保障业务的稳定运行至关重要 如何解读服务器的配置和架构 成为了一个备受关注的话题 本文将围绕服务器配置和架构的解读进行深入探讨 帮助读者更好地理解服务器的性能 扩展性和安
  • 自定义编写zabbix_agent脚本

    vi usr lib systemd system zabbix agent servicce Unit Description Zabbix Agent After syslog target After network target S
  • centos系统有什么好处?

    CentOS是一种基于开源代码的Linux操作系统 它有以下几个优势 1 稳定性 CentOS是一种非常稳定的操作系统 它的代码经过了严格的测试和审查 因此它非常适合作为服务器操作系统使 用 2 安全性 由于CentOS是基于开源代码的操作
  • Jenkins流水线怎么做?

    问CHAT Jenkins流水线怎么做 CHAT回复 Jenkins流水线是一种创建 测试和部署应用程序的方法 以下是为Jenkins创建流水线的步骤 1 安装Jenkins 首先你需要在你的服务器上安装Jenkins 这个过程可能会根据你
  • RF自动化环境安装+自动化实例解析

    RF定义 通用型的 自动测试框架 绝大部分的软件的的自动化系统都可以采用它 特点 测试数据文件 Test Data 对应一个个的测试用例 测试数据文件里面使用的功能小模块叫关键字 由测试库 Test Library Robot Framew
  • 远程控制软件安全吗?一文看懂ToDesk、RayLink、TeamViewer、Splashtop相关安全机制_raylink todesk

    目录 一 前言 二 远程控制中的安全威胁 三 国内外远控软件安全机制 ToDesk RayLink Teamviewer Splashtop 四 安全远控预防 一 前言 近期 远程控制话题再一次引起关注 据相关新闻报道 不少不法分子利用远程
  • 什么是充放电振子理论?

    CHAT回复 充放电振子模型 Charging Reversal Oscillator Model 是一种解释ENSO现象的理论模型 这个模型把ENSO现象比喻成一个 热力学振荡系统 在这个模型中 ENSO现象由三个组成部分 充电 Char
  • 一台java服务器可以跑多少个线程?

    一台java服务器可以跑多少个线程 一台java服务器能跑多少个线程 这个问题来自一次线上报警如下图 超过了我们的配置阈值 打出jstack文件 通过IBM Thread and Monitor Dump Analyzer for Java
  • Kubernetes (十三) 存储——持久卷-动静态分配

    一 简介 二 NFS持久化存储步骤 静态分配 1 集群外主机用上次nfsdata共享目录中创建用来测试的pv 1 3 目录 用来对三个静态pv 2 创建pv的应用文件 vim pv yaml apiVersion v1 kind Persi
  • 内网安全:隧道技术详解

    目录 隧道技术 反向连接技术 反向连接实验所用网络拓扑图及说明 网络说明 防火墙限制说明 实验前提说明 实战一 CS反向连接上线 拿下Win2008 一 使用转发代理上线创建监听器 二 上传后门执行上线 隧道技术 SMB协议 SMB协议介绍
  • 【安全】简单解析统一身份认证:介绍、原理和实现方法

    深入解析统一身份认证 介绍 原理和实现方法 导语 统一身份认证是什么 统一身份认证的原理 统一身份认证的实现 结语 导语 随着互联网的发展和各种在线服务的普及 用户在不同的应用和平台上需要进行多次身份验证 为了简化用户的登录和减少重复操作

随机推荐

  • FreeSurfer和FSL的安装和使用(脑部图像去除头骨+对图像和label同时进行仿射对齐)教程

    FreeSurfer当前只支持Linux系统和Mac OS 我所使用的系统是Ubuntu 16 0 4 FreeSurfer的安装耗时较小 但是在处理时耗时较长 可能需要数个小时 甚至一天 这个取决于机器性能 但是和GPU好像没太大关系 下
  • (转)基于FPGA技术的FAST行情解码研究

    http mp weixin qq com s BviH6gAqej6lHd9XxFKUfg 交易技术前沿 基于FPGA技术的FAST行情解码研究 钟浪辉 陈敏 陈坚 刘啸林 秦轶轩 李道双 2017 09 08 上交所技术服务 本文选自
  • 数据库分表分库理论

    1 数据切分 关系型数据库本身比较容易成为系统瓶颈 单机存储容量 连接数 处理能力都有限 当单表的数据量达到1000W或100G以后 由于查询维度较多 即使添加从库 优化索引 做很多操作时性能仍下降严重 此时就要考虑对其进行切分了 切分的目
  • 第十四届蓝桥杯模拟赛(第一期)—保姆级解释(C语言版)

    1 二进制位数 问题描述 十进制整数 2 在十进制中是 1 位数 在二进制中对应 10 是 2 位数 十进制整数 22 在十进制中是 2 位数 在二进制中对应 10110 是 5 位数 请问十进制整数 2022 在二进制中是几位数 incl
  • cmake(03) : 平台,架构及编译器判断

    1 cmake检测平台架构及编译器的原理 cmake在检测编译器的时候 用了一种很暴力的方法 可以在不运行实际代码的情况下直接知道目标平台的信息 做法是这样的 首先生成一个 cpp文件 包含一些平台检测的 ifdef Identify kn
  • Linux 环境下 docker 安装 ES 7.15.2 和 kibana 7.15.2 详细步骤

    目标 在一台机器内设置3个ES节点和1个kibana节点正常运行 条件 本机器内的IP 192 168 211 130 1 首先安装docker 步骤详见链接https blog csdn net m0 55380752 article d
  • 期货投机和套利交易

    一 期货投机的概念 1 期货投机的定义 指交易者通过预测期货合约未来价格的变化 以在期货市场上获取价差收益为目的的期货交易行为 期货交易具有保证金的杆杠机制 双向交易和对冲机制 当日无负债的结算制度 强行平仓制度 使得期货投机易有高收益 高
  • 微信小程序_安装第三方的UI组件库(详细步骤)

    微信小程序的UI组件库 在我了解的 有两种方式 一种是微信小程序的官方文档自带的小程序 另一种是vant的小程序的UI组件库 一 官方自带的小程序的安装步骤 官方文档 https developers weixin qq com minip
  • Mysql管理

    一 Mysql 一 前言 MySQL是一个关系型数据库管理系统 由瑞典MySQL AB 公司开发 目前属于 Oracle 旗下产品 MySQL 是最流行的关系型数据库管理系统之一 在 WEB 应用方面 MySQL是最好的 RDBMS Rel
  • C++11:转移语义

    为什么需要转移语义 gt File Name main cpp gt Author Xianghao Jia gt mail xianghaojia sina com gt Created Time Mon 09 Dec 2019 04 2
  • ubuntu创建新用户并设置samba服务

    1 新建自己的用户并查看 sudo useradd m s bin bash 用户名 sudo passwd 用户名 ls home t 或者 1创建一个新的普通用户 m 表示用户 s表示shell环境 sudo useradd m gue
  • Selenium:网页屏幕截图

    前言 在学习 Selenium 做 UI自动化时 往往会遇到需要截图的时候 框架自带截图方法 方法 方法释义 save screenshot filename 截取当前屏幕截图 并保存为指定文件 此方法没必要使用 get screensho
  • iOS音视频—Shell脚本语言(语法-echo命令&参数传递)

    That wonderful world is waiting for me Shell脚本语言 语法 echo命令 1 显示普通字符串 echo iPhoneX 标配 8388 2 显示转义字符 echo iPhoneX 顶配 9688
  • 每日一题:路径计数

    路径计数 题目 Daimayuan Online Judge f i j 表示从左上角走到 i j 的方案数 状态转移 i j 由 i 1 j 和 i j 1 转移而来 初始状态 得使得f 1 1 为1 所以初始化f 1 0 或者f 0 1
  • 基于单光子探测的多脉冲周期符合远距离测距

    激光测距技术通过发射主动激光信号对目标进行探测 接收由目标漫反射回来的回波信号并进行统计 处理及换算 从而得到目标的距离 速度信息 实现对目标距离信息的探测 凭借其系统简单 操作灵活 高精度等特点 被广泛运用于民用 科研及军事等各类场合 基
  • Lambda表达式使用详细讲解

    目录 1 新思想 1 1函数式编程思想 1 2 函数式接口 2 通往lambda之路 2 1 什么是lambda表示式 2 2 lambda表示式有哪些特点 2 3 lambda表示式使用场景 2 4 lambda表示式语法 2 5 Lam
  • [Unity] Input.mousetion 屏幕坐标转世界坐标。

    代码如下 Vector3 screenPos Input mousePosition screenPos z 5 0f Vector3 p1 Camera main ScreenToWorldPoint screenPos Vector3
  • 释放数据价值这道难题,Smartbi V11有解

    未来简史 预言 数据将成为人们未来的信仰 未来已来 将至已至 如今 数据所扮演的角色与作用超乎想象 从政府将数据要素列入生产要素之中 到数据驱动型业务场景涌现 企业与组织对于数据及其价值的认可度明显提升 如何充分释放数据价值已成为所有企业与
  • Dijkstra与Bellman-Ford算法对比

    文章目录 TOC Dijkstra Dijkstra 伪代码 Dijkstra 为什么不能有负权重 Dijkstra算法复杂度 Bellman Ford算法 Bellman Ford算法伪代码 Bellman Ford判断是否有负权 Bel
  • 大文件上传如何做断点续传?

    是什么 不管怎样简单的需求 在量级达到一定层次时 都会变得异常复杂 文件上传简单 文件变大就复杂 上传大文件时 以下几个变量会影响我们的用户体验 服务器处理数据的能力 请求超时 网络波动 上传时间会变长 高频次文件上传失败 失败后又需要重新