当前位置: 首页 > news >正文

C# 大文件分片上传完整实现指南

大文件分片上传的核心思路是:前端将大文件切割成多个小分片,逐个发送到服务端暂存,全部接收完成后服务端按顺序合并还原。下面从前后端实现、数据库设计、断点续传、合并逻辑、并发优化和避坑指南六个维度来介绍。

一、核心原理

分片上传不是HTTP协议的内置特性,需要业务层自行实现。前端使用File.slice()(浏览器)或FileStream.Read()(桌面端)将文件按固定大小切片,单片大小建议2~5 MB——太小增加HTTP请求开销,太大降低失败重传效率。每次请求携带三个关键字段:fileId(全文件唯一标识)、chunkIndex(从0开始的片序号)、totalChunks(总片数),服务端按fileId + chunkIndex幂等写入,不能依赖请求顺序。

二、前端实现(C# 桌面端 / WinForms / WPF)

/// <summary>/// 大文件分片上传客户端(使用 HttpClient)/// </summary>publicclassChunkUploader{privatestaticreadonlyHttpClient_httpClient=newHttpClient();privateconstintCHUNK_SIZE=5*1024*1024;// 5MB 每片privateconststringUPLOAD_URL="https://localhost:5001/api/upload/chunk";privateconststringMERGE_URL="https://localhost:5001/api/upload/merge";publicasyncTask<bool>UploadLargeFileAsync(stringfilePath,stringfileId){usingvarfileStream=newFileStream(filePath,FileMode.Open,FileAccess.Read,FileShare.Read,81920,FileOptions.Asynchronous);longfileSize=fileStream.Length;inttotalChunks=(int)Math.Ceiling((double)fileSize/CHUNK_SIZE);// 1. 查询服务端已上传的分片(断点续传)varuploadedChunks=awaitGetUploadedChunksAsync(fileId);for(intchunkIndex=0;chunkIndex<totalChunks;chunkIndex++){if(uploadedChunks.Contains(chunkIndex))continue;// 跳过已上传的分片// 2. 读取分片数据intoffset=chunkIndex*CHUNK_SIZE;intcurrentChunkSize=(int)Math.Min(CHUNK_SIZE,fileSize-offset);byte[]chunkData=newbyte[currentChunkSize];fileStream.Seek(offset,SeekOrigin.Begin);awaitfileStream.ReadAsync(chunkData,0,currentChunkSize);// 3. 计算当前分片的哈希值(用于完整性校验)stringchunkHash=ComputeSha256Hash(chunkData);// 4. 上传分片boolsuccess=awaitUploadChunkAsync(fileId,chunkIndex,totalChunks,chunkData,chunkHash);if(!success){// 失败重试(带指数退避)success=awaitRetryUploadAsync(fileId,chunkIndex,totalChunks,chunkData,chunkHash);if(!success)returnfalse;}}// 5. 所有分片上传完成,触发合并returnawaitMergeChunksAsync(fileId,Path.GetFileName(filePath),fileSize);}privateasyncTask<bool>UploadChunkAsync(stringfileId,intchunkIndex,inttotalChunks,byte[]chunkData,stringchunkHash){usingvarcontent=newMultipartFormDataContent();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");varresponse=await_httpClient.PostAsync(UPLOAD_URL,content);returnresponse.IsSuccessStatusCode;}privateasyncTask<HashSet<int>>GetUploadedChunksAsync(stringfileId){varresponse=await_httpClient.GetAsync($"{UPLOAD_URL}/status?fileId={fileId}");if(!response.IsSuccessStatusCode)returnnewHashSet<int>();varjson=awaitresponse.Content.ReadAsStringAsync();varuploaded=JsonSerializer.Deserialize<List<int>>(json);returnnewHashSet<int>(uploaded??newList<int>());}privateasyncTask<bool>MergeChunksAsync(stringfileId,stringfileName,longfileSize){varmergeData=new{fileId,fileName,fileSize};varcontent=newStringContent(JsonSerializer.Serialize(mergeData),Encoding.UTF8,"application/json");varresponse=await_httpClient.PostAsync(MERGE_URL,content);returnresponse.IsSuccessStatusCode;}privatestaticstringComputeSha256Hash(byte[]data){usingvarsha256=SHA256.Create();byte[]hash=sha256.ComputeHash(data);returnConvert.ToHexString(hash).ToLowerInvariant();}}

关键要点

  • HttpClient必须复用单例实例或用IHttpClientFactory,否则会导致 socket 耗尽;
  • 超时时间需要显式配置为较大值(如 30 分钟),默认 100 秒不足以完成大文件上传;
  • .NET 5+ 中StreamContent默认不会自动 Dispose 底层流,建议改用ByteArrayContent以确保安全。

三、服务端实现(ASP.NET Core)

3.1 服务配置(Program.cs)
varbuilder=WebApplication.CreateBuilder(args);// 禁用默认请求体大小限制(两层都要配置)builder.WebHost.ConfigureKestrel(options=>{options.Limits.MaxRequestBodySize=long.MaxValue;// 禁用 Kestrel 层限制});builder.Services.Configure<FormOptions>(options=>{options.MultipartBodyLengthLimit=long.MaxValue;// 禁用 MVC 层限制});varapp=builder.Build();

ASP.NET Core 中有两层请求体限制:Kestrel 自身的MaxRequestBodySize(默认 30MB)和 MVC 层的MultipartBodyLengthLimit两层必须同时调整才能生效。

3.2 分片上传 API(UploadController)
[ApiController][Route("api/[controller]")][DisableRequestSizeLimit]// 禁用请求大小限制publicclassUploadController:ControllerBase{privatereadonlyIUploadService_uploadService;publicUploadController(IUploadServiceuploadService){_uploadService=uploadService;}/// <summary>/// 上传单个分片(绕过 IFormFile,避免 OOM)/// </summary>[HttpPost("chunk")]publicasyncTask<IActionResult>UploadChunk([FromForm]ChunkUploadRequestrequest){// 验证参数if(string.IsNullOrEmpty(request.FileId)||request.ChunkIndex<0)returnBadRequest("Invalid parameters");// 验证分片哈希usingvarms=newMemoryStream();awaitrequest.File.CopyToAsync(ms);byte[]chunkData=ms.ToArray();stringcomputedHash=ComputeSha256Hash(chunkData);if(!computedHash.Equals(request.ChunkHash,StringComparison.OrdinalIgnoreCase))returnBadRequest("Chunk hash mismatch");// 幂等保存:如果已存在则直接返回成功boolsaved=await_uploadService.SaveChunkAsync(request.FileId,request.ChunkIndex,chunkData,request.ChunkHash);if(!saved)returnConflict(new{message="Chunk already exists",index=request.ChunkIndex});returnOk(new{success=true,index=request.ChunkIndex});}/// <summary>/// 查询已上传的分片索引(断点续传核心)/// </summary>[HttpGet("chunk/status")]publicasyncTask<IActionResult>GetUploadedChunks([FromQuery]stringfileId){varuploadedChunks=await_uploadService.GetUploadedChunkIndicesAsync(fileId);returnOk(uploadedChunks);}/// <summary>/// 合并所有分片/// </summary>[HttpPost("merge")]publicasyncTask<IActionResult>MergeChunks([FromBody]MergeRequestrequest){// 加锁防止并发合并boolmerged=await_uploadService.MergeChunksAsync(request.FileId,request.FileName);if(!merged)returnConflict(new{message="Merge failed or already in progress"});returnOk(new{success=true,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关联到 UploadSession
ChunkIndexINT分片序号(从 0 开始)
ChunkSizeINT该分片大小(最后一片可能较小)
ChunkHashVARCHAR(128)该分片的 SHA256 值
StoredPathVARCHAR(500)分片在磁盘上的存储路径
UploadedAtDATETIME2上传时间

状态持久化策略

内存维护活跃会话可以提升性能,但进程崩溃会丢失状态。生产环境应在关键节点落库:首次上传时插入记录,每个分片成功后更新UploadedChunksCountlastChunkIndex,合并完成后将Status改为Completed并清理临时文件。

五、分片合并实现

/// <summary>/// 安全合并分片(使用 Seek 定位写入,避免内存溢出)/// </summary>publicasyncTask<bool>MergeChunksAsync(stringfileId,stringfinalFileName){varchunks=awaitGetChunksOrderedAsync(fileId);if(chunks.Count==0)returnfalse;// 检查是否所有分片都已到达inttotalChunks=awaitGetTotalChunksCountAsync(fileId);if(chunks.Count!=totalChunks)returnfalse;stringtempDir=Path.Combine(_config["Storage:ChunkPath"],fileId);stringfinalPath=Path.Combine(_config["Storage:FinalPath"],finalFileName);// 使用 FileStream 配合 Seek 定位写入,而非全量加载usingvarfinalStream=newFileStream(finalPath,FileMode.Create,FileAccess.Write,FileShare.None,81920,useAsync:true);intchunkSize=_config.GetValue<int>("ChunkSize",5*1024*1024);foreach(varchunkinchunks){longoffset=chunk.ChunkIndex*(long)chunkSize;finalStream.Seek(offset,SeekOrigin.Begin);stringchunkPath=Path.Combine(tempDir,$"{fileId}_{chunk.ChunkIndex}.tmp");usingvarchunkStream=newFileStream(chunkPath,FileMode.Open,FileAccess.Read);awaitchunkStream.CopyToAsync(finalStream);}awaitfinalStream.FlushAsync();// 合并完成后校验全文件哈希(可选)stringfinalHash=awaitComputeFileSha256Async(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 小时未完成上传的临时分片。

六、断点续传实现

断点续传的核心是客户端在开始上传前先向服务端查询已接收的分片索引,跳过这些索引再上传剩余分片

流程如下:

  1. 客户端计算fileId(通常为文件名_文件大小_最后修改时间或文件内容的 MD5);
  2. 客户端发送 HEAD/GET 请求GET /api/upload/chunk/status?fileId=xxx,获取服务端已接收的chunkIndex列表;
  3. 客户端比对本地分片列表,跳过已上传的分片,仅上传缺失部分;
  4. 每上传成功一个分片,服务端立即持久化状态到数据库;
  5. 所有分片上传完成后,调用/merge接口触发合并。

注意事项

  • 不要用本地文件修改时间或 MD5 做续传依据,服务端可能清理过临时文件;
  • 每个分片上传后必须检查 HTTP 状态码和响应体中的明确确认信息,遇到 409 Conflict(分片已存在)可直接跳过,遇到 500 错误则采用指数退避重试策略(最多 3 次);
  • 断点续传需要服务端持久化状态,仅依赖磁盘临时文件是不够的——IIS 或 Kestrel 重启后已上传的分片会丢失。

七、并发上传优化

多个分片可以并发上传以提升效率,但需控制并发数避免带宽抢占:

// 使用 SemaphoreSlim 控制最大并发数privatestaticreadonlySemaphoreSlim_semaphore=newSemaphoreSlim(3);// 最多 3 个并发publicasyncTaskUploadWithConcurrencyAsync(stringfilePath,stringfileId,inttotalChunks){vartasks=newList<Task>();for(intchunkIndex=0;chunkIndex<totalChunks;chunkIndex++){await_semaphore.WaitAsync();intindex=chunkIndex;// 捕获变量tasks.Add(Task.Run(async()=>{try{awaitUploadSingleChunkAsync(filePath,fileId,index,totalChunks);}finally{_semaphore.Release();}}));}awaitTask.WhenAll(tasks);}

八、避坑指南

1. 服务端默认限制问题

ASP.NET Core 有两层请求体限制,必须同时调整才生效。Kestrel 默认MaxRequestBodySize为 30MB,MVC 层也有自己的限制,两层都要配置为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),它们内置了分片上传、断点续传和错误重试机制。如果需要完全自建,请务必关注上述的数据库设计、幂等性、并发控制和临时文件清理等生产环境要点。

http://www.jsqmd.com/news/637696/

相关文章:

  • 1 【3D Gaussian Splatting: From Theory to Real-Time Implementation】第一级:基础理论与数学建模
  • 万象视界灵坛部署案例:高校AI实验室零基础学生30分钟完成多模态项目搭建
  • 必收藏!AI小白/程序员轻松入门大模型,从AIGC到实战应用全解析
  • 2026年口碑好的中性笔装配机/苏州笔装配机/苏州白板笔装配机/苏州记号笔装配机用户口碑推荐厂家 - 行业平台推荐
  • 一键切换node版本
  • 收藏!小白程序员入门大模型的30个核心指标详解
  • Multi-Agent Planner:多智能体协作的架构设计
  • 武汉武昌环境好的写字楼出租排行榜
  • 知网AI率高怎么降?免费方法和付费工具效果实测对比
  • 小白程序员必看:零基础转型大模型应用开发,薪资涨幅超30%!收藏版学习路径分享
  • 小白程序员必看!2-3小时/天,3个月蜕变,轻松上手大模型,收藏这份高性价比学习路线
  • 不需要Memory Bank:CMDR-IAD用2D+3D双分支重建做工业异常检测,MVTec 3D 97.3%
  • Cogito-V1-Preview-Llama-3B在软件测试中的应用:自动生成测试用例与缺陷报告
  • 收藏!小白程序员必备:AI大模型技术入门与高薪就业指南
  • 如何在规划中融入因果推理能力?
  • 收藏 | AI Agent记忆机制详解:小白程序员必备,助你轻松入门大模型世界!
  • 深入解析Buffer在存储器电路设计中的关键作用:驱动能力与负载优化
  • TCGA 数据挖掘实战 —— WGCNA 模块与临床表型关联分析
  • Flutter video_player 2.10.1 插件避坑指南:从iOS权限到Web端CORS,一次搞定全平台配置
  • 2026年4月专业的清洗剂定制厂家推荐,VCI防锈海绵/气相防锈母粒/VCI气相防锈袋,清洗剂企业哪个值得选 - 品牌推荐师
  • 深入解析:零基础学深度学习需要学哪些框架?PyTorch 和 TensorFlow 选哪个?
  • SBTI在线测试:解锁趣味人格,3分钟读懂真实自我
  • 深入浅出讲解操作系统——实时调度
  • Shader Graph:可视化编程在 URP/HDRP 中的应用
  • 用GEO优化,让品牌适配未来AI商业
  • 2026年评价高的内蒙古专用有机肥/内蒙古园林绿化专用有机肥/园林绿化专用有机肥横向对比厂家推荐 - 品牌宣传支持者
  • 用JRC水体数据集和GEE,5分钟搞定近40年你家附近水域变化分析
  • Unity NGO 系列教程(五):如何构建多人联机区域触发系统
  • 实测对比:OpenCV微信QRCode vs ZXing二维码识别性能(附C++测试代码)
  • 2026年口碑好的土壤改良专用有机肥/大田专用有机肥/内蒙古园林绿化专用有机肥/花卉专用有机肥厂家选择推荐 - 行业平台推荐