跳到主要内容
约 16 分钟8 张卡

文件上传

文件上传是把客户端文件传到服务端,并让它成为可识别、可授权、可使用的业务资源。小附件可以用一次请求完成;大文件需要考虑分块、断点续传,以及断网后怎样确认哪些操作已经成功。

上传进度到 100%,通常只说明某一段传输已经完成。完整文件是否通过校验、对象是否生成、附件记录是否提交,是后续几个不同的事实。解释上传方案时,要把这些事实和失败后的处理方式连起来。

1. 面试核心​

先记住这几条,再读正文
  • 文件可以经过业务服务转发,也可以由客户端直传存储。直传减少业务服务的数据转发,但不会取消业务授权和完成确认。卡 · 综合题
  • multipart/form-data 是一次请求体内组织字段和文件的格式,不自动提供跨请求的断点续传。HTTP 传输分块、应用分块和对象存储多段上传各有职责。卡 · multipart 表单
  • 分块续传需要一个持久的上传会话,记录文件约束和已确认分块。恢复时以服务端确认的状态为准,不能把客户端已发送的字节当作服务端已保存的字节。卡 · 综合题
  • 分块身份至少要关联上传会话和块编号或偏移。同一身份、相同内容可以重试;不同内容必须按明确规则处理,并用原子写入防止并发覆盖。卡 · 分块写入的幂等规则
  • 分块校验和完整文件校验解决不同问题。块正确不代表没有漏块、乱序或选错文件,客户端摘要也不代表内容可信。卡 · 分块摘要与完整文件摘要
  • 完成请求要冻结待完成的内容、核对清单、生成对象并保存结果。完成请求重试复用原上传身份和结果,不再次创建一份附件。卡 · 冻结内容与返回结果
  • 对象存储和业务数据库通常不共享一个本地事务。对象已经写成、业务记录尚未提交时,需要持久状态和结果核对来恢复,不能把超时直接判定为未写入。卡 · 对象与业务记录的一致性
  • 上传会话、分块、完成接口和下载入口都要校验资源归属。随机标识、文件摘要和客户端文件名都不能代替授权。卡 · 权限、资源限制与验证

2. 上传的数据路径​

上传方案首先要明确两条路径:文件字节经过哪里,业务状态由谁决定。业务服务负责用户身份、配额、附件归属等规则;文件内容可以由它转发,也可以直接送到存储服务。

方案文件字节经过哪里选择时要考虑什么
业务服务转发客户端 → 业务服务 → 存储便于在转发过程中检查内容;业务服务承担连接、带宽、缓冲和超时压力
客户端直传客户端 → 存储;业务服务另行创建会话和确认完成减少业务服务转发;需要限制上传凭据的对象范围,并核对存储结果
专用上传服务客户端 → 上传服务 → 存储可集中管理分块、限额和处理任务;需要维护上传状态与业务系统之间的关系

单机小项目把文件存到本地磁盘可以成立,但要说明重启、扩容、多实例访问和备份怎么办。如果多个实例都可能处理同一上传会话,就不能把唯一的分块状态放在某个实例的临时内存里。文件与对象接口的区别见对象存储。

以直传附件为例,业务服务先创建属于当前用户的上传会话,再给客户端指定对象或分段的上传权限。客户端上传后通知业务服务确认;业务服务根据可信的存储结果和业务约束决定附件是否可用。不能仅凭客户端发送一句“上传成功”就创建可下载记录。

文件传输失败时,先定位失败阶段。创建会话失败、单块响应丢失、最终对象生成失败和业务记录提交失败,恢复方式不同。HTTP 超时只说明本次等待结束,不能推出远端没有执行;通用语义见 HTTP。

3. multipart 表单​

multipart/form-data 把多个表单字段和文件放进同一个请求体,各部分由 boundary 分隔。每一部分有自己的描述头和内容,文件内容可以直接使用二进制字节,不必先转成 Base64。

下面是请求头与请求体的格式示意,省略了 HTTP 消息长度字段;实际行分隔使用 CRLF:

Content-Type: multipart/form-data; boundary=upload-demo

--upload-demo
Content-Disposition: form-data; name="title"

report
--upload-demo
Content-Disposition: form-data; name="file"; filename="report.txt"
Content-Type: text/plain

hello
--upload-demo--

name 标识表单字段,filename 是客户端提供的文件名,不能直接当作服务端文件路径。最后一条分隔线末尾的 -- 表示 multipart 内容结束。分隔符必须与请求头约定一致,也不能与各部分内容中的分隔线冲突。

同一个字段上传多个文件时,可以使用多个同名的 part。这里的 part 是一次请求中的 MIME 部分,不表示服务端已经提供了独立重试这些部分的接口。浏览器使用 FormData 时,由浏览器生成带 boundary 的 Content-Type,用法见 Fetch API。

几个包含“分块”的机制不能混在一起:

机制划分的对象是否自然产生可恢复的上传会话
multipart/form-data一次请求体中的字段与文件不会
HTTP/1.1 的 chunked 传输编码消息在连接上的传输块不会;连接内的分帧不是业务分块记录
应用分块上传文件按编号或偏移拆成多次请求取决于应用是否保存会话、进度和完成规则
S3 多段上传一个对象上传过程中的各段服务提供上传 ID、分段和完成 API;业务仍需检查预期文件和附件状态

给任意 PUT 加上 Content-Range,也不能假定服务器支持续传。RFC 9110 的 partial PUT 依赖双方约定;不了解服务接口就发送部分内容,可能被拒绝,甚至被当成完整替换。

4. 上传会话与断点续传​

4.1 会话需要保存什么​

上传会话是一次文件上传的持久记录。它把多次网络请求关联起来,并规定这些请求最终要组成哪一个文件。

下面采用“固定块大小、最后一块可以较短、块编号从 1 开始”的应用协议。它是一种设计示例,不是 HTTP 或所有对象存储统一要求。

信息作用
uploadId、所属用户或租户关联请求,并校验资源归属
预期总字节数、块大小、内容摘要算法及预期值固定文件约束,防止重试时悄悄换成另一个文件
目标对象键、必要的对象版本确定最终内容位置,恢复时沿用同一身份
块编号、实际长度、校验结果、存储位置或返回凭据记录哪些块已经确认保存
状态、过期时间、最终业务 ID控制是否允许继续写入,以及重复完成时返回什么

总大小为 10 字节、每块最多 4 字节时,预期分块是 1:4、2:4、3:2。第 3 块先到也可以接受,但不能因为已收到三个请求就认为完整:三个请求可能都在重传第 1 块。

创建会话本身也可能响应丢失。客户端可以为一次创建意图提供幂等键;相同用户、相同键和相同参数返回原会话,同键不同参数报冲突。这个约束应由持久唯一记录保护,原理见幂等键。

4.2 客户端怎样恢复​

断点续传先查询服务端确认的进度,再传缺失内容。客户端本地可以记住 uploadId,但发送完成事件和进度条不能作为权威记录:响应可能丢失,服务端也可能在落盘前中断。

编号分块协议可以返回已完成块的编号、长度和摘要。客户端与本地文件的分块计划核对后,只补缺失块。若用户选择了另一个文件,即使文件名一样,也不能直接沿用旧会话;应按已约定的文件身份重新核对。

另一种方式是按连续偏移追加。tus 1.0.0 的核心协议通过 HEAD 查询 Upload-Offset,再用 PATCH 提交后续字节;客户端偏移与服务端不一致时返回 409,不修改资源。它和可乱序提交的编号分块不同,并行上传需要相应扩展或其他明确协议。

分块大小也有代价:块越小,一次失败重传的内容越少,但请求数量和元数据开销越大;并发越高,对带宽、连接、内存和存储请求配额的压力越大。应根据文件大小和环境限制设置上限,不把“每块固定几 MB、并发固定几个”当作通用最优值。

5. 重复分块与内容校验​

5.1 分块写入的幂等规则​

同一个分块重传时,服务端必须能识别它。这里采用 (uploadId, partNumber) 作为唯一身份,并规定已确认分块不可被不同内容覆盖:

收到的请求处理方式
新编号,长度和摘要正确保存内容及分块记录,再确认成功
已有编号,内容相同返回已保存结果,不追加一份内容
已有编号,内容不同返回冲突,由客户端核对文件或创建新会话
编号超出范围、长度不符、摘要不符拒绝,不推进已确认进度

判断重复和保存结果必须防止并发竞争。两个请求都先查到“没有第 2 块”,然后分别写入不同内容,会让最终文件取决于谁最后覆盖。数据库唯一键、条件对象写入或等效原子机制,应与明确的冲突处理一起使用。

分块字节和元数据如果分别存储,还会出现“字节已写、分块记录没写”的间隙。可以用稳定的临时对象键和条件写入,再核对对象结果补齐记录;不能只解决数据库唯一键,却忽略对象已经被覆盖。S3 的同编号分段可以被后续上传替换,具体规则见对象存储,应用的不可变分块规则需要自己保障。

5.2 分块摘要与完整文件摘要​

分块摘要针对单块的实际字节,帮助发现损坏或冲突;完整文件摘要针对按约定顺序组成的全部字节,帮助核对最终文件。把若干分块摘要字符串拼接起来再求摘要,通常不是原文件摘要。

下面在顺序读取时,同时计算每块摘要和完整摘要。示例使用 Python 3.11 标准库:

import hashlib
import io


def scan_parts(stream, chunk_size):
if chunk_size <= 0:
raise ValueError("chunk_size must be positive")
whole = hashlib.sha256()
parts = []
number = 1
while True:
data = stream.read(chunk_size)
if not data:
break
whole.update(data)
parts.append((number, len(data), hashlib.sha256(data).hexdigest()))
number += 1
return parts, whole.hexdigest()


parts, digest = scan_parts(io.BytesIO(b"abcdefghij"), 4)
print([(number, size) for number, size, _ in parts])
print(digest == hashlib.sha256(b"abcdefghij").hexdigest())

输出为 [(1, 4), (2, 4), (3, 2)] 和 True。whole 接收的是原始字节流,顺序没有改变。这段代码演示摘要计算,不是完整上传客户端;编号分块若要求除末块外长度固定,应使用能读满指定长度的文件读取方式,或为可能短读的流补足缓冲。

客户端上传的摘要只是声明,可信的一方仍要对实际内容校验。最终至少检查预期分块集合、长度、顺序和完整文件;若使用存储服务提供的校验能力,要确认算法、校验范围以及服务是否真的执行了验证。不能把所有 ETag 都当作整文件 MD5。

完整性也不同于安全性。用户完全可以上传恶意内容,并为这些字节提供正确的 SHA-256。文件类型、内容处理和访问权限仍需独立校验。

6. 完成提交​

6.1 冻结内容与返回结果​

完成提交把一个仍可接收分块的会话推进为可使用的文件。完成接口不能只统计收到多少块,还要检查分块编号是否齐全、长度是否符合计划,以及最终内容是否符合约束。

一种可恢复的状态设计如下:

状态允许的主要动作保持的约束
OPEN查询、接收分块、申请完成或中止已确认分块遵循冲突规则
FINALIZING查询、核对外部结果、恢复完成固定待完成的清单,迟到分块不能再改变它
READY读取结果,重复完成返回同一业务 ID不再重新创建附件
ABORTED查询中止结果不再接受旧会话写入

冻结清单和分块写入必须使用同一套并发约束。如果完成线程刚核对完清单,另一个线程又替换了分块,生成的就可能不是刚才校验的文件。可以在数据库事务中条件转换状态,并让分块提交也检查该状态;外部存储中的内容还需使用不可变键、指定版本或对应的条件约束。

生成对象之后,再在本地事务中提交业务附件记录和 READY 状态。附件表以 uploadId 等稳定业务身份防重,重复完成返回已有的 fileId。采用异步处理时,可以返回 202 和状态查询入口,但不能把“接受处理”说成“文件已经可下载”。

6.2 超时后的再次完成​

完成响应丢失时,客户端查询原会话,或按接口约定重试同一完成请求。服务端根据持久状态决定下一步:

  • 已经 READY:返回原附件结果。
  • 仍在 FINALIZING:核对原目标对象和完成操作,继续恢复,或返回处理中。
  • 仍为 OPEN:重新检查完整性并竞争完成资格。
  • 已中止或过期且不允许恢复:拒绝旧请求,提示重新创建会话。

对象存储的完成接口也有自己的响应规则。例如 S3 CompleteMultipartUpload 可能在 HTTP 200 响应体中携带错误,应让 SDK 正确解析结果,不能只看状态码。服务按提交清单组合对象,也不天然知道业务预期完整文件有多大;预期长度和分段集合仍要由应用核对。

7. 对象与业务记录的一致性​

对象成功写入和附件记录成功提交,通常是两个独立操作。把它们写在同一个函数里,不会让它们自动共同提交或共同回滚。

沿用前面的状态设计,失败恢复要覆盖这些位置:

失败位置可能留下的事实恢复办法
分块响应丢失分块可能已保存查询原会话,或同身份、同内容重试
FINALIZING 已提交,对象尚未写成会话已冻结,但没有最终内容按原清单和目标键继续生成对象
对象写成,业务事务尚未提交有对象,没有可用附件记录核对对象身份、大小和校验结果,再补业务事务
业务事务提交后响应丢失附件已可用,客户端仍未知查询或重复完成,返回原业务 ID
长期未完成或用户中止临时块或孤立对象占用空间按状态、归属和引用关系清理,并核对清理结果

恢复时不能看到同名对象就直接认定成功。同一键可能指向错误内容,或者已被另一条写入覆盖。稳定的上传身份、不可变目标或版本,以及可信校验结果,要共同证明它正是本次上传的结果。条件写入冲突后应读取并核对,不能无条件覆盖来“修复”。

也不能仅凭一次不存在响应就认定所有先前写入都已经停止。若旧写入仍在执行,迟到结果可能稍后出现;需要依据服务契约、幂等身份及状态转换处理。服务契约与缓存的区别见对象存储。

后台恢复任务可以扫描长期停留在 FINALIZING 的会话。能够证明对象正确且业务条件仍有效时补齐记录;证据不完整或对象冲突时进入明确的待处理状态。清理任务也要与完成任务协调,先取得合法清理状态,再删除不再引用的内容,避免把正在完成的对象删掉。通用的核对与修复机制见分布式流程恢复。

8. 权限、资源限制与验证​

上传接口的每一步都要检查当前主体能否操作这个会话。知道 uploadId、对象键或摘要,不代表拥有该文件;创建会话时的鉴权也不能代替后续每次请求的授权。直传凭据应限制到必要的对象、动作和有效期,预签名链接在有效条件内可能被重复使用。

服务端还应限制文件总大小、单块大小、并发上传数和未完成会话占用。读取请求时就需要控制大小和超时,不能先把无限内容放进内存,再检查是否超限。客户端声明的 MIME 类型和文件名只作为输入;服务端生成受控存储位置,不能按文件名中的路径写入任意目录。具体业务需要的扫描或转码完成前,可以保持私有和待处理状态。

验证上传方案时,应针对状态变化安排故障,而不只测试一次成功上传:

验证输入或故障应确认的结果
乱序分块、重复同块、同编号不同内容顺序可重建;相同重试不重复写;冲突遵循约定
缺块、最后一块过长、摘要错误、空文件不把不完整内容标记为成功;空文件按明确规则处理
并发完成和迟到分块内容冻结后不再变化,业务记录只创建一次
对象写成后进程退出重启能核对同一个对象,补齐业务记录
业务提交后响应连接断开重试返回原结果,不产生第二份附件
他人会话、过期会话、中止后写入不越权,不把失效会话重新推进为成功

本文配套验证使用独立本地 HTTP 服务、SQLite 和固定版本 MinIO,真实发送请求并注入进程退出与响应丢失。参考服务只处理不超过 1 MiB 的合成文件,为了验证分块与元数据原子提交,把小块字节一起存入 SQLite;这不是大文件的生产存储方案。最终对象使用独立 MinIO 服务,因而可以验证对象与业务数据库之间的失败间隙。它不能证明云端服务一致性、断电持久性、多实例吞吐或生产鉴权能力,也不是 tus 协议实现。

9 · 面试题卡片​

本页题目与参考回答

9.1 文件服务器怎样选型,传输失败后怎样处理?​

题型:综合题

考察重点:数据路径、存储需求、失败阶段与结果确认。

相关内容:上传的数据路径、对象与业务记录的一致性。

参考回答

先根据文件访问方式、大小、并发和多实例共享需求选择存储,再明确文件字节的传输路径。 业务服务转发便于集中检查,但承担流量和连接成本;客户端直传可以减少转发,仍要由业务服务创建有权限的上传会话并核对完成结果。

失败时先区分创建会话、传块、生成对象和业务提交。超时后结果可能未知,应查询原会话或沿用原身份重试;对象已成但业务未提交时核对对象并恢复记录。只有文件约束和业务条件都满足,才把附件标记为可用。

9.2 multipart/form-data 能否直接实现断点续传?​

考察重点:请求体格式、传输分帧与应用上传协议的区别。

相关内容:multipart 表单。

参考回答

不能仅靠这种格式实现断点续传。 它用 boundary 在一个请求体中划分字段和文件,不负责跨请求保存上传进度。续传还需要会话身份、已确认块或偏移、校验和完成协议。

HTTP 的传输分块也不是应用层可重试的文件块。对象存储多段上传提供自己的上传 ID 和完成 API,仍需与业务文件记录衔接。

9.3 分块上传和断点续传怎样实现?​

题型:综合题

考察重点:会话、分块身份、权威进度、校验与完成顺序。

相关内容:上传会话与断点续传、完成提交。

参考回答

先创建持久上传会话,固定文件大小、分块规则和归属,再按稳定身份提交分块。 服务端验证实际长度和摘要,保存成功后记录进度;响应丢失时允许同块重试。

恢复时查询服务端已确认的块或偏移,补传缺失内容。全部到齐后冻结清单,核对编号、顺序、总大小及内容,生成最终对象并提交业务记录。完成接口也要幂等,重试沿用原 uploadId,返回同一附件结果。

9.4 两个请求同时上传相同块编号但内容不同,怎样避免错误覆盖?​

考察重点:分块唯一身份、内容冲突和并发写入约束。

相关内容:分块写入的幂等规则。

参考回答

先定义同编号的冲突规则,再用原子机制执行。 若约定分块不可变,就以会话和块编号建立唯一身份:同内容返回已有结果,不同内容拒绝。

不能只先查后写。元数据可以由唯一约束保护,外部存储字节还需要不可变键或条件写入,防止记录虽然唯一、字节却已经被另一请求覆盖。若采用允许替换的协议,还应明确版本和完成清单怎样绑定实际内容。

9.5 每个分块的摘要都正确,能否证明完整文件正确且安全?​

考察重点:分块完整性、完整文件约束与内容安全的区别。

相关内容:分块摘要与完整文件摘要。

参考回答

不能。 单块摘要正确不能排除漏块、顺序错误或混入另一文件的合法分块,还要检查预期集合、长度和完整内容。摘要必须对应实际字节及明确算法,不能只相信客户端声明或把 ETag 一律当作 MD5。

恶意文件也能有正确摘要。摘要检查不能替代文件类型、内容处理和访问权限检查。

9.6 完成请求并发执行或响应丢失时,怎样保证只产生一份附件?​

考察重点:冻结清单、状态转换、稳定身份与结果复用。

相关内容:冻结内容与返回结果、超时后的再次完成。

参考回答

用持久的上传身份关联整个完成过程,冻结后不再允许分块变化,并为业务结果建立唯一约束。 外部对象操作沿用固定目标或幂等身份;业务附件记录与 READY 状态在本地事务中一起提交。

重复请求看到 READY 就返回原业务 ID;看到 FINALIZING 就查询或恢复原操作,不能重新生成一套身份。进程内锁只能约束当前进程,持久状态和外部写入约束仍然需要保留。

9.7 对象写入成功后服务崩溃,数据库没有附件记录,怎样恢复?​

考察重点:跨系统提交间隙、可信结果核对与清理竞争。

相关内容:对象与业务记录的一致性。

参考回答

根据持久上传会话找到原目标对象,验证它确实对应这次上传,再补齐业务事务。 核对不能只有“对象存在”,还应包括上传身份、大小和可信校验结果或版本。

补齐操作以上传身份防重,避免恢复任务和客户端重试各建一份记录。如果对象冲突、业务条件已失效或结果不能确认,应保留待处理状态并按规则修复或清理;清理也要检查状态和引用关系,不能删除正在完成或已经被引用的内容。

9.8 知道 uploadId 或文件 SHA-256,是否就可以续传和下载?​

考察重点:标识、完整性与资源授权的不同职责。

相关内容:权限、资源限制与验证。

参考回答

不可以,标识和摘要都不是权限证明。 每次查询、传块、完成和下载都要根据当前身份检查资源归属与操作权限。

直传凭据只授予所需对象和动作,并限制有效期。服务端生成受控存储位置,限制大小、并发和未完成会话占用;不能把客户端文件名、MIME 类型或“上传成功”通知直接当作可信事实。

10. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明