大文件分片上传的核心思路是前端将大文件切割成多个小分片逐个发送到服务端暂存全部接收完成后服务端按顺序合并还原。下面从前后端实现、数据库设计、断点续传、合并逻辑、并发优化和避坑指南六个维度来介绍。一、核心原理分片上传不是HTTP协议的内置特性需要业务层自行实现。前端使用File.slice()浏览器或FileStream.Read()桌面端将文件按固定大小切片单片大小建议2~5 MB——太小增加HTTP请求开销太大降低失败重传效率。每次请求携带三个关键字段fileId全文件唯一标识、chunkIndex从0开始的片序号、totalChunks总片数服务端按fileId chunkIndex幂等写入不能依赖请求顺序。二、前端实现C# 桌面端 / WinForms / WPF/// summary/// 大文件分片上传客户端使用 HttpClient/// /summarypublicclassChunkUploader{privatestaticreadonlyHttpClient_httpClientnewHttpClient();privateconstintCHUNK_SIZE5*1024*1024;// 5MB 每片privateconststringUPLOAD_URLhttps://localhost:5001/api/upload/chunk;privateconststringMERGE_URLhttps://localhost:5001/api/upload/merge;publicasyncTaskboolUploadLargeFileAsync(stringfilePath,stringfileId){usingvarfileStreamnewFileStream(filePath,FileMode.Open,FileAccess.Read,FileShare.Read,81920,FileOptions.Asynchronous);longfileSizefileStream.Length;inttotalChunks(int)Math.Ceiling((double)fileSize/CHUNK_SIZE);// 1. 查询服务端已上传的分片断点续传varuploadedChunksawaitGetUploadedChunksAsync(fileId);for(intchunkIndex0;chunkIndextotalChunks;chunkIndex){if(uploadedChunks.Contains(chunkIndex))continue;// 跳过已上传的分片// 2. 读取分片数据intoffsetchunkIndex*CHUNK_SIZE;intcurrentChunkSize(int)Math.Min(CHUNK_SIZE,fileSize-offset);byte[]chunkDatanewbyte[currentChunkSize];fileStream.Seek(offset,SeekOrigin.Begin);awaitfileStream.ReadAsync(chunkData,0,currentChunkSize);// 3. 计算当前分片的哈希值用于完整性校验stringchunkHashComputeSha256Hash(chunkData);// 4. 上传分片boolsuccessawaitUploadChunkAsync(fileId,chunkIndex,totalChunks,chunkData,chunkHash);if(!success){// 失败重试带指数退避successawaitRetryUploadAsync(fileId,chunkIndex,totalChunks,chunkData,chunkHash);if(!success)returnfalse;}}// 5. 所有分片上传完成触发合并returnawaitMergeChunksAsync(fileId,Path.GetFileName(filePath),fileSize);}privateasyncTaskboolUploadChunkAsync(stringfileId,intchunkIndex,inttotalChunks,byte[]chunkData,stringchunkHash){usingvarcontentnewMultipartFormDataContent();content.Add(newByteArrayContent(chunkData),file,$chunk_{chunkIndex});content.Add(newStringContent(fileId),fileId);content.Add(newStringContent(chunkIndex.ToString()),chunkIndex);content.Add(newStringContent(totalChunks.ToString()),totalChunks);content.Add(newStringContent(chunkHash),chunkHash);varresponseawait_httpClient.PostAsync(UPLOAD_URL,content);returnresponse.IsSuccessStatusCode;}privateasyncTaskHashSetintGetUploadedChunksAsync(stringfileId){varresponseawait_httpClient.GetAsync(${UPLOAD_URL}/status?fileId{fileId});if(!response.IsSuccessStatusCode)returnnewHashSetint();varjsonawaitresponse.Content.ReadAsStringAsync();varuploadedJsonSerializer.DeserializeListint(json);returnnewHashSetint(uploaded??newListint());}privateasyncTaskboolMergeChunksAsync(stringfileId,stringfileName,longfileSize){varmergeDatanew{fileId,fileName,fileSize};varcontentnewStringContent(JsonSerializer.Serialize(mergeData),Encoding.UTF8,application/json);varresponseawait_httpClient.PostAsync(MERGE_URL,content);returnresponse.IsSuccessStatusCode;}privatestaticstringComputeSha256Hash(byte[]data){usingvarsha256SHA256.Create();byte[]hashsha256.ComputeHash(data);returnConvert.ToHexString(hash).ToLowerInvariant();}}关键要点HttpClient必须复用单例实例或用IHttpClientFactory否则会导致 socket 耗尽超时时间需要显式配置为较大值如 30 分钟默认 100 秒不足以完成大文件上传.NET 5 中StreamContent默认不会自动 Dispose 底层流建议改用ByteArrayContent以确保安全。三、服务端实现ASP.NET Core3.1 服务配置Program.csvarbuilderWebApplication.CreateBuilder(args);// 禁用默认请求体大小限制两层都要配置builder.WebHost.ConfigureKestrel(options{options.Limits.MaxRequestBodySizelong.MaxValue;// 禁用 Kestrel 层限制});builder.Services.ConfigureFormOptions(options{options.MultipartBodyLengthLimitlong.MaxValue;// 禁用 MVC 层限制});varappbuilder.Build();ASP.NET Core 中有两层请求体限制Kestrel 自身的MaxRequestBodySize默认 30MB和 MVC 层的MultipartBodyLengthLimit两层必须同时调整才能生效。3.2 分片上传 APIUploadController[ApiController][Route(api/[controller])][DisableRequestSizeLimit]// 禁用请求大小限制publicclassUploadController:ControllerBase{privatereadonlyIUploadService_uploadService;publicUploadController(IUploadServiceuploadService){_uploadServiceuploadService;}/// summary/// 上传单个分片绕过 IFormFile避免 OOM/// /summary[HttpPost(chunk)]publicasyncTaskIActionResultUploadChunk([FromForm]ChunkUploadRequestrequest){// 验证参数if(string.IsNullOrEmpty(request.FileId)||request.ChunkIndex0)returnBadRequest(Invalid parameters);// 验证分片哈希usingvarmsnewMemoryStream();awaitrequest.File.CopyToAsync(ms);byte[]chunkDatams.ToArray();stringcomputedHashComputeSha256Hash(chunkData);if(!computedHash.Equals(request.ChunkHash,StringComparison.OrdinalIgnoreCase))returnBadRequest(Chunk hash mismatch);// 幂等保存如果已存在则直接返回成功boolsavedawait_uploadService.SaveChunkAsync(request.FileId,request.ChunkIndex,chunkData,request.ChunkHash);if(!saved)returnConflict(new{messageChunk already exists,indexrequest.ChunkIndex});returnOk(new{successtrue,indexrequest.ChunkIndex});}/// summary/// 查询已上传的分片索引断点续传核心/// /summary[HttpGet(chunk/status)]publicasyncTaskIActionResultGetUploadedChunks([FromQuery]stringfileId){varuploadedChunksawait_uploadService.GetUploadedChunkIndicesAsync(fileId);returnOk(uploadedChunks);}/// summary/// 合并所有分片/// /summary[HttpPost(merge)]publicasyncTaskIActionResultMergeChunks([FromBody]MergeRequestrequest){// 加锁防止并发合并boolmergedawait_uploadService.MergeChunksAsync(request.FileId,request.FileName);if(!merged)returnConflict(new{messageMerge failed or already in progress});returnOk(new{successtrue,filePath$/uploads/{request.FileName}});}}publicclassChunkUploadRequest{publicstringFileId{get;set;}publicintChunkIndex{get;set;}publicintTotalChunks{get;set;}publicstringChunkHash{get;set;}publicIFormFileFile{get;set;}}publicclassMergeRequest{publicstringFileId{get;set;}publicstringFileName{get;set;}publiclongFileSize{get;set;}}关键要点不要使用IFormFile直接处理 GB 级文件它会触发完整文件读取和内存缓冲导致 OOM。但分片上传场景下单片只有 2-5 MB用IFormFile是可行的每片保存后必须校验哈希网络传输中单片出错很常见仅靠文件大小无法判断内容正确性接口必须支持幂等写入——重复上传同一片应直接返回成功而非报错。四、数据库设计跟踪上传状态为支持断点续传和状态恢复需要设计两张核心表上传会话表UploadSession字段类型说明SessionIdGUID PK文件上传会话唯一标识FileNameVARCHAR(255)原始文件名FileSizeBIGINT文件总大小字节FileHashVARCHAR(128)整个文件的 SHA256 值秒传校验ChunkSizeINT分片大小字节TotalChunksINT总分片数UploadedChunksCountINT已上传分片数StatusTINYINT状态0-上传中1-合并中2-已完成3-失败CreatedAtDATETIME2创建时间UpdatedAtDATETIME2更新时间分片记录表UploadedChunk字段类型说明ChunkIdBIGINT PK自增主键SessionIdGUID FK关联到 UploadSessionChunkIndexINT分片序号从 0 开始ChunkSizeINT该分片大小最后一片可能较小ChunkHashVARCHAR(128)该分片的 SHA256 值StoredPathVARCHAR(500)分片在磁盘上的存储路径UploadedAtDATETIME2上传时间状态持久化策略内存维护活跃会话可以提升性能但进程崩溃会丢失状态。生产环境应在关键节点落库首次上传时插入记录每个分片成功后更新UploadedChunksCount和lastChunkIndex合并完成后将Status改为Completed并清理临时文件。五、分片合并实现/// summary/// 安全合并分片使用 Seek 定位写入避免内存溢出/// /summarypublicasyncTaskboolMergeChunksAsync(stringfileId,stringfinalFileName){varchunksawaitGetChunksOrderedAsync(fileId);if(chunks.Count0)returnfalse;// 检查是否所有分片都已到达inttotalChunksawaitGetTotalChunksCountAsync(fileId);if(chunks.Count!totalChunks)returnfalse;stringtempDirPath.Combine(_config[Storage:ChunkPath],fileId);stringfinalPathPath.Combine(_config[Storage:FinalPath],finalFileName);// 使用 FileStream 配合 Seek 定位写入而非全量加载usingvarfinalStreamnewFileStream(finalPath,FileMode.Create,FileAccess.Write,FileShare.None,81920,useAsync:true);intchunkSize_config.GetValueint(ChunkSize,5*1024*1024);foreach(varchunkinchunks){longoffsetchunk.ChunkIndex*(long)chunkSize;finalStream.Seek(offset,SeekOrigin.Begin);stringchunkPathPath.Combine(tempDir,${fileId}_{chunk.ChunkIndex}.tmp);usingvarchunkStreamnewFileStream(chunkPath,FileMode.Open,FileAccess.Read);awaitchunkStream.CopyToAsync(finalStream);}awaitfinalStream.FlushAsync();// 合并完成后校验全文件哈希可选stringfinalHashawaitComputeFileSha256Async(finalPath);if(!finalHash.Equals(awaitGetExpectedFileHashAsync(fileId),StringComparison.OrdinalIgnoreCase)){File.Delete(finalPath);returnfalse;}// 清理临时分片文件和目录foreach(varchunkinchunks){File.Delete(Path.Combine(tempDir,${fileId}_{chunk.ChunkIndex}.tmp));}Directory.Delete(tempDir);returntrue;}合并要点不要用File.AppendAllBytes()或File.ReadAllBytes()File.WriteAllBytes()大文件会内存溢出必须使用FileStream.Seek()按分片编号计算偏移量后写入确保写入位置精确合并前必须校验三个条件分片哈希完整、全部分片已到达、加锁防止并发合并合并成功后立即清理临时文件失败时也要清理并标记任务为失败状态建议设置后台定时任务如每 30 分钟执行一次扫描并清理超过 2 小时未完成上传的临时分片。六、断点续传实现断点续传的核心是客户端在开始上传前先向服务端查询已接收的分片索引跳过这些索引再上传剩余分片。流程如下客户端计算fileId通常为文件名_文件大小_最后修改时间或文件内容的 MD5客户端发送 HEAD/GET 请求GET /api/upload/chunk/status?fileIdxxx获取服务端已接收的chunkIndex列表客户端比对本地分片列表跳过已上传的分片仅上传缺失部分每上传成功一个分片服务端立即持久化状态到数据库所有分片上传完成后调用/merge接口触发合并。注意事项不要用本地文件修改时间或 MD5 做续传依据服务端可能清理过临时文件每个分片上传后必须检查 HTTP 状态码和响应体中的明确确认信息遇到 409 Conflict分片已存在可直接跳过遇到 500 错误则采用指数退避重试策略最多 3 次断点续传需要服务端持久化状态仅依赖磁盘临时文件是不够的——IIS 或 Kestrel 重启后已上传的分片会丢失。七、并发上传优化多个分片可以并发上传以提升效率但需控制并发数避免带宽抢占// 使用 SemaphoreSlim 控制最大并发数privatestaticreadonlySemaphoreSlim_semaphorenewSemaphoreSlim(3);// 最多 3 个并发publicasyncTaskUploadWithConcurrencyAsync(stringfilePath,stringfileId,inttotalChunks){vartasksnewListTask();for(intchunkIndex0;chunkIndextotalChunks;chunkIndex){await_semaphore.WaitAsync();intindexchunkIndex;// 捕获变量tasks.Add(Task.Run(async(){try{awaitUploadSingleChunkAsync(filePath,fileId,index,totalChunks);}finally{_semaphore.Release();}}));}awaitTask.WhenAll(tasks);}八、避坑指南1. 服务端默认限制问题ASP.NET Core 有两层请求体限制必须同时调整才生效。Kestrel 默认MaxRequestBodySize为 30MBMVC 层也有自己的限制两层都要配置为long.MaxValue。2. Stream 行为差异.NET Framework 中StreamContent会自动 Dispose 底层流而 .NET 5 默认不会。建议统一使用ByteArrayContent避免兼容性问题。3. HTTP 顺序不可靠HTTP 请求不保证顺序到达服务端必须以fileId chunkIndex为准进行幂等写入不能依赖请求到达顺序进行合并。4. 大文件哈希计算计算整个文件的 SHA256 时不要用SHA256.Create().ComputeHash(fileStream)一次性读入内存而应使用TransformBlock/TransformFinalBlock增量分块计算避免 OOM。5. 合并时的并发控制合并操作必须加锁防止并发多次触发。可使用文件锁FileStream.Lock()或分布式锁如 Redis SETNX实现。6. 临时文件清理必须设置自动清理机制用后台定时任务扫描lastModified超过设定时间如 2 小时的临时分片并删除避免磁盘被残留文件占满。九、方案选择建议方案适用场景优点缺点自建分片上传需要完全掌控、自定义业务逻辑灵活可控、无外部依赖开发成本高、需要处理所有边界情况WebUploader ASP.NET MVCWeb 端大文件上传历史项目成熟稳定、社区资源多前端依赖外部组件阿里云 OSS / 腾讯云 COS直接对接云存储分片上传已内置、高可靠、支持断点续传需要云服务账号、有流量费用Azure Blob Storage微软生态项目与 .NET 集成好、原生支持块上传仅限 Azure 环境建议如果项目已经使用云存储优先使用云厂商的 SDK如阿里云 OSS、Azure Blob、腾讯云 COS它们内置了分片上传、断点续传和错误重试机制。如果需要完全自建请务必关注上述的数据库设计、幂等性、并发控制和临时文件清理等生产环境要点。