- 前言:大文件上传的普遍方案基本都是分片上传,如果把文件上传看做是一个不可分割的事务,那么分片的目标就是把一个大事务划分为一个个小事务;
1. BFF层(Backend for Frontend)
BFF层(Backend for Frontend):后端为前端服务的层,我们通常把他叫做中间层,凡是上了一定规模的公司,基本上都会上中间层。因为他的业务已经足够复杂,他的数据规则出现了巨大的鸿沟。正在读文章的你,一定少不了跟后端吵架,前端想要的数据是这种格式,但是后端给你的数据是另外一种格式,你问他能不能改一下,后端说改不了,他的数据就是这种格式,你爱用不用。或者你要的数据要调好几个接口,才能请求完整,你问他能不能合在一起给你,后端继续说给不了。是因为后端懒,不愿意给你整吗?不排除这种情况,其实根本原因在于,后端的数据规格跟结构,跟前端就是不一样的。
在很多中小型企业里面,后端的数据格式来自于它的存储格式,数据库里面怎么存的,数据结构就是怎么样子,反映的是
数据的存储;而前端的数据格式,反映的是UI的渲染。这两种数据格式天生就不一致,如果是大型企业,反映的是业务格式,比如上了一些DDD开发模型。有了BFF层后,前端很清楚自己需要什么样的格式,因此就可以去后端服务器拿业务数据,比如要请求多次,在BFF层组装完成后,返回给前端最合适的数据格式。当然这只是中间层的一个左右,它还有很多作用。
2.如何减少页面阻塞?
分片上传的第一个首要目标就是尽量避免相同的分片重复上传。服务器必须要能够识别来自各个客户端的上传请求。
服务器如何识别哪些分片是相同的?
首先需要对相同的下一个准确的定义:
文件内容一样即为相同。对文件内容进行二进制对比是一个非常耗时的操作,于是可以选择基于内容的
hash来进行对比。hash算法包括
MD5、SHA-1,可以将文艺长度的数据转换为一个定长的数据,有个第三方组件库Spark-MD5
客户端需要做两件事:
- 1.对文件进行分片,并计算每个分片的hash值;
- 2.根据所有分片的hash值,计算整个文件的hash值;
而计算hash值是一件
CPU密集型的操作,如果不加处理,将会导致长时间阻塞主线程;
为了解决这个问题,理想状态下,每次上传的大文件都是最新的,这样就无需等待整体hash的计算结果,直接上传分片就行,同时可以把分片操作使用多线程+异步的方式进行处理。
第一次上传有了前几个分片之后,中断后继续上传,发现上一次上传的几个分片已经存在,直接快进到第4个分片,不管中断多少次,直到上传完成为止;
这样做的好处是:页面完全无阻塞,也无需等待整体haah即可启动上传
- 对于新文件上传,可以缩短整体上传时间,消除页面阻塞;
- 对于旧文件上传,可能会产生一些无效的请求,但这些请求仅传递的是hash,并不真实上传文件数据,所以对网络和服务器影响很小。
3.前后端如何协调?
3.1.创建文件协议
当客户端发送分片到服务端的时候,需要告知服务器分片属于哪一次文件上传,因此需要一个唯一标识来表示某一次文件上传。
如何获取文件上传的标识?
uploadToken: 文件上传的唯一标识chunSize: 分片大小(字节 )
3.2.haah校验
客户端需要校验单个分片或整个文件的hash,服务器需要告知客户端目前具体情况。
Upload-Hash-Type: 取值chunk或者file, 分别表示分片 hash和文件整体hash。Upload-Hash: 分片或文件的具体hash值。hahFile: 指示服务器是否已经存储了对应的分片或文件。res: 当校验文件hash时,指示该文件剩余那些没有上传。
3.3.分片数据上传
分片数据通过二进制上传到服务器,服务把分片保存起来;
3.4.分片合并
所有分片全部上传后,通过该协议请求服务器完成合并。
Merge:浏览器告诉服务器,已经上传完了,可以合并了。url: 如果该文件已经完成上传,返回文件所在地址。
4.如何保证分片不重复?
这里的重复指的是:不保存、不上传重复分片。这就必须要求分片
跨文件唯一,并且 永不删除
也就是说:服务器并不保存合并之后的文件,仅记录文件中的分片顺序
4.1 合并分片到底做了什么?
合并会造成很多问题,最主要的是:极其耗时、数据冗余,所以服务器并不发生真正的合并,而是在数据库中记录文件中包含的分片。因此,合并操作时,服务器仅做简单的处理:
- 1.校验文件大小
- 2.校验文件hash
- 3.标记文件状态
- 4.生成文件访问地址
4.2 访问文件怎么办?
由于服务器并未发生真正的文件合并,当后续请求该文件时,服务器需要动态处理,具体做法:
- 1.服务器收到对文件的请求,并在数据库中找到对应的文件。
- 2.服务器读取文件的所有分片ID,依次找到对应的分片文件。
- 3.服务器利用TaskQueue的并发控制能力,逐步产生文件读取流,并利用管道直接输出到网络I/O