HTML5文件上传组件的深度剖析

768 查看

前段时间在w3ctech技术交流会中分享了基于 HTML5 技术的文件上传组件,由于ppt携带的信息非常有限,故在此整理成文章分享出来,供感兴趣的同学阅读。

HTML VS FLASH

对于文件上传,相信还有不少同学还停留在FLASH时代,其实现在 HTML5 不仅可以实现文件上传,而且可以做得更好。

以下是对 HTML5 与 FLASH 就文件上传方面的功能调研测试得出的结果。

  功能描述              FLASH   HTML5
  --------------------- ------- ------------
  文件多选              ✓       ✓
  格式过滤              ✓       ✓
  拖拽(文件 & 文件夹)   ✗       ✓
  截屏粘贴              ✗       ✓
  Cookie & Session      ✗       ✓
  文件内容读取          ✓       ✓ 快150%
  图片预览&裁剪         ✓       ✓ 快200%
  文件上传              ✓       ✓ 快10%
  进度跟踪              ✓       ✓ 更加精准

PS: 截屏粘贴是指,如果剪切板里面存在图片数据,是可以通过 CTRL + V 将此图片作为文件添加到文件上传组件中的。让剪切板中有图片数据有很多方式:截屏软件(如QQ截屏),浏览器中右击图片点击复制,QQ聊天软件中复制图片...

可以看出,采用 HTML5 技术与传统的 FLASH 技术相比,能实现的功能更多且性能优势特别明显。

更多调研细节请移步到这里

虽然 HTML5 优势非常明显,但是如果目前支持 HTML5 的浏览器占比情况不理想,采用 HTML5 技术的文件上传还是不能带来足够的收益!

让我们先来看看由 TNW
提供的2014年3月份全球浏览器占比图。

通过浏览器占比可以推算出,目前支持 HTML5 的浏览器占比高达64.5%,加上 HTML5 有如此大的优势,看来完全没有理由拒绝采用 HTML5 来实现文件上传了。

但是还有35%的浏览器不支持 HTML5,怎么办?

为了考虑最大的兼容性,目前WebUploader同时实现了 HTML5 和 FLASH 两套运行时,在不支持 HTML5 的浏览器里面自动切换成 FLASH 方式上传,这样的好处是,既能在条件允许的情况保证 HTML5 发挥出其高效的优势,又能在不支持 HTML5 的浏览器里面保证能正常运行。

如何优化文件上传性能?

对于文件上传性能优化,可以从两个方面来着手,即上传前的优化和上传过程中的优化。

上传前的优化

主要有两个思路。

  • 思路一:通过减少文件体积,减少上传流量来优化。
  • 思路二:通过合并小文件,减少请求数来优化。

基于这两个思路,我们尝试了以下几种方案。

  • 图片压缩

    如果上传的是图片类文件,存在一部分用户喜欢直接选择相机或者手机拍摄的原始文件进行上传,对于这类图片,图片的分辨极高,高达5000多。这就注定了此图片的文件体积不会太小。其实如果只是在电脑上查看,1千多的分辨率就已经足够。于是我们尝试通过js将此类图片进行缩小,得出的结果是:1张(5184×3456)大小为5M的jpg图片,缩小到(1600x1600)后,体积变成了407kb, 直接节省了4.5M的流量。

  • ZIP 合并小文件

    类似于拷贝文件夹到U盘,如果小文件比较多,拷贝过程非常慢,通常我们的做法是将文件夹打包成一个文件再拷贝,速度往往要快出许多。其实文件上传也一样,如果能把体积比较小的文件合并成一个文件,请求数就会少了很多,这样是不是就提高了整体文件上传速度呢?

    但是,通过测试得出的结果是不尽如意的。ZIP的运算效率太低,以至于只有在2G网络下才有速度提升,3G和wifi网络下反而变慢了。更多细节请移步到这里

  • SPRITE 合并小图片

    类似于css sprite, 将多个小图片画到一张大图片上,通过这种方式来进行合并,思路和zip合并差不多,但是采用的技术不太一样,此方案是直接用canvas来画,经过测试发现速度比zip快出来10倍,总体能带来20%左右的速度提升。

    但是此方案只满足于图片类文件,且服务端需要通过位置信息还原出小文件,带来一定的服务端开发成本,目前并没有采用此方案。更多细节请移步到这里

  • 直接合并文件内容

    其实,并不需要采用ZIP或者SPRIT方式合并文件,把文件读取出 arraybuffer 后是直接可以连接在一起的,之后还可以再次转成 blob 发送到服务端,或者直接发送 arraybuffer,理论上性能应该比SPRITE方案靠谱。

    但是这块没有进行详细的调研,在此就不多说了。

上传过程中优化

主要采用并发与分块,以下将细说这两个方案。

并发上传

采用此方案主要是源于单一请求无法让网络达到饱和,于是我们来尝试采用并发方式看能否提高总体文件上传速度。

以下是通过测试20个1M的文件在不同的并发数下得到的总体上传时间对比图。

很明显,并发数越多速度越快!

但是,并发数越多,给服务端带来的压力也越大,如何去选择一个合适的并发数呢?

主要可以从三方面去考虑。

  1. 并发数越多,服务端压力越大,所以选择并发数不能太大!
  2. 同时每个浏览器都有固定的最大并发数限制,所以选择并发数不能超出这个值。
  3. 从上面的图标可以看出,并发数到了3以后,收益开始渐渐不明显。

如是,最佳的并发数应该是3。

PS: 以下是常用浏览器的最大并发数信息。根据这个表可以说明为什么前面的测试结果,并发数只测试到了6,原因是chrome的最大并发数是6,当并发设置到7的时候,第7个请求是处于等待状态,直到前面某个请求完成,才会开始有进度。

  浏览器        HTTP 1.1   HTTP 1.0
  ------------- ---------- ----------
  IE 6, 7       2          4
  IE 8          6          6
  Firefox2      2          8
  Firefox3      6          6
  Safari 3, 4   4          4
  Chrome 1, 2   6          ?
  Chrome 3      4          4
  Chrome 4+     6          ?

为什么并发会更快?

这里列出了我个人觉得可能的原因。

  • 多请求能抢占更多的带宽。
  • 服务器端可能针对单一请求限速。
  • 跨域时,并发可以共用options验证请求,浏览器有个特点是,对于跨域请求,如果在一定的时间内,有多个请求,只会发送一个options请求验证的。

左边是非并发的情况,右边为采用并发的情况。可以看出,当不采用并发的时候,每个文件上传请求前都会进行options请求验证,而并发的时候,三个文件上传请求共用了一个opions请求。

分块上传

为什么要采用分块上传?

试想一下,如果上传的文件是一个大文件,本来上传时间就相对较久,再加上网络不稳定各种因素影响,容易导致传输中断,用户除了重传整个文件外没有更好的选择。采用分片上传可以很好地解决这个问题。

什么是分片上传?

分块上传,就是把一个大的文件分成若干块,一块一块的传输。如上面的case, 如果传输中断,仅需重传出错的分片,而不需要重传整个文件,大大减少了重传开销。

那么,采用分块上传还有哪些优势?

  • 更强容错能力

    如以上的case, 出错重传,仅需重传一小块。

  • 可以模拟暂停与继续

    对于一个http请求,其实并没有暂停功能,要不就是已完成,要不就是已中断。如果不分块,是没法做暂停功能。但是如果采用分块上传,在某个分块上传完了后不自动开始下个分块上传,是不是就实现了暂停功能?

    利用此功能,就可以通过网络检测,在网络断开的时候自动暂停传输,网络恢复后,继续传输,给用户带来更好的体验效果。

  • 利用并发提速

    如果单独的上传一个大文件,只有采用了分块上传才能利用并发请求来提速。

  • 更精准的速度跟踪

    关于客户端监控上传进度,其实监控的都是客户端的发送速度,服务端有没有接收,有没有存储,是不知道的,只有在整个请求完毕,收到服务端反馈后才能确定数据已经成功接收。这样也就解释了进度显示的时候,长长出现,进度条瞬间到100%,要过一段时间才全部完成。如果采用分块上传,每个分块上传完成,可以确定这个分块服务端已经成功接收。

当然,分块也会有一定的副作用,本来是一个请求,分块后变成了多个请求,自然会带来网络开销。那么具体是个什么的情况呢?

以下是通过测试3个30M的文件在3个并发下调整不同的分片大小得出的总体时间消耗表。

可以看出来,分块越小,时间消耗越大,尤其是分块大小小到256K的时候,时间花费增长特别明显。

那么,如何选择一个合适的分块大小?

主要有三个方面的考虑。

  • 分块越小,请求越多,开销越大。所以不能设置得太小。
  • 分块越大,灵活度越小,前面所说的那些优势就会相对越不明显。故不能太小。
  • 服务端一般都会有个固定大小的接受buffer(clientbodybuffer_size), 分块的大小最好是这个值的整数倍。

综合这些考虑,推荐的分块大小是2M-5M,具体size根据产品中文件上传的大小分布来定。如果上传的文件大部分是500M以上,很大的文件,建议是5M, 如果相对较小,推荐2M。

断点续传

有了分块上传,其实我们可以实现更多的功能。试想,如果服务端能够把每个已成功上传的分块都记住,客户端可以快速验证,条件选择跳过某个分块,是不是就实现了断点续传?

那么,断点续传能带来哪些好处?

  1. 节省流量,避免上传重复的分块。
  2. 减少用户等待时间。
  3. 可恢复的上传。出现中断,就算浏览器刷新或者是换了台电脑也能恢复到上次中断的位置。

那么现在最关键的问题是如何标识一个分块了。怎样标识让服务端好入库,同时客户端可以快速的计算出来与服务端验证,换句话说就是,如何去找出分块的唯一ID。

之前尝试过文件名+分块编号,或者再加文件大小,文件最后修改时间什么的,都无法保证分块的唯一性。于是还是直接采用 MD5 的方式来序列化文件内容吧,这样就算是文件不同名,只要内容是一致的也会正确地识别出是同一个分块。

那么现在的逻辑就是,每次分块上传前,根据内容 MD5 序列化,得到一个唯一ID,与服务端验证,如果此分块已经存在于服务端,则直接跳过此分块上传,否则上传此分块,成功后,服务端记下此分块ID。

如是,分块上传是不是具有了秒传的功能?既然分块能秒传,那么整个文件是否也可以秒传?

秒传

分块能秒传,整个文件当然也能秒传,关键还是看 MD5 的性能。

通过以上测试结果可以看出,如果文件大小在 10M 以内,是可以真正的秒级内完成的,大于这个值时间花费就大于1秒了,比如一个200M的文件 MD5 时间花费需要13秒左右。

但是,即便如此,相比于文件传输时间花费,MD5 的时间花费根本就不算什么。对于类似于百度云,文库这类的产品,在上传一个文件的时候很可能服务端已经存在了此文件,如果多等个几秒钟,能跳过整个文件上传,我觉得是非常划算的。

另外,如果是一次上传多个文件是可以在算法上去优化这个过程的。

  • 验证过程提前到当前文件的传输期

    如果当前文件已经在传输了,这个时候,用户是处于等待状态,机器也处于等待期,如果把下一个文件的验证过程移至此过程,那么用户的等待 MD5 的时间和等待当前文件传输完成的时间就重合了。这样用户就只需要等待第一个文件的验证过程。

  • 小文件优先处理,减少用户等待时间

    因为第一个文件的验证等待无法避免,如果第一个文件处理的文件越小,是不是等待的时间就越短?所以把队列中最小的一个文件放到第一个优先处理可以进一步减少用户等待时间

  • 更换序列化算法,取段MD5

    其实对于某些二进制文件,如JPEG,前面一段数据记录了很多此图片的信息,比如:拍摄时间,相机名称,图片尺寸,图片旋转度等等,直接 MD5 这一段数据基本上就可以保证此文件的唯一性了。只要取段的总大小小于10M,再大的文件也能在1秒内完成序列号工作。

参考资料


by 2betop
via 百度 FEX