【前端面试】中大文件:采样切片标识文件,动态切片大小,动态并发请求,断点续传,选择重传

目录

切片上传~spark-md5 原理:流式计算+分块处理

文件标识spark-md5:A->B

A.切片哈希值合并

B.首尾切片+其他切片前中后各取2M

计算hash:A->B(参考React的Fiber架构)

A.线程:web-worker

B.空闲:requestIdleCallback

异步并发控制:A->B(参考http2的多路复用)

A.promise.allSettled()

B.并发数max=3

a.同源并发连接数限制

b.分布式并发请求:不同域名/不同服务器

域名分片:分散到多个子域名

CDN(内容分发网络):分散到服务器

重传:参考SR选择重传协议

A.后端响应:失败次数

B.超时无响应:指数加权移动平均公式

动态:一轮并发请求中切片大小固定

并发数(TCP协议的快恢复)

1.设定阙值,从阙值开始并发

2.每次+1

3.遇到失败/重传,阙值减少到当前次数的一半,下次从阙值开始并发

切片大小:file.slice(startByte, endByte)

1.计算切片平均速度与上一次速度/最低速度的比率rate

2.下轮切片大小=min[切片大小*=rate,网速/RRT]

文件碎片清理

预/校验请求

上传请求参数:大文件标识,文件大小

响应:是否上传过+断点数组

传输请求

上传请求参数:切片内容和大文件标识、文件名、是否传输完成

固定切片大小:切片数,切片索引

动态调整大小:切片起始字节-结束字节

传输完成:最后一个并发请求数组,且并发请求数恢复成原值

响应

上传:每个切片上传后会返回是否成功,根据总切片数和已上传成功数更新进度条

下载:返回206状态码,报告进度

合并:原始为文件大小的null

中等文件:10MB为单位

正反向代理

代理缓冲

代理服务器放行

proxy

nginx

大文件切片:100MB为单位

断点:存储切片hash

前端方案A

localstorage

后端方案B

服务端

传输状态码

响应头Content-Range+206状态码

上传

前端

后端

下载

前端

后端

文件标识:spark-md5

哈希碰撞


切片上传~spark-md5原理:流式计算+分块处理

分块计算: spark-md5 将文件分成多个块(默认大小为 64 KB),然后逐个块计算 MD5 哈希值。这种分块的方式使得可以在读取文件的过程中逐步计算哈希值,而不需要等整个文件加载到内存中。

文件流处理: 通过使用 FileReader 或其他方式,spark-md5 可以以流的形式读取文件内容。每读取一个块,就对该块进行 MD5 计算。

文件标识spark-md5:A->B

另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态

1.5G的文件,全量大概要20秒,抽样大概1秒

A.切片哈希值合并

B.首尾切片+其他切片前中后各取2M

计算hash:A->B(参考ReactFiber架构

A.线程:web-worker

开一个web worker线程即可,因为创建也有开销,无关多核单核处理器,都可以实现线程并行

而大文件传输的主要开销在网络请求,所以只用开启一个线程计算切片id就够用

B.空闲:requestIdleCallback

异步并发控制:A->B(参考http2的多路复用)

A.promise.allSettled()

B.并发数max=3


+async sendRequest(forms, max=4) {
+  return new Promise(resolve => {
+    const len = forms.length;
+    let idx = 0;
+    let counter = 0;
+    const start = async ()=> {
+      // 有请求,有通道
+      while (idx < len && max > 0) {
+        max--; // 占用通道
+        console.log(idx, "start");
+        const form = forms[idx].form;
+        const index = forms[idx].index;
+        idx++
+        request({
+          url: '/upload',
+          data: form,
+          onProgress: this.createProgresshandler(this.chunks[index]),
+          requestList: this.requestList
+        }).then(() => {
+          max++; // 释放通道
+          counter++;
+          if (counter === len) {
+            resolve();
+          } else {
+            start();
+          }
+        });
+      }
+    }
+    start();
+  });
+}

async uploadChunks(uploadedList = []) {
  // 这里一起上传,碰见大文件就是灾难
  // 没被hash计算打到,被一次性的tcp链接把浏览器稿挂了
  // 异步并发控制策略,我记得这个也是头条一个面试题
  // 比如并发量控制成4
  const list = this.chunks
    .filter(chunk => uploadedList.indexOf(chunk.hash) == -1)
    .map(({ chunk, hash, index }, i) => {
      const form = new FormData();
      form.append("chunk", chunk);
      form.append("hash", hash);
      form.append("filename", this.container.file.name);
      form.append("fileHash", this.container.hash);
      return { form, index };
    })
-     .map(({ form, index }) =>
-       request({
-           url: "/upload",
-         data: form,
-         onProgress: this.createProgresshandler(this.chunks[index]),
-         requestList: this.requestList
-       })
-     );
-   // 直接全量并发
-   await Promise.all(list);
     // 控制并发  
+   const ret =  await this.sendRequest(list,4)

  if (uploadedList.length + list.length === this.chunks.length) {
    // 上传和已经存在之和 等于全部的再合并
    await this.mergeRequest();
  }
},

a.同源并发连接数限制

不同的浏览器在不同的协议、不同操作系统、不同网络情况上限制数不同、通常来说是6-8个

不同的协议:HTTP/1或 HTTP/2(多路复用(Multiplexing)的特性,允许在单个连接上并行发送多个请求)

不同的浏览器:谷歌、火狐、自带浏览器

b.分布式并发请求:不同域名/不同服务器

将应用部署到多个服务器、使用负载均衡器,或者通过分布式架构实现的

域名分片:分散到多个子域名

每个子域名都需要建立连接,可能会增加DNS查找连接建立的开销。

CDN(内容分发网络):分散到服务器

重传:参考SR选择重传协议

A.后端响应:失败次数

async sendRequest(urls, max=4) {
-      return new Promise(resolve => {
+      return new Promise((resolve,reject) => {
         const len = urls.length;
         let idx = 0;
         let counter = 0;
+        const retryArr = []

 
         const start = async ()=> {
           // 有请求,有通道
-          while (idx < len && max > 0) {
+          while (counter < len && max > 0) {
             max--; // 占用通道
             console.log(idx, "start");
-            const form = urls[idx].form;
-            const index = urls[idx].index;
-            idx++
+            // 任务不能仅仅累加获取,而是要根据状态
+            // wait和error的可以发出请求 方便重试
+            const i = urls.findIndex(v=>v.status==Status.wait || v.status==Status.error )// 等待或者error
+            urls[i].status = Status.uploading
+            const form = urls[i].form;
+            const index = urls[i].index;
+            if(typeof retryArr[index]=='number'){
+              console.log(index,'开始重试')
+            }
             request({
               url: '/upload',
               data: form,
               onProgress: this.createProgresshandler(this.chunks[index]),
               requestList: this.requestList
             }).then(() => {
+               urls[i].status = Status.done
               max++; // 释放通道
               counter++;
+              urls[counter].done=true
               if (counter === len) {
                 resolve();
               } else {
                 start();
               }
-            });
+            }).catch(()=>{
+               urls[i].status = Status.error
+               if(typeof retryArr[index]!=='number'){
+                  retryArr[index] = 0
+               }
+              // 次数累加
+              retryArr[index]++
+              // 一个请求报错3次的
+              if(retryArr[index]>=2){
+                return reject()
+              }
+              console.log(index, retryArr[index],'次报错')
+              // 3次报错以内的 重启
+              this.chunks[index].progress = -1 // 报错的进度条
+              max++; // 释放当前占用的通道,但是counter不累加
+              
+              start()
+            })
           }
         }
         start();

}

B.超时无响应:指数加权移动平均公式

EWMA公式的特点是最新的观测值被加权得到更高的权重,前面的观测值权重以指数级衰减。因此,EWMA公式对变化率较大的数据可以更快地做出反应,可以用于平滑时间序列数据。
指数加权移动平均(Exponentially Weighted Moving Average),他是一种常用的序列处理方式。

在 t时刻,他的移动平均值公式是:

为真实值

// 设置一个超时 Promise
const timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Request timed out");
    }, 3000); // 这里设置一个较短的超时时间为 3 秒
});

// 使用 Promise.race() 来执行超时控制
Promise.race([backendPromise, timeoutPromise])
    .then((response) => {
        // 如果后端请求成功响应,在这里处理响应
        console.log("Backend response:", response);
    })
    .catch((error) => {
        // 如果超时或者后端请求失败,在这里处理错误
        console.error("Error:", error);
    });

动态:一轮并发请求中切片大小固定

并发数(TCP协议的快恢复)

1.设定阙值,从阙值开始并发

2.每次+1

3.遇到失败/重传,阙值减少到当前次数的一半,下次从阙值开始并发

切片大小:file.slice(startByte, endByte)

1.计算切片平均速度与上一次速度/最低速度的比率rate

2.下轮切片大小=min[切片大小*=rate,网速/RRT]

async handleUpload1(){
      // @todo数据缩放的比率 可以更平缓  
      // @todo 并发+慢启动

      // 慢启动上传逻辑 
      const file = this.container.file
      if (!file) return;
      this.status = Status.uploading;
      const fileSize = file.size
      let offset = 1024*1024 
      let cur = 0 
      let count =0
      this.container.hash = await this.calculateHashSample();

      while(cur<fileSize){
        // 切割offfset大小
        const chunk = file.slice(cur, cur+offset)
        cur+=offset
        const chunkName = this.container.hash + "-" + count;
        const form = new FormData();
        form.append("chunk", chunk);
        form.append("hash", chunkName);
        form.append("filename", file.name);
        form.append("fileHash", this.container.hash);
        form.append("size", chunk.size);

        let start = new Date().getTime()
        await request({ url: '/upload',data: form })
        const now = new Date().getTime()
        
        const time = ((now -start)/1000).toFixed(4)
        let rate = time/30
        // 速率有最大2和最小0.5
        if(rate<0.5) rate=0.5
        if(rate>2) rate=2
        // 新的切片大小等比变化
        console.log(`切片${count}大小是${this.format(offset)},耗时${time}秒,是30秒的${rate}倍,修正大小为${this.format(offset/rate)}`)
        // 动态调整offset
        offset = parseInt(offset/rate)
        // if(time)
        count++
      }
    }

文件碎片清理

可以考虑定期清理,当然 ,我们可以使用node-schedule来管理定时任务 比如我们每天扫一次target,如果文件的修改时间是一个月以前了,就直接删除

// 为了方便测试,我改成每5秒扫一次, 过期1钟的删除做演示
const fse = require('fs-extra')
const path = require('path')
const schedule = require('node-schedule')


// 空目录删除
function remove(file,stats){
    const now = new Date().getTime()
    const offset = now - stats.ctimeMs 
    if(offset>1000*60){
        // 大于60秒的碎片
        console.log(file,'过期了,浪费空间的玩意,删除')
        fse.unlinkSync(file)
    }
}

async function scan(dir,callback){
    const files = fse.readdirSync(dir)
    files.forEach(filename=>{
        const fileDir = path.resolve(dir,filename)
        const stats = fse.statSync(fileDir)
        if(stats.isDirectory()){
            return scan(fileDir,remove)
        }
        if(callback){
            callback(fileDir,stats)
        }
    })
}
// *    *    *    *    *    *
// ┬    ┬    ┬    ┬    ┬    ┬
// │    │    │    │    │    │
// │    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
// │    │    │    │    └───── month (1 - 12)
// │    │    │    └────────── day of month (1 - 31)
// │    │    └─────────────── hour (0 - 23)
// │    └──────────────────── minute (0 - 59)
// └───────────────────────── second (0 - 59, OPTIONAL)
let start = function(UPLOAD_DIR){
    // 每5秒
    schedule.scheduleJob("*/5 * * * * *",function(){
        console.log('开始扫描')
        scan(UPLOAD_DIR)
    })
}
exports.start = start

预/校验请求

上传请求参数:大文件标识,文件大小

响应:是否上传过+断点数组

  • 等于0表示没有上传过,直接上传
  • 等于1曾经上传过,不需要再上传了(或:障眼法文件秒传递)
  • 等于2表示曾经上传过一部分,现在要继续上传

传输请求

上传请求参数:切片内容和大文件标识、文件名、是否传输完成

固定切片大小:切片数,切片索引

动态调整大小:切片起始字节-结束字节

传输完成:最后一个并发请求数组,且并发请求数恢复成原值

响应

上传:每个切片上传后会返回是否成功,根据总切片数和已上传成功数更新进度条

下载:返回206状态码,报告进度

合并:原始为文件大小的null

由发送方通知传输完毕后(包括超时失败),且切片缺失时,会将缺失的切片内容替换为null,就像在视频中呈现出缺帧,文字中出现一个null,接收方进行合并

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

字节跳动面试官:请你实现一个大文件上传和断点续传 - 掘金

字节跳动面试官,我也实现了大文件上传和断点续传 - 掘金

Web Uploader

中等文件:10MB为单位

正反向代理

正向代理是代理用户客户端,为客户端发送请求,对服务器隐藏真实客户端

反向代理以代理服务器来接收客户端的请求,然后将请求转发给内部网络上的服务器,将从服务器上得到的结果返回给客户端。

正向代理主要是用来解决访问限制问题;

反向代理则是提供负载均衡、安全防护等作用。

同区机房的响应快,

在客户端网络质量很好的情况下,比如同机房内,这时关闭反向代理缓冲直接将响应实时转发给客户端效率更高。

代理缓冲

合适的网速将响应传递给客户端。

解决了server端连接过多的问题,也保证了能持续稳定的向客户端传递响应

性能优化: 缓冲区大小的合理设置可以减少对后端服务器的频繁请求,从而降低延迟。通过在代理服务器上缓存一些常用或重复请求的响应,可以加速对这些资源的访问。

代理服务器放行

过大的缓冲区可能导致内存占用过高,在传输完成后释放多余的内存

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就失效了,所以推荐后者

传输状态码

响应头Content-Range+206状态码

Content-Range: bytes <start>-<end>/<total>

206状态码,表示服务器成功处理了部分请求,并返回了相应的数据范围。

很多平台的游戏文件,比如 Xbox、PlayStation 下载超过 100 G 的文件的时候,也是这样实现的。

上传

前端

<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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值