大文件上传教程优化【详细讲解】

前言
这几天看了关于几篇关于大文件上传的文章,都用到了从零实现了大文件上传中的切片上传、断点续传、秒传、暂停等功能,其实既是面试题,也是在实际业务中会用到的功能,今天我就从0开始写一篇关于大文件上传的详细总结,也是借鉴了以下两篇文章的精髓做出的总结。字节跳动面试官:请你实现一个大文件上传和断点续传 、 全网最全面的"大文件上传"

背景
在实际业务中,甲方爸爸突然说:我要上传一个10G的学习资源👲

你:???

一般情况下,前端上传文件就是new FormData,然后把文件 append 进去,然后post发送给后端就完事了,但是文件越大,上传的文件也就越长,如果在上传过程中,突然被网管拔了网线,又或者电脑突然中木马了,又或者请求超时,等待过久等等情况,十分影响甲方大哥的体验。

所以这时候就要用到大文件上传了,确保能让用户在上传的时候想停就停(暂停功能),就算断网了也能继续接着上传(断点上传),如果是之前上传过这个文件了(服务器还存着),就不需要做二次上传了(秒传)

使用的技术栈

前端使用的是:vue+ts

后端使用的是:node.js

一,大文件的概念

        大文件上传是指通过互联网向服务器传输容量超过常规处理能力的文件(通常指大于100MB甚至数GB的文件)。它在现代互联网应用中广泛存在,如视频网站、云存储服务、企业数据备份等场景。其核心目标是在不中断、高效且安全的前提下完成大体积数据的传输。

二,核心技术

  • 分块上传(Chunked Upload)

    • 原理:将大文件分割成多个小块(chunks),逐个上传,最后在服务器端合并。

    • 优点

      • 减少单次上传的数据量,降低网络波动的影响。

      • 支持断点续传,上传失败时只需重传失败的部分。

    • 实现

      • 前端使用 File.slice() 方法将文件分块。

      • 后端接收分块并保存,最后合并。

    • 实现代码

    • 前端

    • import SparkMD5 from "spark-md5";
      import {ref} from 'vue'
      const CHUNK_SIZE = 1024 * 1024;
      const fileHash=ref<string>('')
      const fileName=ref<string>('')
      //文件分片
      const createChunks = (file: File) => {
        let cur = 0;
        let chunks = [];
        while (cur < file.size) {
          chunks.push(file.slice(cur, cur + CHUNK_SIZE));
          cur += CHUNK_SIZE;
        }
        return chunks;
      };
      //hash计算
      const calculateHash = (chunks: Blob[]) => {
        return new Promise((resolve) => {
          const targets:Blob[] = []; //存储所有参与计算
          const spark = new SparkMD5.ArrayBuffer();
          const fileReader = new FileReader();
          chunks.forEach((chunk, index) => {
            if (index === 0 || index === chunks.length - 1) {
              //第一个和最后一个切片全部参与计算
              targets.push(chunk);
            } else {
              targets.push(chunk.slice(0, 2));
              targets.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2));
              targets.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE));
            }
          });
          fileReader.readAsArrayBuffer(new Blob(targets));
          fileReader.onload = function (e) {
            spark.append((e.target as FileReader).result);
            // console.log("hash-------" + spark.end());
            resolve(spark.end())
          };
        });
      };
      
      //上传切片
      const uploadChunks= async(chunks:Blob[])=>{
        const data=chunks.map((chunk,index)=>{
          return{
            fileHash:fileHash.value,
            chunkHash:fileHash.value+'-'+index,
            chunk
          }
        })
        const formDatas=data.map((item)=>{
          const formData=new FormData()
          formData.append('fileHash',item.fileHash)
          formData.append('chunkHash',item.chunkHash)
          formData.append('chunk',item.chunk)
          return formData
        })
        // console.log(formDatas);
        const max=6//最大并发请求数
        let index=0
        const taskPoal=[]//请求池
        while(index<formDatas.length){
          const task=fetch('http://localhost:3001/upload',{
            method:'POST',
            body:formDatas[index]
          })
          taskPoal.splice(taskPoal.findIndex((item:any)=>item===task))
          taskPoal.push(task)
          if(taskPoal.length===max){
            await Promise.race(taskPoal)
            taskPoal.splice(taskPoal.findIndex((item:any)=>item===task))
          }
          index++
        }
        await Promise.all(taskPoal)
        
      }
      const handfile = async (e: Event) => {
        // console.log((e.target as HTMLInputElement).files);
        const file = (e.target as HTMLInputElement).files;
        if (!file) return;
        //读取选择的文件
        // console.log(file[0]);
        fileName.value=file[0].name
        //调用文件分片
        const chunks = createChunks(file[0]);
        // console.log(chunks);
        //调用hash计算
        const hash=await calculateHash(chunks);
        // console.log(hash);
        fileHash.value = hash as string;
        //上传分片
        uploadChunks(chunks)

      后端

    • var express = require('express');
      var router = express.Router();
      var multiparty = require('multiparty');
      // 导入 path 模块
      const path = require('path');
      // 导入 fs-extra 模块
      const fse = require('fs-extra');
      
      const extractExt=filename=>{
        return filename.slice(filename.lastIndexOf('.'),filename.length)
      }
      
      
      const upload_dir = path.resolve(__dirname, 'uploads')
      
      router.post('/upload', function (req, res) {
        const form = new multiparty.Form()
        form.parse(req, async function (err, fields, files) {
          if (err) {
            res.status(500).json({
              ok: false,
              msg: "上传失败"
            })
            return
          }
          // console.log(fields);
          // console.log(files);
          const fileHash = fields['fileHash'][0]
          const chunkHash = fields['chunkHash'][0]
          //临时存放目录
          const chunkPath = path.resolve(upload_dir, fileHash)
          if (!fse.existsSync(chunkPath)) {
            await fse.mkdir(chunkPath)
          }
          const oldPath=files['chunk'] [0] ['path']
          //将切片放到这个文件夹中
          await fse.move(oldPath,path.resolve(chunkPath,chunkHash))
          res.status(200).json({
            ok: true,
            msg: "上传成功"
          })
        })
      router.post('/merge', async function (req,res){
          const {fileHash,fileName,size}=req.body
          console.log(fileHash,fileName,size);
          //如果已经存在该文件,就没必要合并了
          const filePath=path.resolve(upload_dir,fileHash+extractExt(fileName))
          if(fse.existsSync(filePath)){
            res.status(200).json({
              ok: true,
              msg: "合并成功"
            })
            return
          }
          //如果不存在该文件,才去合并
          const chunkDir=path.resolve(upload_dir,fileHash)
          if(!fse.existsSync(chunkDir)){
            res.status(401).json({
              ok:false,
              msg:'合并失败'
            })
            return
          }
          const chunkPaths=await fse.readdir(chunkDir)
          console.log(chunkPaths);
          chunkPaths.sort((a,b)=>{
            return a.split('-') [1] - b.split('-') [1]
          })
          const list= chunkPaths.map((chunkName,index)=>{
            return new Promise(resolve=>{
            const chunkPath=path.resolve(chunkDir,chunkName)
            const readStrream=fse.createReadStream(chunkPath)
            const writeStream=fse.createWriteStream(filePath,{
                start:index*size,
                end:(index+1)*size
            })
            readStrream.on('end', async()=>{
              await fse.unlink(chunkPath)
              resolve()
            })
            readStrream.pipe(writeStream)
            })
            
          })
      await Promise.all(list)
      await fse.remove(chunkDir)
      
          res.status(200).json({
            ok: true,
            msg: "开始合并"
          })
        })
      })
      module.exports = router;
      

  • 断点续传

    • 原理:记录已上传的文件部分,上传中断后可以从断点继续上传。

    • 优点:避免重复上传,节省带宽和时间。

    • 实现

      • 前端记录已上传的分块信息。

      • 后端支持查询已上传的分块,跳过已上传部分。

    • 实现代码

    • 前端

      const uploadChunks = async (chunks: Blob[]) => {
          // 检查已上传的切片
          const response = await fetch('http://localhost:3004/check-chunks', {
              method: 'POST',
              headers: {
                  'content-Type': 'application/json'
              },
              body: JSON.stringify({
                  fileHash: fileHash.value
              })
          });
          const { uploadedChunks } = await response.json();
      

      后端

    • router.post('/check-chunks', async function (req, res) {
          const { fileHash } = req.body;
          const chunkDir = path.resolve(upload_dir, fileHash);
          let uploadedChunks = [];
          if (fse.existsSync(chunkDir)) {
              const files = await fse.readdir(chunkDir);
              uploadedChunks = files.map(file => file.split('-')[1]);
          }
          res.status(200).json({
              ok: true,
              uploadedChunks
          });
      });

  •  并行上传

    • 原理:同时上传多个分块,充分利用带宽。

    • 优点:加快上传速度,尤其是带宽充足时。

    • 实现

      • 前端使用多个并发请求上传不同分块。

      • 后端支持并发处理分块上传。

  • 压缩文件

     
    • 原理:上传前对文件进行压缩,减少传输数据量。

    • 优点:减少上传时间,节省带宽。

    • 注意:压缩和解压缩会消耗计算资源,需权衡压缩率和时间。

    • 实现代码

    • 前端

    • 先进行安装
      npm install compressorjs
      
      const handfile = async (e: Event) => {
          const file = (e.target as HTMLInputElement).files;
          if (!file) return;
          const originalFile = file[0];
          fileName.value = originalFile.name;
      
          // 压缩文件
          new Compressor(originalFile, {
              quality: 0.6, // 压缩质量,范围 0 - 1
              success: async (compressedResult) => {
                  // 调用文件分片
                  const chunks = createChunks(compressedResult);
                  // 调用 hash 计算
                  const hash = await calculateHash(chunks);
                  fileHash.value = hash as string;
                  // 上传分片
                  uploadChunks(chunks);
              },
              error: (err) => {
                  console.error('文件压缩失败:', err);
              }
          });
      };

      后端

    • const gunzip = zlib.createGunzip(); // 解压流
      
                  readStream.pipe(gunzip).pipe(writeStream);

三,前后端实现细节

  • 前端部分
    • 文件大小校验(前端+后端双重校验)。
    • 进度条可视化(WebSocket/轮询接口更新进度)。
    • 错误处理与重试机制(指数退避算法)。
  • 后端部分
    • 接收分片并存储(临时目录 vs 对象存储)。
    • 分片合并逻辑(按顺序拼接文件流)。
    • 安全防护(文件类型过滤、病毒扫描集成)。

四,大文件上传的优势和劣势

优势

  • 高效数据传输:大文件上传允许一次性传输大量数据,减少了多次传输小文件所带来的额外开销,如网络连接建立、数据包头传输等。对于需要传输大量数据的场景,如备份整个数据库或上传大型视频素材,大文件上传能够显著提高传输效率,节省时间。
  • 完整数据呈现:在许多情况下,数据是以一个整体的形式存在和使用的,如一个完整的设计项目文件、一部高清电影等。大文件上传能够确保数据的完整性,避免因分割成小文件传输而可能出现的文件丢失、顺序错误或数据不完整等问题,保证接收方可以获得完整、可用的数据。
  • 便于数据管理:将相关数据整合在一个大文件中进行上传,有利于数据的组织和管理。在存储和检索时,只需对一个文件进行操作,而不需要处理多个分散的小文件,降低了数据管理的复杂性,提高了数据的可维护性和可查找性。
  • 支持复杂数据结构:某些应用场景需要处理复杂的数据结构和关系,大文件可以包含各种类型的数据,如图像、音频、文本、元数据等,并且能够保持这些数据之间的关联和结构。例如,一个包含多个图层和特效的视频编辑项目文件,上传大文件可以确保所有相关数据都能被正确传输和处理,方便后续的编辑和制作。

劣势

  • 网络要求高:大文件上传需要占用大量的网络带宽,对网络环境的稳定性和速度要求较高。如果网络不稳定或带宽不足,上传过程中可能会出现卡顿、中断等问题,导致上传失败或需要重新上传,这在一定程度上会影响工作效率。
  • 上传时间长:由于大文件包含的数据量巨大,即使在网络条件良好的情况下,上传也可能需要较长时间。尤其是在处理数 GB 甚至更大的文件时,可能需要数小时甚至更长时间才能完成上传,这对于紧急需要使用数据的情况来说是一个明显的劣势。
  • 设备性能挑战大:在上传大文件时,发送端和接收端的设备需要具备足够的计算资源和存储能力来处理和临时存储这些数据。如果设备的 CPU、内存或硬盘性能不足,可能会导致设备运行缓慢,甚至出现死机等问题,影响其他业务的正常进行。
  • 容易出现数据错误:大文件在传输过程中,由于数据量庞大,出现错误的概率相对较高。可能会由于网络干扰、设备故障等原因导致数据损坏或丢失。虽然可以通过一些校验和纠错机制来检测和修复错误,但这也增加了传输的复杂性和成本。
  • 管理难度增加:对于服务器或存储系统来说,处理大文件需要更多的资源和更复杂的管理策略。例如,需要更大的存储空间来容纳大文件,可能需要更复杂的文件系统和存储架构来确保数据的安全和高效访问。同时,在进行数据备份、恢复和迁移时,大文件也会带来更大的挑战。

对劣势进行优化

        

网络方面

  • 优化网络配置
    • 可以升级网络设备,如更换更高性能的路由器、交换机等,以提升网络的稳定性和传输能力。
    • 对于企业用户,可考虑增加网络带宽,如从 100Mbps 提升到 1000Mbps 甚至更高,确保大文件上传时有足够的网络资源。
  • 采用 CDN 等技术:内容分发网络(CDN)可以将大文件缓存到离用户更近的节点,减少数据传输的距离和时间。在上传时,可以利用 CDN 的分布式存储特性,将文件分块上传到不同的节点,提高上传速度和稳定性。
  • 优化传输协议:选用更高效的传输协议或对现有协议进行优化。如 HTTP/3 相比 HTTP/2 在传输性能上有进一步提升,可减少传输延迟和丢包率。也可以在应用层对传输协议进行优化,如采用断点续传技术,当上传中断时能从断点处继续上传,而不是重新开始。

上传时间方面

  • 文件分块上传:将大文件分割成多个较小的块进行上传,然后在服务器端进行合并。这样可以并行上传多个文件块,充分利用网络带宽,同时也降低了单个文件上传失败的风险。
  • 多线程上传:在客户端使用多线程技术,同时开启多个线程进行文件上传,每个线程负责上传文件的一部分,从而加快整体上传速度。

设备性能方面

  • 硬件升级:根据设备的性能瓶颈,对硬件进行升级。如增加内存、更换更快的 CPU 或升级硬盘为固态硬盘(SSD)等,以提高设备处理和存储大文件的能力。
  • 资源调度优化:通过优化操作系统和应用程序的资源调度算法,确保在上传大文件时,系统能够合理分配计算资源和存储资源,避免因资源分配不合理导致的设备运行缓慢。可以关闭其他占用大量资源的应用程序,为大文件上传腾出更多的系统资源。

数据错误方面

  • 增加校验机制:除了常规的校验和纠错机制外,还可以采用更复杂的校验算法,如循环冗余校验(CRC)、哈希校验等,确保数据的完整性和准确性。在上传前和上传后对文件进行哈希计算,对比哈希值是否一致,以判断数据是否传输正确。
  • 数据冗余存储:采用数据冗余技术,如将文件同时存储在多个不同的服务器或存储设备上,即使某个存储位置的数据出现错误或丢失,也可以从其他位置获取到正确的数据。
  • 实时监控与修复:在上传过程中,实时监控数据的传输状态,一旦发现数据错误,立即暂停上传,进行错误定位和修复。可以利用一些数据修复工具或算法,对损坏的数据进行修复。

管理方面

  • 采用分布式存储系统:使用分布式存储系统,如 Ceph、GlusterFS 等,将大文件分散存储在多个节点上,不仅可以解决单个服务器存储容量不足的问题,还能提高数据的可用性和可扩展性。
  • 数据管理工具优化:引入专业的数据管理工具,对大文件进行集中管理和监控。这些工具可以提供文件的存储位置、访问权限、备份状态等详细信息,方便管理员进行管理和维护。
  • 制定合理的管理策略:根据业务需求和数据特点,制定合理的数据备份、恢复和迁移策略。例如,定期对大文件进行备份,采用增量备份或差异备份等方式,减少备份时间和存储空间。在进行数据迁移时,提前进行规划和测试,确保数据的安全和完整。

五,大文件上传适用的场景

      媒体与娱乐行业

            视频制作与分发:视频制作团队经常需要上传高分辨率的视频素材、成片到视频编辑平台、内容管理系统或视频分享网站。如影视制作公司将拍摄的 4K、8K 原始素材上传到云端剪辑平台,供剪辑师远程协作编辑;视频博主将制作好的长视频上传到抖音、B 站等平台与观众分享。

            音频制作与发布:音乐制作公司上传高音质的音乐文件到音乐发行平台,如将无损音乐格式的专辑上传到 QQ 音乐、网易云音乐等平台供用户收听和下载。

        教育领域

  •         在线教育资源共享:在线教育平台的教师需要上传教学视频、教学课件(如几十 MB 甚至上百 MB 的 PPT、PDF 文档)等学习资料,供学生在线学习和下载。比如学而思网校的老师将课程视频和配套资料上传到平台,学生可以根据自己的学习进度随时查看。
  •         学术研究资料上传:科研人员在学术交流平台上传学术论文、研究报告、实验数据等资料,方便同行之间的交流和查阅。如在知网等学术平台上,学者们上传自己的研究成果,供全球学术界检索和参考。

企业办公与协作

  •         企业文件存储与共享:企业员工需要将大型的项目文档、设计图纸、合同文件等上传到企业内部的文件管理系统或云盘,实现文件的共享和协同编辑。像建筑设计公司的设计师将建筑设计图纸上传到公司的项目管理系统,供项目团队成员查看和讨论。
  •         数据备份与归档:企业会将重要的业务数据、数据库文件等上传到备份服务器或云存储中,以防止数据丢失。如银行每天将大量的交易数据上传到数据中心进行备份,以便在出现故障或灾难时能够恢复数据。

医疗行业

  •         医学影像存储与传输:医院的放射科等部门需要将 CT、MRI 等医学影像文件上传到医院的 Picture Archiving and Communication System(PACS),方便医生随时调阅查看患者的影像资料进行诊断。例如患者在进行完 CT 检查后,影像数据会上传到 PACS 系统,供放射科医生和临床医生会诊使用。
  •         医疗数据共享与研究:医疗机构之间或科研团队在进行医学研究时,需要上传和共享大量的医疗数据,包括患者的病历、基因数据等。如多个医院合作进行一项关于癌症的研究项目,各医院将患者的相关数据上传到统一的研究平台,供科研人员进行分析。

云计算与大数据领域

  •         数据仓库与分析:数据分析师和数据科学家需要将大量的原始数据上传到数据仓库或大数据分析平台,如将电商平台的海量用户交易数据上传到阿里云的 MaxCompute 等平台,进行数据分析和挖掘,以获取有价值的商业洞察。
  •         机器学习与模型训练:在人工智能领域,研究人员需要上传大规模的数据集到云计算平台进行机器学习模型的训练。如百度的深度学习平台 PaddlePaddle,研究人员会将图像、文本等各种类型的大文件数据集上传到平台,训练图像识别、自然语言处理等模型。

分享

大文件上传的优势和劣势分别是什么?

企业如何确保大文件上传和下载的安全性?

大文件上传的速度受哪些因素影响?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值