方案:
普遍性方案是文件分片,把大文件打散成小事物,从而降低上传失败的风险。
实现涉及到诸多技术细节。
比如底层协议标准如何制定,协议标准决定了前后端如何交互,决定了前后端代码如何开发。
除了协议之外,还涉及前端如何控制并发,如何高效分片,设计后端如何存储分片,如何高效合并分片,如何保证分片唯一性等。
市面上没有统一解决方案,虽然有公有云上的oss,但我们的产品可能会部署到私有云,所以最稳妥办法还是自行实现。
实现:
客户端分片,计算分片哈希和完整哈希,再使用哈希和服务器换取当前文件信息。
计算哈希是cpu密集型操作,会导致长时间ui阻塞。虽然可以用web worker来加速,但经过我的测试,即便是使用了多线程,超大文件(10g以上)的阻塞仍然超过30s。这是不能接受的。
因此,我对上传流程做出优化,我假定大部分文件都是一个新文件,于是在流程上我允许用户在获得完整hash之前直接上传分片,这样一来几乎可以零延迟到上传,等到整体hash计算出来之后再向服务器补充hash数据。
四个通信协议:
因为涉及bff层,所以需要编写服务端代码。最大的挑战是如何保证每个分片的唯一性。这种唯一性既包含了存储的唯一性,也包含传输的唯一性。
存储的唯一性保证分片不会重复保存,避免数据的冗余。
传输的唯一性保证分片不会重复上传,避免通信的冗余。
要保证分片不会重复保存,就必须让分片和文件解偶,没有从属关系。文件独立记录,按照顺序依次指向不同分片。
要保证分片不重复上传,就必须让分片永不删除。如果合并文件之后删除分片就会导致下次重复上传找不到对应分片,必须重复上传。
最后是合并分片逻辑,如果真把分片合并成一个大文件,大文件的数据实际上是冗余的,整个过程也极其耗时。所以我做了这样的一些处理。当服务器收到合并请求时,服务器只需要做一些简单校验(文件大小,分片数量)就可以了,只需要生成url即可。当用户下载文件时,使用文件流依次读取分片数据,用流管道直接响应给客户端即可。
合并和文件访问效率都很高,服务器也不会有冗余存储。
先告知服务器要传文件了,需要服务器返回唯一标识。
const chunkSize = 5 * 1024 * 1024; // 5MB
const chunks = [];
let cur = 0;
while (cur < file.size) {
chunks.push(file.slice(cur, cur + chunkSize));
cur += chunkSize;
}
为了标识文件的唯一性(用于秒传和断点续传),需要计算文件的 MD5 或 SHA-256。
在正式上传前,先发一个请求给服务端,带上文件哈希值。
使用 FormData 包装每个分片,并通过 XMLHttpRequest 或 Fetch 发送。
当所有分片上传完成后,前端发送一个“合并”指令给服务端。
服务端根据分片索引将文件块合并成原始文件,并校验合并后的哈希值是否一致。
参与项目通用库开发
亮点:从0-1开发upload-sdk,该sdk为所有文件上传,特别是大文件上传场景提供前后端的支撑,统一了所有文件上传的开发方式。完成了从底层协议到工具类/前端组件/后端中间件的开发。
在实现层面,为保证使用的灵活性,利用多种设计模式完成了sdk和上层应用的完全解偶,并对服务器存储进行精细设计,保证了文件传输和存储的唯一性。