# multipart-md5 **Repository Path**: starcn/multipart-md5 ## Basic Information - **Project Name**: multipart-md5 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2023-06-13 - **Last Updated**: 2023-06-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 大文件Md5实现以及优化 #### 前言 之前在自己项目中做了一个大文件分片上传的功能,满心欢喜的写道自己的简历中。自己当时只实现了: * 大文件通过 `File.prototype.slice`做分片,循环调用接口上传 * 断点续传也只做到了接口报错,重新调用接口续传 * 大文件MD5只实现到了直接调用一个库`browser-md5-file.js` 后面出去面试,面试官提的几个问题都无法回答 * 大文件MD5,页面假死情况怎么处理 * 后台传输怎么处理 后续自己通过查阅整理,通过如下方案实现了大文件MD5的功能。 > 如文章有错误的地方,感谢您的指正 > 也欢迎您分享更好的实现方式 #### 介绍 主要使用worker线程进行大文件md5加密的优化 #### 实现方式 直接引入 vue / element-ui 源码,与相关md5的库 #### 项目启动方式 * 全局安装一个 http-server `npm install --global http-server` * 在根文件夹下,启动一个服务器 `http-server` #### 源码 https://gitee.com/m_zyx/multipart-md5 #### 主要目录 介绍几个主要目录下的的内部功能介绍: * md5-test 对于文件 File / Blob 类型的数据进行不同md5库的加密方式进行验证, 我这里使用`Hash_1.0.4.exe`程序获取一张图片的md5值为`93d16b5ee12efa6af044b5ccaf02cec6` 现验证主要包括以下方式: |文件读取方式| md5库 | md5库算法 | 计算结果 | 是否正确 | |:---------|:---- |:-----------| :------ | :-----: | | readAsBinaryString | spark-md5 | SparkMD5.hash | 419fd4afc295e632efbce61ca49c8f7e | false | | readAsBinaryString | spark-md5 | SparkMD5.append & SparkMD5.end | AŸÔ¯Â•æ2ï¼æ¤œ~ | false | | readAsArrayBuffer | spark-md5 | SparkMD5.hash | 93d16b5ee12efa6af044b5ccaf02cec6 | true | | readAsArrayBuffer | spark-md5 | SparkMD5.ArrayBuffer & append & end | 93d16b5ee12efa6af044b5ccaf02cec6 | true | | readAsDataURL | spark-md5 | SparkMD5.hash | cf4f4de6ada92285b12ef1094a70af81 | false | | readAsArrayBuffer | md5-js | md5 | 93d16b5ee12efa6af044b5ccaf02cec6 | true | | readAsBinaryString | md5-js | md5 | 419fd4afc295e632efbce61ca49c8f7e | false | > 结论: > * 无论是使用 `spark-md5` 还是使用 `md5-js` 库的时候,只要是读取方式是采用 `readAsArrayBuffer` 是正确的 > * `spark-md5`无论是直接使用 `hash` 方式 还是使用`append`拼接方式的时候都是能够正常计算出来 * web-workers 大文件下的md5计算的方式(请优先准备一个超过2GB的文件): * ``index.js`` * ``testFileReaderLimitAndSingleThread`` 该方法测试项目: * 当读取的文件大小`size >= 2 GB` 的时候,`FileReader`直接监听到了`error`方法 > 该方法只是用过chrome浏览器实验得出来的,经过查阅并没有找到对于`FileReader`的大小限制的说明。所以暂不明确这是否是浏览器的标准或者不同浏览器下有不同的限制 * 测试js单线程,并且事件是以队列形式进行的,具体现象如下: 1. 在计算文件md5的过程中,点击页面内的 【显示弹框】 按钮的时候,页面不会展示出来``this.$message.success(`点击了${++this.clickCount}次`)``的弹框 > 此时由于计算md5速率比较慢,并且由于`js`是单线程,所以会导致"页面假死"的现象 2. 如果多次点击的情况下计算完成后,会【一次性】全部弹出刚才的弹框,并且按照点击顺序展示 > 展示出来`js`的事件队列的形式 * ``testFileReaderWorker`` 该方法的测试项目为: * 分片的基础方法: `slice` * 开启`web worker`线程,具体现象如下: 1. 后台计算md5,点击页面内的 【显示弹框】 按钮的时候,页面会展示出来``this.$message.success(`点击了${++this.clickCount}次`)``的弹框 > 此时由于计算md5速率比较慢的计算放到`worker`的线程中,所以页面内的线程不会被阻塞 * ``testFileMultipleUpload`` 该方法的测试项目为: * 将超过2GB的文件计算md5的完整计算放到`worker`线程中 * ``worker.js`` * ``web worker``的几个说明: * ``web worker``引入文件使用 ``importScripts`` 这里的路径是使用的相对路径 * 全局对象使用`self`来获取 * 使用``web wokder``线程注意以下几个注意点 1. **同源限制** 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。 2. **DOM 限制** Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。 3. **通信联系** Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。 4. **脚本限制** Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。 5. **文件限制** Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。 * ``multipleHash`` 计算的主要思路: * 使用`file.slice`方法获取切片`Blob` * 将切片`Blob`使用`FileReader.prototype.readAsArrayBuffer`来获取到切片的`ArrayBuffer` * 将切片后的`ArrayBuffer`使用 `SparkMD5.ArrayBuffer & append & end` 方式来计算`md5` * 计算完成后将`md5`值通过`self.postMessage(md5)`的方式传递给`主线程` 过程中发现的问题: * 直接`while`循环将`file.slice(startIdx, endIdx)`的结果读取后使用`readAsArrayBuffer`完成后通过`load`的回调来完成。由于是异步的,所以无法保证``SparkMD5.ArrayBuffer.prototype.append``是按照正确的顺序来完成插入 * ``SparkMD5.ArrayBuffer.prototype.append``是异步的所以调用``SparkMD5.ArrayBuffer.prototype.end``的时间节点无法获取到, 导致`self.postMessage(md5)`的方式传递给`主线程`的时间节点无法确定 解决方案及代码实现: * 实现一个``Md5Queue``队列来收集切片(``Md5Queue.prototype.push``) * 实现一个``Md5Queue.prototype.calcMd5``来递归调用``SparkMD5.ArrayBuffer.prototype.append`` 最后切片取完之后调用``SparkMD5.ArrayBuffer.prototype.end``,获取到``md5``值 * 使用一个订阅-发布模式``evnnt``来做通知``主线程``md5值计算完成 * ``utils/base.js`` * ``Md5Queue`` 使用队列的方式来完成大文件分片方式计算md5 * ``Event`` 模仿使用vue的事件中心源码来实现订阅-发布者模式 * ``FileReaderSecondry`` 二次封装出来一个``FileReader``的类 * ``TimeTemp`` 封装出来一个记录时间戳的单例,方便展示md5整个过程的时长 #### 几个疑惑等待后续调研 1. 现在使用的是直接引入html/js/css的方式来做代码实现,并没有使用``vue-cli``的形式来做。 那么在使用``vue-cli``的形式下,``web worker``的注册以及注册后的 ``postMessage`` 要怎么通知? 2. 如果使用``vue-cli``的形势下,到了具体页面每次都需要重新注册一次``web worker``的线程吗? 3. 如果一个浏览器有多个相同的标签页,这个标签页都有注册一次``web worker``的代码,那么他是只注册一次,还是注册多次? 注册多次那么由于核心数的限制,会不会出问题? 4. ``navigator.hardwareConcurrency``可以获取到当前计算机的cpu核心数, 那么``Worker``类可以拿到当前已开启的线程数量吗?可以通过这个来做开启线程的判断 参考文章: https://segmentfault.com/a/1190000014938305 https://segmentfault.com/a/1190000017578650?utm_source=sf-similar-article https://juejin.cn/post/6844904046436843527