大文件Md5实现以及优化

8,567 阅读6分钟

大文件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

源码

gitee.com/m_zyx/multi…

主要目录

介绍几个主要目录下的的内部功能介绍:

  • md5-test
    对于文件 File / Blob 类型的数据进行不同md5库的加密方式进行验证,
    我这里使用Hash_1.0.4.exe程序获取一张图片的md5值为93d16b5ee12efa6af044b5ccaf02cec6
    现验证主要包括以下方式:

    文件读取方式md5库md5库算法计算结果是否正确
    readAsBinaryStringspark-md5SparkMD5.hash419fd4afc295e632efbce61ca49c8f7efalse
    readAsBinaryStringspark-md5SparkMD5.append & SparkMD5.endAŸÔ¯Â•æ2ï¼æ¤œ~false
    readAsArrayBufferspark-md5SparkMD5.hash93d16b5ee12efa6af044b5ccaf02cec6true
    readAsArrayBufferspark-md5SparkMD5.ArrayBuffer & append & end93d16b5ee12efa6af044b5ccaf02cec6true
    readAsDataURLspark-md5SparkMD5.hashcf4f4de6ada92285b12ef1094a70af81false
    readAsArrayBuffermd5-jsmd593d16b5ee12efa6af044b5ccaf02cec6true
    readAsBinaryStringmd5-jsmd5419fd4afc295e632efbce61ca49c8f7efalse

    结论:

    • 无论是使用 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是单线程,所以会导致"页面假死"的现象

          1. 如果多次点击的情况下计算完成后,会【一次性】全部弹出刚才的弹框,并且按照点击顺序展示

          展示出来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类可以拿到当前已开启的线程数量吗?可以通过这个来做开启线程的判断

参考文章:
segmentfault.com/a/119000001…
segmentfault.com/a/119000001…
juejin.cn/post/684490…