返回文章列表

大文件上传


方案

普遍性方案是文件分片,把大文件打散成小事物,从而降低上传失败的风险。

细节

底层协议标准如何制定,协议标准决定了前后端如何交互,决定了前后端代码如何开发。

除了协议之外,还涉及前端如何控制并发,如何高效分片,设计后端如何存储分片,如何高效合并分片,如何保证分片唯一性等。

市面上没有统一解决方案,虽然有公有云上的oss,但产品可能会部署到私有云,所以还是要学会自行实现。

方案

客户端分片,计算分片哈希和完整哈希,再使用哈希和服务器换取当前文件信息。

计算哈希是cpu密集型操作,会导致长时间ui阻塞。

虽然可以用web worker来加速,但经过我的测试,即便是使用了多线程,超大文件(10g以上)的阻塞仍然超过30s。这是不能接受的。

因此,我对上传流程做出优化,我假定大部分文件都是一个新文件,于是在流程上我允许用户在获得完整hash之前直接上传分片,这样一来几乎可以零延迟到上传,等到整体hash计算出来之后再向服务器补充hash数据。

通信协议设计

四个通信协议:

  1. 创建文件协议,前端用head请求,换取上传唯一token,后续请求必须携带该token

  2. 哈希校验,前端把某个分片hash,或者整个文件hash发送给服务器,得到分片和文件状态。

  3. 分片上传协议,前端将分片的二进制数据发送到服务器存储。

  4. 分片合并协议,前端提示服务器可以完成分片合并。

存储

因为涉及bff层,所以需要编写服务端代码。

最大的挑战是如何保证每个分片的唯一性。这种唯一性既包含了存储的唯一性,也包含传输的唯一性。存储的唯一性保证分片不会重复保存,避免数据的冗余。传输的唯一性保证分片不会重复上传,避免通信的冗余。

要保证分片不会重复保存,就必须让分片和文件解偶,没有从属关系。文件独立记录,按照顺序依次指向不同分片。

要保证分片不重复上传,就必须让分片永不删除。如果合并文件之后删除分片就会导致下次重复上传找不到对应分片,必须重复上传。

最后是合并分片逻辑,如果真把分片合并成一个大文件,大文件的数据实际上是冗余的,整个过程也极其耗时。所以我做了这样的一些处理。当服务器收到合并请求时,服务器只需要做一些简单校验(文件大小,分片数量)就可以了,只需要生成url即可。当用户下载文件时,使用文件流依次读取分片数据,用流管道直接响应给客户端即可。

合并和文件访问效率都很高,服务器也不会有冗余存储。

流程:

  1. 先告知服务器要传文件了,需要服务器返回唯一标识。

  2. 文件切片

const chunkSize = 5 * 1024 * 1024; // 5MB
const chunks = [];
let cur = 0;
while (cur < file.size) {
    chunks.push(file.slice(cur, cur + chunkSize));
    cur += chunkSize;
}
  1. 生成指纹。为了标识文件的唯一性(用于秒传和断点续传),需要计算文件的 MD5SHA-256

  2. 秒传校验。在正式上传前,先发一个请求给服务端,带上文件哈希值。

  • 场景 A: 服务端已存在该文件 -> 直接返回“上传成功”(秒传)。

  • 场景 B: 服务端已存在部分分片 -> 返回已收到的分片索引列表(断点续传)。

  • 场景 C: 服务+端不存在该文件 -> 开始完整上传。

  1. 并发上传。使用 FormData 包装每个分片,并通过 XMLHttpRequestFetch 发送。

  • 并发控制: 不要同时发起成百上千个请求。建议维护一个发送队列,将并发数控制在 3-6 个,以防占用过多浏览器资源导致页面卡顿。

  • 重试机制: 单个分片上传失败时,应支持自动重试(如重试 3 次)。

  1. 合并请求。当所有分片上传完成后,前端发送一个“合并”指令给服务端。服务端根据分片索引将文件块合并成原始文件,并校验合并后的哈希值是否一致。