直接用HttpClient.PostAsync传大文件会失败,因默认超时短、内存占用高、中断需全重传;应采用前端分片+后端按块接收+校验合并的断点续传方案。

为什么直接用 HttpClient.PostAsync 传大文件会失败
因为默认超时时间短、内存占用高、网络中断就全盘重传。常见现象是上传到 80% 左右卡住,抛出 TaskCanceledException 或 HttpRequestException,根本原因是底层 Stream 一次性读入内存(尤其用 File.ReadAllBytes),或未设置 Timeout 和 MaxResponseContentBufferSize。
真正可行的路径是:前端分片 → 后端按块接收 → 合并前校验 MD5/SHA256 → 支持按块查询已传状态。
- 切片大小建议固定为
4 * 1024 * 1024(4MB),太小增加 HTTP 开销,太大不利于内存控制 - 每个分片必须携带唯一标识:
fileId(如 UUID)、chunkIndex、totalChunks、chunkHash(可选) - 后端不要依赖 Session 存储分片状态——改用 Redis 或数据库记录
fileId:chunkIndex → status
ASP.NET Core 中如何接收单个分片并落盘
关键不是“怎么存”,而是“怎么安全地存”:避免并发写同一文件、防止恶意覆盖、确保原子性。推荐用临时目录 + 块文件名含 fileId 和 chunkIndex,例如 uploads/{fileId}/{chunkIndex}.bin。
示例控制器方法:
[HttpPost("upload/chunk")]
public async Task<IActionResult> UploadChunk([FromForm] string fileId,
[FromForm] int chunkIndex,
[FromForm] int totalChunks,
IFormFile file)
{
var uploadDir = Path.Combine("uploads", fileId);
Directory.CreateDirectory(uploadDir);
var chunkPath = Path.Combine(uploadDir, $"{chunkIndex}.bin");
<pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;">await using var fs = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await file.CopyToAsync(fs); // 注意:这里必须用流式拷贝,禁用 file.OpenReadStream().ToArray()</code>- 务必设
FileMode.Create + <code>FileShare.None,防止多线程同时写同一块 -
CopyToAsync的缓冲区大小设为4096,配合异步 I/O 避免阻塞线程池 - 别用
IFormFile.Length做校验——它可能不准;应以实际写入字节数为准,或额外计算SHA256比对
怎么判断某块是否已上传成功(断点续传核心)
客户端每次上传前先发个 GET /upload/status?fileId=xxx&chunkIndex=yyy,后端查该块是否存在且大小正确(比如不小于 99% 的目标大小),返回 { "exists": true } 即可跳过。
注意不能只查文件存在——可能写了一半崩溃了。所以得加一层校验:
- 查文件长度:
new FileInfo(chunkPath).Length >= expectedSize * 0.99 - 或更稳妥:把每块哈希存进 Redis,上传完立即写入,查询时比对
chunkHash - Redis key 建议用
upload:status:{fileId}:{chunkIndex},过期时间设为 24 小时防堆积
合并分片时为什么不能直接 FileStream 追加
因为顺序错乱、中间缺块、磁盘空间不足都会导致最终文件损坏。必须先校验所有块存在且完整,再按 chunkIndex 排序合并。
示例合并逻辑:
var chunks = Directory.GetFiles(uploadDir, "*.bin")
.Select(p => new { Path = p, Index = int.Parse(Path.GetFileNameWithoutExtension(p)) })
.OrderBy(x => x.Index)
.ToList();
<p>if (chunks.Count != totalChunks) throw new InvalidOperationException("Missing chunks");</p><p>await using var output = File.Create(finalPath);
foreach (var chunk in chunks)
{
await using var input = File.OpenRead(chunk.Path);
await input.CopyToAsync(output); // 不要 Buffer.BlockCopy,保持流式和可控内存
}- 合并前必须检查
chunks.Count == totalChunks,否则直接报错,不静默跳过 - 用
File.OpenRead而非File.ReadAllBytes,避免单块 4MB 导致 GC 压力 - 合并完成后立刻计算最终文件的
SHA256并与前端传来的总哈希比对,不一致就删掉重来
最易被忽略的是清理机制:上传成功后没删临时块,几天后磁盘爆满。建议加一个后台服务定期扫描 uploads/* 下超过 2 小时未完成的目录并删除。


















