【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传

news2024/12/29 10:38:16

目录

中等文件代理服务器放行:10MB为单位

proxy

nginx

大文件切片:100MB为单位

断点:存储切片hash

前端方案A

localstorage

后端方案B

服务端

上传

前端

后端

下载

前端

后端

多个大文件传输:spark-md5

哈希碰撞

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求

中等文件代理服务器放行:10MB为单位

proxy

proxy_buffering来控制是否启用代理缓冲,

proxy_buffer_sizeproxy_buffers来调整缓冲区的大小

nginx

在nginx.conf配置文件中,找到或添加一个 httpserverlocation 块,具体位置取决于希望修改的范围。在该块中,添加或修改 client_max_body_size 指令

http {
    ...
    server {
        ...
        location /upload {
            client_max_body_size 100M;
            ...
        }
        ...
    }
    ...
}

检查配置文件是否有语法错误:

sudo nginx -t

如果没有报告错误,重新加载Nginx以使配置更改生效:

sudo systemctl reload nginx

React版本见:前端文件流、切片下载和上传:优化文件传输效率与用户体验 - 掘金

<pre> 标签可定义预格式化的文本。

<pre> 标签的一个常见应用就是用来表示计算机的源代码

Blob(Binary Large Object)对象:存储二进制数据

ArrayBuffer 对象类型:缓存二进制数据

大文件切片:100MB为单位

每个片段大小通常在几百KB到几MB之间

 

断点:存储切片hash

前端方案A

localstorage

  1. 容量限制: 不同浏览器可能有不同的限制,但通常容量限制在 5MB 到 10MB 之间。用于存储断点下标够用

  2. 遵循同源策略

  3. 持久性: 关闭后也存在,只有用户主动清除浏览器缓存或使用代码删除数据,

  4. 访问同步,在读取或写入大量数据时,可能阻塞

  5. 数据类型: string

  6. 适用场景:容量小,非敏感,持久性数据。如果需要处理更大容量的数据,或者需要在不同域之间共享数据,可以考虑 IndexedDB 或服务器端存储。

这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

后端方案B

服务端

前端方案有一个缺陷,如果换了个浏览器就localstorage就失效了,所以推荐后者

上传

前端

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="startUpload">Start Upload</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      file: null,
      chunkSize: 1024 * 1024, // 1MB
      totalChunks: 0,
      uploadedChunks: [],
    };
  },
  methods: {
    handleFileChange(event) {
      this.file = event.target.files[0];
    },
    startUpload() {
      if (this.file) {
        this.totalChunks = this.getTotalChunks();
        this.uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
        this.uploadChunks(0);
      }
    },
    uploadChunks(startChunk) {
      if (startChunk >= this.totalChunks) {
        console.log('Upload complete');
        localStorage.removeItem('uploadedChunks'); 
        return;
      }
      //模拟每次至多发起5个并发请求,实际开发中根据请求资源的限定决定?
      const endChunk = Math.min(startChunk + 5, this.totalChunks);

      const uploadPromises = [];
      for (let chunkIndex = startChunk; chunkIndex < endChunk; chunkIndex++) {
        if (!this.uploadedChunks.includes(chunkIndex)) {
          const startByte = chunkIndex * this.chunkSize;
          const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.file.size);
          const chunkData = this.file.slice(startByte, endByte);

          const formData = new FormData();
          formData.append('chunkIndex', chunkIndex);
          formData.append('file', chunkData);

          uploadPromises.push(
            fetch('/upload', {
              method: 'POST',
              body: formData,
            })
          );
        }
      }
      Promise.allSettled(uploadPromises)
        .then(() => {
          const newUploadedChunks = Array.from(
            new Set([...this.uploadedChunks, ...Array.from({ length: endChunk - startChunk }, (_, i) => i + startChunk)])
          );
          this.uploadedChunks = newUploadedChunks;
          localStorage.setItem('uploadedChunks', JSON.stringify(this.uploadedChunks));

          this.uploadChunks(endChunk);
        })
        .catch(error => {
          console.error('Error uploading chunks:', error);
        });
    },
    getTotalChunks() {
      return Math.ceil(this.file.size / this.chunkSize);
    },
  },
};
</script>

后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');

app.use(express.json());
app.use(express.static(chunkDirectory));

const storage = multer.diskStorage({
  destination: chunkDirectory,
  filename: (req, file, callback) => {
    callback(null, `chunk_${req.body.chunkIndex}`);
  },
});

const upload = multer({ storage });

app.post('/upload', upload.single('file'), (req, res) => {
  const { chunkIndex } = req.body;
  console.log(`Uploaded chunk ${chunkIndex}`);
  res.sendStatus(200);
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

下载

前端

<template>
  <div>
    <button @click="startDownload">Start Download</button>
  </div>
</template>

<script>
import { saveAs } from 'file-saver';

export default {
  data() {
    return {
      totalChunks: 0,
      chunkSize: 1024 * 1024, // 默认1M
      fileNm: "file.txt",
      downloadedChunks: [],
      chunks: [], // 存储切片数据
      concurrentDownloads: 5, // 并发下载数量
    };
  },
  methods: {
    startDownload() {
      this.fetchMetadata();
    },
    fetchMetadata() {
      fetch('/metadata')
        .then(response => response.json())
        .then(data => {
          this.totalChunks = data.totalChunks;
          this.chunkSize = data.chunkSize;
          this.fileNm = data.fileNm;
          this.continueDownload();
        })
        .catch(error => {
          console.error('Error fetching metadata:', error);
        });
    },
   async continueDownload() {
      const storedChunks = JSON.parse(localStorage.getItem('downloadedChunks')) || [];
      this.downloadedChunks = storedChunks;

      const downloadPromises = [];
      let chunkIndex = 0;

      while (chunkIndex < this.totalChunks) {
        const chunkPromises = [];
        
        for (let i = 0; i < this.concurrentDownloads; i++) {
          if (chunkIndex < this.totalChunks && !this.downloadedChunks.includes(chunkIndex)) {
            chunkPromises.push(this.downloadChunk(chunkIndex));
          }
          chunkIndex++;
        }

        await Promise.allSettled(chunkPromises);
      }
// 当所有切片都下载完成时 合并切片
      this.mergeChunks();
    },
    
    downloadChunk(chunkIndex) {
      return new Promise((resolve, reject) => {
        const startByte = chunkIndex * this.chunkSize;
        const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.totalChunks * this.chunkSize);
 //我不太清楚实际开发中切片是靠idx,还是startByte、endByte,还是两者都用....
        fetch(`/download/${chunkIndex}?start=${startByte}&end=${endByte}`)
          .then(response => response.blob())
          .then(chunkBlob => {
            this.downloadedChunks.push(chunkIndex);
            localStorage.setItem('downloadedChunks', JSON.stringify(this.downloadedChunks));

            this.chunks[chunkIndex] = chunkBlob; // 存储切片数据

            resolve();
          })
          .catch(error => {
            console.error('Error downloading chunk:', error);
            reject();
          });
      });
    },
    mergeChunks() {
      const mergedBlob = new Blob(this.chunks);
      // 保存合并后的 Blob 数据到本地文件
      saveAs(mergedBlob, this.fileNm);
      // 清空切片数据和已下载切片的 localStorage
      this.chunks = [];
      localStorage.removeItem('downloadedChunks');
    },
  },
};
</script>

后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');

app.use(express.json());

app.get('/metadata', (req, res) => {
  const filePath = path.join(__dirname, 'file.txt'); 
  const chunkSize = 1024 * 1024; // 1MB
  const fileNm='file.txt';
  const fileStats = fs.statSync(filePath);
  const totalChunks = Math.ceil(fileStats.size / chunkSize);
  res.json({ totalChunks, chunkSize, fileNm });
});

app.get('/download/:chunkIndex', (req, res) => {
  const chunkIndex = parseInt(req.params.chunkIndex);
  const chunkSize = 1024 * 1024; // 1MB
  const startByte = chunkIndex * chunkSize;
  const endByte = (chunkIndex + 1) * chunkSize;

  const filePath = path.join(__dirname, 'file.txt'); 

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.status(500).send('Error reading file.');
    } else {
      const chunkData = data.slice(startByte, endByte);
      res.send(chunkData);
    }
  });
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

多个大文件传输:spark-md5

MD5(Message Digest Algorithm 5):哈希函数

若使用 文件名 + 切片下标 作为切片 hash,这样做文件名一旦修改就失去了效果,

所以应该用spark-md5根据文件内容生成 hash

webpack 的contenthash 也是基于这个思路实现的

另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

// /public/hash.js

// 导入脚本
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;

  // 递归加载下一个文件块
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);

      // 检查是否处理完所有文件块
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        // 更新进度百分比并发送消息
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });

        // 递归调用以加载下一个文件块
        loadNext(count);
      }
    };
  };

  // 开始加载第一个文件块
  loadNext(0);
};
  1. 切片hash/传输等目的都是为了

  2. 内存效率: 对于大文件,一次性将整个文件加载到内存中可能会导致内存占用过高,甚至造成浏览器崩溃。通过将文件切成小块,在处理过程中只需要操作单个块,减小了内存的压力。

  3. 性能优化: 如果直接将整个文件传递给哈希函数,可能会导致计算时间较长,尤其是对于大文件。分成小块逐个计算哈希值,可以并行处理多个块,提高计算效率。

  4. 错误恢复: 在上传或下载过程中,网络中断或其他错误可能会导致部分文件块没有传输成功。通过分块计算哈希,你可以轻松检测到哪些块没有正确传输,从而有机会恢复或重新传输这些块。

  5. // 生成文件 hash(web-worker)
    calculateHash(fileChunkList) {
      return new Promise(resolve => {
        // 创建一个新的 Web Worker,并加载指向 "hash.js" 的脚本
        this.container.worker = new Worker("/hash.js");
    
        // 向 Web Worker 发送文件块列表
        this.container.worker.postMessage({ fileChunkList });
    
        // 当 Web Worker 发送消息回来时触发的事件处理程序
        this.container.worker.onmessage = e => {
          const { percentage, hash } = e.data;
    
          // 更新 hash 计算进度
          this.hashPercentage = percentage;
    
          if (hash) {
            // 如果计算完成,解析最终的 hash 值
            resolve(hash);
          }
        };
      });
    },
    
    // 处理文件上传的函数
    async handleUpload() {
      if (!this.container.file) return;
    
      // 将文件划分为文件块列表
      const fileChunkList = this.createFileChunk(this.container.file);
    
      // 计算文件 hash,并将结果存储在容器中
      this.container.hash = await this.calculateHash(fileChunkList);
    
      // 根据文件块列表创建上传数据对象
      this.data = fileChunkList.map(({ file, index }) => ({
        fileHash: this.container.hash,
        chunk: file,
        hash: this.container.file.name + "-" + index,
        percentage: 0
      }));
    
      // 上传文件块
      await this.uploadChunks();
    }
    

哈希碰撞

输入空间通常大于输出空间,无法完全避免碰撞

哈希(A) = 21 % 10 = 1

哈希(B) = 31 % 10 = 1

所以spark-md5 文档中要求传入所有切片并算出 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求

面试官桀桀一笑:你没做过大文件上传功能?那你回去等通知吧! - 掘金

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

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

相关文章

002-Nacos 简单集群模式源码解析

目录 介绍架构分析添加实例-同步信息给其他集群服务添加实例-提交同步任务添加实例-执行同步任务实例健康状态监控 介绍 Nacos 启动默认会使用集群模式&#xff0c;也就是没有带有-m standalone 的时候就是用的简单集群模式 另外我们再分析单机模式注册实例的时候最后一部分是把…

【王道-绪论】

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.1 操作系统的概念功能和定义一、操作系统的概念和定义二、操作系统的功能和目标1、资源的管理者2、向用户提供服务2.1图形化用户接口2.2联机命令接口2.3脱机命令接口2.4程序接口 3、对硬件机器的拓展 三、总结 No.2 操作系…

Spring Cloud 系列之OpenFeign:(7)链路追踪zipkin

传送门 Spring Cloud Alibaba系列之nacos&#xff1a;(1)安装 Spring Cloud Alibaba系列之nacos&#xff1a;(2)单机模式支持mysql Spring Cloud Alibaba系列之nacos&#xff1a;(3)服务注册发现 Spring Cloud 系列之OpenFeign&#xff1a;(4)集成OpenFeign Spring Cloud …

第三章内存管理

1.内存的基础知识 内存可存放数据。程序执行前需要先放到内存中才能被CPU处理――缓和CPU与硬盘之间的速度矛盾 指令中的地址参数直接给出了变量x的实际存放地址(物理地址) 1&#xff0e;绝对装入 绝对装入:在编译时&#xff0c;如果知道程序将放到内存中的哪个位置,编译程序将…

springboot+Vue--打基础升级--(二)写个主菜单导航界面

1. 华为OD机考题 答案 2023华为OD统一考试&#xff08;AB卷&#xff09;题库清单-带答案&#xff08;持续更新&#xff09; 2023年华为OD真题机考题库大全-带答案&#xff08;持续更新&#xff09; 2. 面试题 一手真实java面试题&#xff1a;2023年各大公司java面试真题汇总--…

ansible(2)-- ansible常用模块

部署ansible&#xff1a;ansible&#xff08;1&#xff09;-- 部署ansible连接被控端_luo_guibin的博客-CSDN博客 目录 一、ansible常用模块 1.1 ping 1.2 command 1.3 raw 1.4 shell 1.5 script 1.6 copy 1.7 template 1.8 yum 11.0.1.13 主控端(ansible)11.0.1.12 被控端(k8s…

K8S deployment挂载

挂载到emptyDir 挂载在如下目录&#xff0c;此目录是pod所在的node节点主机的目录&#xff0c;此目录下的data即对应容器里的/usr/share/nginx/html&#xff0c;实现目录挂载 apiVersion: apps/v1 kind: Deployment metadata:annotations:deployment.kubernetes.io/revision: …

Qt实现简单的漫游器

文章目录 Qt的OpenGL窗口GLSL的实现摄像机类的实现简单的漫游器 Qt的OpenGL窗口 Qt主要是使用QOpenGLWidget来实现opengl的功能。  QOpenGLWidget 提供了三个便捷的虚函数&#xff0c;可以重载&#xff0c;用来重新实现典型的OpenGL任务&#xff1a; paintGL&#xff1a;渲染…

零基础入门网络安全,收藏这篇不迷茫【2023最新】

前言 最近收到不少关注朋友的私信和留言&#xff0c;大多数都是零基础小友入门网络安全&#xff0c;需要相关资源学习。其实看过的铁粉都知道&#xff0c;之前的文里是有过推荐过的。新来的小友可能不太清楚&#xff0c;这里就系统地叙述一遍。 01.简单了解一下网络安全 说白…

【ARM Linux 系统稳定性分析入门及渐进12 -- GDB内存查看命令 “x“(examine)】

文章目录 gdb 内存查看命令 examine 上篇文章&#xff1a;ARM Linux 系统稳定性分析入门及渐进11 – GDB( print 和 p 的使用| 和 &#xff1a;&#xff1a;的使用|ptype|{&#xff1c;type&#xff1e;} &#xff1c;addr&#xff1e; ) gdb 内存查看命令 examine examine是…

【C# 基础精讲】LINQ 基础

LINQ&#xff08;Language Integrated Query&#xff09;是一项强大的C#语言特性&#xff0c;它使数据查询和操作变得更加简洁、灵活和可读性强。通过使用LINQ&#xff0c;您可以使用类似SQL的语法来查询各种数据源&#xff0c;如集合、数组、数据库等。本文将介绍LINQ的基础概…

Kvm配置ovs网桥

环境&#xff1a;部署在kvm虚拟环境上&#xff08;让虚拟机和宿主机都可以直接从路由器获取到独立ip&#xff09; 1、安装ovs软件安装包并启动服务&#xff08;一般采用源码安装&#xff0c;此处用yum安装&#xff09; yum install openvswitch-2.9.0-3.el7.x86_64.rpm syste…

UVC摄像头

1 版本历史 1.1 UVC uvc_version UVC 1.0: Sep-4-2003 UVC 1.1: Jun-1-2005 UVC 1.5: August-9-2012, H.264 video codec. Linux 4.5 introduces UVC 1.5, but does not support H264. 1.2 V4L版本历史 Video4Linux取名的灵感来自1992 Video for Windows&#xff08;V4W&#x…

第一节 Dubbo框架的介绍

1. 什么是Dubbo &#xff1f; 官⽹地址&#xff1a; http://dubbo.apache.org/zh/ ⽬前&#xff0c;官⽹上是这么介绍的&#xff1a;Apache Dubbo 是⼀款⾼性能、轻量级的开源 Java 服务 框架 在⼏个⽉前&#xff0c;官⽹的介绍是&#xff1a;Apache Dubbo 是⼀款⾼性能、轻…

mysql的两张表left join 进行关联后,索引进行优化案例

一 mysql的案例 1.1 不加索引情况 1.表1没加索引 2.表2没加索引 3.查看索引 1.2 添加索引 1.表1添加索引 2.表2添加索引 3.查看

python使用dir()函数获取对象中可用的属性和方法(看不到python源码又想知道怎么调用,DLL调用分析,SDK二次开发技巧)

有时候调用一些SDK&#xff0c;但是人家又是封装成dll文件形式调用的&#xff0c;这时没法看源码&#xff0c;也不想看其对应的开发文档&#xff08;尤其有些开发文档写得还很难懂&#xff0c;或者你从某个开源社区拿过来&#xff0c;就根本没找到开发文档&#xff09;&#xf…

回归预测 | MATLAB实现IPSO-SVM改进粒子群优化算法优化支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现IPSO-SVM改进粒子群优化算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现IPSO-SVM改进粒子群优化算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xf…

回归预测 | MATLAB实现WOA-BP鲸鱼优化算法优化BP神经网络多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现WOA-BP鲸鱼优化算法优化BP神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现WOA-BP鲸鱼优化算法优化BP神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09;效果一览基本…

运动恢复结构(SfM)_OpenMVG_代码的核心逻辑

参考: 深蓝学院,基于图像的三维重建 课程 1. 输入输出 数据集 fountain,选⾃Lund University开源的三维重建数据集 (http://www.maths.lth.se/matematiklth/personal/calle/dataset/dataset.html ) 2. 特征提取与特征匹配

优于立方复杂度的 Rust 中矩阵乘法

优于立方复杂度的 Rust 中矩阵乘法 迈克克维特 跟随 发表于 更好的编程 6 分钟阅读 7月 <> 143 中途&#xff1a;三次矩阵乘法 一、说明 几年前&#xff0c;我在 C 年编写了 Strassen 矩阵乘法算法的实现&#xff0c;最近在 Rust 中重新实现了它&#xff0c;因为我继续…