.NET Core WebApi 文件上传下载:流式处理、大文件分片与断点续传实战

发布时间:2026/10/11 5:20:56
.NET Core WebApi 文件上传下载:流式处理、大文件分片与断点续传实战 简介面向 .NET Core WebAPI 初、中级开发者的文件上传与下载服务示例工程完整演示了基于 IFormFile 的文件接收、请求解析、磁盘保存、响应输出以及下载时响应头的设置并覆盖异步处理、异常捕获、安全校验等常见实现思路。压缩包共五十个文件主体为 C# 源代码另含配置、工程文件、前端示例、容器化部署文件和说明文档整体约二百零六 KB目录按服务端、控制台客户端、前端演示、节点服务等模块划分结构清晰便于按需查阅。资源中提供可复用的上传中间件、缩略图中间件与均衡上传中间件并配有前端调用示例、配置说明和部署文件能帮助读者快速搭建一个具备上传与下载能力的 Web API 文件服务原型也能为已有项目改造提供参考。目前已经有一千九百二十一人学习下载。1. 从一次线上事故说起.NET Core WebApi 上传下载没那么简单某公司内部要做一个基于 .NET Core WebApi 的文件管理服务需求听起来很简单前端上传附件、按文件名下载附件。开发周期排了三天结果上线第一天就翻车。小文件能传大文件传一半断掉下载中文文件名直接乱码200MB 的文件一下载服务器内存就往上窜。后来才发现上传下载服务表面上是两个接口实际上牵扯到 Kestrel 请求体限制、流式读写、Range 请求头、文件名编码、临时文件策略这一堆东西。这篇笔记就把这套方案完整拆开讲从最小可跑通的代码到大文件分片、断点续传、路径安全再到高频故障的排查思路。适合刚接手 WebApi 文件功能的后端开发也适合被线上文件问题折腾过的熟手对照检查边界。2. 上传服务落地IFormFile、流式写入与大文件分片怎么选上传是文件服务的第一个入口。常见的做法有三种小文件直接用 IFormFile 绑定中文件用流式写入大文件做分片合并。选型的判断标准很简单文件大小、并发量、是否需要断点续传。下面从最小可跑通的代码开始一层层往上加。2.1 最小可跑通一个接收 IFormFile 的 Action 长什么样先给一个最朴素的写法。客户端用 multipart/form-data 把文件发过来服务端用 IFormFile 接收。这个写法在 10MB 以下的文件、低并发场景下完全够用。[HttpPost(upload)] [RequestSizeLimit(20 * 1024 * 1024)] public async TaskIActionResult Upload(IFormFile file) { if (file null || file.Length 0) return BadRequest(未收到文件或文件为空); var ext Path.GetExtension(file.FileName); var fileName ${Guid.NewGuid():N}{ext}; var savePath Path.Combine(_env.ContentRootPath, uploads, fileName); Directory.CreateDirectory(Path.GetDirectoryName(savePath)!); using var stream new FileStream(savePath, FileMode.Create, FileAccess.Write); await file.CopyToAsync(stream); return Ok(new { url $/files/{fileName} }); }逻辑说明先用RequestSizeLimit声明这个接口允许的最大请求体大小超过直接返回 413。文件名的处理是重点。file.FileName是客户端传来的原始文件名里面可能带路径、特殊字符直接拼到物理路径里会有路径穿越风险。所以这里不保留原名用Guid重新生成存储名扩展名单独取出来。这样既避免重名覆盖也把路径穿越的入口堵掉一半。参数说明RequestSizeLimit(20 * 1024 * 1024)这里的 20MB 是请求体上限含文件本体和 multipart 的 boundary 开销所以实际文件上限会略小于 20MB。FileMode.Create表示如果同名文件存在就直接覆盖这在后面并发场景是一个隐患。CopyToAsync是异步拷贝不会阻塞线程池但要注意它内部依然是边读边写不是一次性读完再写。这个写法的问题也明显IFormFile在 ASP.NET Core 中会把请求体缓冲到内存或临时文件文件一大内存就吃紧。实测超过 50MB 的文件服务器内存会出现明显尖峰。所以超过这个量级就要换流式写法。2.2 流式写入为什么不能直接 SaveAsAsync 一把梭很多教程会写file.SaveAsAsync(path)这个 API 内部做的事情是把整个文件内容读进内存再一次性写盘。文件一大内存直接起飞。流式写入的核心是边读边写内存占用恒定在一个小范围内。[HttpPost(upload-stream)] public async TaskIActionResult UploadStream() { if (!Request.ContentType?.Contains(multipart/form-data, StringComparison.OrdinalIgnoreCase) ?? true) return BadRequest(Content-Type 必须是 multipart/form-data); var boundary HeaderUtilities.RemoveQuotes( MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; var reader new MultipartReader(boundary, Request.Body); var section await reader.ReadNextSectionAsync(); while (section ! null) { var hasContentDisposition ContentDispositionHeaderValue.TryParse( section.ContentDisposition, out var disposition); if (hasContentDisposition disposition.DispositionType form-data) { var fileName disposition.FileName.Value; var savePath Path.Combine(_env.ContentRootPath, uploads, ${Guid.NewGuid():N}{Path.GetExtension(fileName)}); Directory.CreateDirectory(Path.GetDirectoryName(savePath)!); using var target new FileStream(savePath, FileMode.Create, FileAccess.Write); await section.Body.CopyToAsync(target); } section await reader.ReadNextSectionAsync(); } return Ok(new { result ok }); }逻辑说明这一段是真正的流式上传。MultipartReader逐段读取请求体每个 section 是一个表单字段当它带Content-Disposition: form-data且有filename时就是一个文件字段。section.Body是流直接CopyToAsync到目标文件全程没有把整个文件加载进内存。这里不再依赖框架的IFormFile自动绑定而是自己控制读取过程所以能精确掌握每段数据写入磁盘的时机。参数说明MediaTypeHeaderValue.Parse(Request.ContentType).Boundary是从 Content-Type 里解析出 multipart 的分隔符如果客户端没带 boundary 会解析失败所以要放在 try-catch 里。disposition.FileName.Value在多数浏览器里是纯文件名但某些客户端会带完整路径这里还是只取扩展名存储名用 Guid进一步规避路径问题。注意MultipartReader需要安装Microsoft.AspNetCore.WebUtilities包它在框架内是内置依赖不用额外引 NuGet。这个接口同样受 Kestrel 的MaxRequestBodySize限制上行 30MB 以内的文件直接用MultipartReader很稳。超过这个量建议走分片。2.3 大文件分片上传合并逻辑与断点记录分片上传解决的是两个问题一是单次请求体过大被网关或服务器拦二是网络抖动导致大文件传一半要整个重来。常见的分片方案是客户端把文件切成 N 个 5MB 的块按顺序上传服务端把每一块追加到同一个临时文件最后一块上传完成后执行「校验 改名」。[HttpPost(upload-chunk)] public async TaskIActionResult UploadChunk( [FromForm] string uploadId, [FromForm] int chunkIndex, [FromForm] int totalChunks, IFormFile chunk) { if (chunk null || chunk.Length 0) return BadRequest(分片为空); if (!Regex.IsMatch(uploadId, ^[a-zA-Z0-9]{32}$)) return BadRequest(uploadId 不合法); var tempDir Path.Combine(_env.ContentRootPath, temp, uploadId); Directory.CreateDirectory(tempDir); var chunkPath Path.Combine(tempDir, ${chunkIndex:D4}.part); using (var stream new FileStream(chunkPath, FileMode.Create, FileAccess.Write)) { await chunk.CopyToAsync(stream); } var uploadedCount Directory.GetFiles(tempDir, *.part).Length; if (uploadedCount totalChunks) { var finalPath Path.Combine(_env.ContentRootPath, uploads, ${uploadId}.dat); await using var finalStream new FileStream(finalPath, FileMode.Create, FileAccess.Write); for (var i 0; i totalChunks; i) { var partPath Path.Combine(tempDir, ${i:D4}.part); await using var partStream new FileStream(partPath, FileMode.Open, FileAccess.Read); await partStream.CopyToAsync(finalStream); } Directory.Delete(tempDir, true); return Ok(new { url $/files/{uploadId}.dat }); } return Ok(new { received uploadedCount }); }逻辑说明uploadId是客户端生成的一次上传会话标识建议用 Guid服务端用正则限制了字符集和长度这是防止路径穿越的第二道闸。每个分片落盘为0000.part、0001.part这样的临时文件数字序号保证合并顺序。每收完一片检查目录里的.part文件数量等于totalChunks就说明全齐了按顺序合并成一个最终文件。合并完成后删掉临时目录防止残留垃圾。参数说明{chunkIndex:D4}是四位补零如果有超过 9999 个分片这个格式会出问题。实际分片数一般不会超过几十个5MB 一片、2GB 文件也就 400 片。Directory.GetFiles(tempDir, *.part).Length这个判断有个隐含问题如果某个分片上传了两次数量是对的但内容被覆盖了所以合并前最好校验每个分片的大小是否一致。客户端分片时要保证除最后一片外每片大小相同服务端合并前可以抽样检查。这个方案的缺点是没有元数据记录中途有几个分片上传失败时客户端只能靠服务端返回的received数量自行判断重传哪些片。更健壮的方案是维护一张分片状态表但作为起步方案目录文件数判定已经够用。2.4 参数与选型请求体大小、超时、并发之间怎么权衡选哪个方案取决于文件尺寸和并发量。我一般这么定10MB 以下用IFormFile简单直接10MB 到 200MB 用流式写入内存可控超过 200MB 或需要断点续传直接分片。这个阈值不是绝对的但作为起步判断够用。Kestrel 的默认请求体上限是 30MB超过会直接拒绝。如果只调大单个接口的限制要配合全局设置一起看builder.WebHost.ConfigureKestrel(options { options.Limits.MaxRequestBodySize 100 * 1024 * 1024; // 100MB });参数说明MaxRequestBodySize设的是单请求体上限单位是字节。调大这个值要注意两点一是如果前面挂 IIS 或 Nginx网关层的请求体限制也要同步调否则还是被网关拦二是内存压力因为 Kestrel 处理大请求体时内部会有缓冲配置和物理内存要匹配。RequestSizeLimit特性是 per-action 覆盖MaxRequestBodySize是全局兜底两个同时存在时取更严格的那一方。超时方面MultipartReader方案本身是流式的不担心长时间占用内存但要关注反向代理的超时设置。Nginx 默认proxy_read_timeout是 60 秒传大文件时容易断要调大。这个不属于后端代码但排查线上问题时要往这个方向看。并发方面分片方案天然支持并发上传不同分片但合并时要加锁或用uploadId目录隔离避免两个请求同时合并同一个uploadId。目录隔离是最简单的办法因为uploadId是客户端生成的同一个 ID 并发合并的概率极低。若担心恶意请求服务端可以加一个内存锁或数据库唯一约束但这类场景通常不需要。提示上传目录和临时目录最好分开定时任务定期清理超过 24 小时没合并的 temp 目录避免磁盘被半截文件占满。3. 下载服务落地流式输出、断点续传与文件名编码上传做完下载是另一半。下载服务的核心诉求是不把文件整个读进内存支持断点续传中文文件名不乱码。这三个点分别对应FileStream流式输出、Range请求头、Content-Disposition编码。3.1 用 FileStream 做流式下载物理路径与 Content-Type 的对应最朴素的下载接口是根据文件名找物理路径把文件流交给响应。很多人会写File.ReadAllBytes()然后return File(bytes, contentType)这个写法在小文件时没感觉上 G 的文件会直接把内存打爆。[HttpGet(files/{fileName})] public async TaskIActionResult Download(string fileName) { var safeName Path.GetFileName(fileName); if (string.IsNullOrEmpty(safeName)) return BadRequest(文件名不合法); var fullPath Path.Combine(_env.ContentRootPath, uploads, safeName); if (!System.IO.File.Exists(fullPath)) return NotFound(); var contentType GetContentType(safeName); var stream new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); return File(stream, contentType, safeName); }逻辑说明Path.GetFileName(fileName)是关键一步请求 URL 里的fileName如果带了../Path.GetFileName会把它剥掉只保留最右侧的文件名。这样从入口处就杜绝了路径穿越。FileShare.Read允许其他进程同时读这个文件避免多个下载请求互相阻塞。File方法传入流后框架会负责把流写入响应并设置Content-Length头。参数说明GetContentType通常是一个扩展名到 MIME 的映射表比如.pdf映射application/pdf、.docx映射application/vnd.openxmlformats-officedocument.wordprocessingml.document。查不到时用application/octet-stream兜底。return File(stream, contentType, safeName)里的第三个参数是浏览器下载时的文件名这里有个隐藏的编码问题如果文件名是中文直接传字符串可能导致乱码具体处理在 3.3 里讲。另一个要注意的点FileStream打开文件时如果指定了FileShare.Read那么当文件被上传接口正在写入时下载接口可以读到一部分内容这是流式混用场景下的行为如果业务上不允许边传边下需要改为FileShare.None并在写入方也用独占方式打开。3.2 断点续传解析 Range 头并返回 206断点续传不是客户端断点续传而是服务端要响应Range请求头。下载工具、浏览器、播放器都会发Range: bytes0-1023这样的请求只取文件的一部分。如果服务端总是返回 200 全量数据下载中断后就得从头开始所以支持 206 是提升大文件下载体验的关键。[HttpGet(files/{fileName})] public async TaskIActionResult DownloadWithRange(string fileName) { var safeName Path.GetFileName(fileName); var fullPath Path.Combine(_env.ContentRootPath, uploads, safeName); if (!System.IO.File.Exists(fullPath)) return NotFound(); var fileLength new FileInfo(fullPath).Length; var rangeHeader Request.Headers[Range].FirstOrDefault(); if (string.IsNullOrEmpty(rangeHeader)) return File(new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read), GetContentType(safeName), safeName); var range ParseRange(rangeHeader, fileLength); if (range null) return StatusCode(416); // Range Not Satisfiable var (start, end) range.Value; var stream new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read) { Position start }; Response.StatusCode 206; Response.ContentType GetContentType(safeName); Response.Headers[Content-Range] $bytes {start}-{end}/{fileLength}; Response.Headers[Accept-Ranges] bytes; var buffer new byte[81920]; var remaining end - start 1; while (remaining 0) { var read await stream.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, remaining))); if (read 0) break; await Response.Body.WriteAsync(buffer.AsMemory(0, read)); remaining - read; } return new EmptyResult(); }逻辑说明ParseRange是自定义的 Range 解析函数处理bytesstart-end、bytesstart-、bytes-suffix三种形式。解析结果超出文件长度时返回 null再返回 416。响应状态码设为 206并带上Content-Range头告诉客户端这次返回的是文件的哪个区间。循环里按 80KB 一块读避免流结束后还继续写。参数说明81920是 80KB是 Stream 读写的常见缓冲区大小与 TCP 窗口匹配吞吐和内存占用平衡。Response.Body.WriteAsync直接写响应流不走File的快捷方法因为File不会帮你设置Content-Range。remaining用long计算大文件超过 2GB 不会溢出。这个手写方案的收益是完整的控制权代价是ParseRange要自己写测试覆盖边界情况。如果不想手写框架提供了更短的路子return PhysicalFile(fullPath, contentType, safeName, enableRangeProcessing: true);PhysicalFile的enableRangeProcessing: true会让框架自动解析 Range 头并输出 206省掉手写解析逻辑。我一般建议追求快速上线用PhysicalFile想完全控制头信息再手写。注意PhysicalFile的fileDownloadName参数同样有中文字符编码问题需要配合 3.3 的处理方式。3.3 中文文件名Content-Disposition 的编码坑文件名乱码是上传下载最常见的玄学问题。原因出在Content-Disposition头的编码方式上。现代浏览器支持 RFC 5987 的filename*格式用 UTF-8 百分号编码但老客户端只认filename的 ISO-8859-1。所以稳妥的做法是两个都带上。private static string BuildContentDisposition(string fileName) { var encoded Uri.EscapeDataString(fileName); return $attachment; filename\{fileName}\; filename*UTF-8{encoded}; }逻辑说明filenamexxx是给老客户端看的filename*UTF-8xxx是给现代浏览器看的。Chrome、Firefox、Edge 都会优先读filename*中文显示正常老工具链读filename至少不会报错。EscapeDataString会把中文转成百分号编码空格会编码成%20正好符合filename*的规范。注意Response.Headers[Content-Disposition]直接赋值时ASP.NET Core 会把带引号的分号值按 RFC 6266 重新格式化如果文件名里有双引号或分号会破坏头结构。所以更稳的做法是文件存储名只保留白名单字符原始文件名单独存元数据不直接参与 HTTP 头构造。如果必须用原始名先做字符过滤去掉、;、换行符。3.4 大目录下的下载性能这里有个黑匣子uploads目录下文件数量上万之后每次下载都要做一次Path.Combine加File.Exists开销并不大真正的性能瓶颈在文件系统的元数据查找。如果文件名是分散的随机 Guid多数文件系统在目录条目多时查找变慢。常见做法是按时分目录uploads/2024/11/25/{guid}.dat这样每个目录下的文件数量可控查找和冷备都方便。FileStream打开文件时FileShare.Read允许多个下载进程同时读但写入方要等读锁释放。所以下载接口和上传接口最好分开目录上传写temp/uploads下载读files避免同一个文件边传边下时锁冲突。另一个隐蔽问题Windows 上频繁打开关闭 FileStream 会带来句柄压力如果下载 QPS 高考虑用FileOptions.SequentialScan提示操作系统按顺序读取能减少磁盘寻道。4. 上传下载避坑5 个高频故障的现象、原因与解决这一章是踩坑记录每一条都是线上真实见过的故障。按「现象 → 原因 → 解决」的顺序写照着排查能省不少时间。4.1 413 错误Kestrel 的 MaxRequestBodySize 和 IIS 的 uploadReadAheadSize现象上传 50MB 的文件接口直接返回 413响应头里有Server: Kestrel字样。用 Postman 测本地没问题部署到服务器就报错或者反过来本地报错但服务器正常。原因三层限制层层叠加。Kestrel 的默认最大请求体是 30MB这是第一层。如果前面挂了 IIS 或 Nginx网关有自己的请求体限制这是第二层。框架的RequestSizeLimit特性是第三层。任何一层不满足就 413问题在于你先改的那一层未必是真正拦截的那一层。解决先确认是哪一层拦的。看响应头里的Server字段是Kestrel就调 Kestrel 的MaxRequestBodySize是nginx就调client_max_body_size是 IIS 就调uploadReadAheadSize。调 Kestrel 时注意RequestSizeLimit和MaxRequestBodySize同时存在时取更严格值如果接口上写了RequestSizeLimit(20MB)而全局配了 100MB这个接口仍然只能传 20MB。排查顺序不要跳直接改到最后发现没生效浪费时间。4.2 传一半断了服务器上留了半个文件现象客户端上传 2GB 文件传到 60% 网络断开服务端的FileMode.Create已经把目标文件建出来了流中断后文件只有 1.2GB 且文件损坏下次同名上传又覆盖它看起来没事但磁盘上堆了一堆永远没写完的残骸。原因FileMode.Create是直接打开目标文件写入中途断流不会自动清理。如果每次存储名都用 Guid这些半截文件就永久留在磁盘上肉眼根本发现不了直到磁盘满了才报警。解决先写临时文件全部写完再原子改名。具体做法先写到uploads/temp/{guid}.part写完调用File.Move(tmpPath, finalPath)。File.Move在同一分区内是原子操作不会出现半个文件。分片方案里这个思路更彻底每片一个.part文件合并时才生成最终文件合并失败可以随时清理临时目录。定时任务扫描 temp 目录删除超过 24 小时的.part文件这是生产环境必须加的兜底。4.3 路径穿越文件名里带 ../ 直接打穿目录现象请求/files/../../etc/passwd服务端把参数拼进路径读到了 uploads 目录之外的文件。更隐蔽的是下载接口把fileName直接拼Path.Combine../让它一路向上跳最终能读到任意有权限访问的文件。原因Path.Combine(_env.ContentRootPath, uploads, fileName)中fileName来自 URL 参数不可信。Path.Combine会把..当相对路径解析最终访问到 uploads 之外的目录。很多代码是从内网工具改来的假设客户端可信这在公网场景直接暴雷。解决两层防护。第一层是入口过滤Path.GetFileName(fileName)先剥掉所有路径部分只留文件名再配合白名单正则校验^[a-zA-Z0-9._-]$不匹配直接拒绝。第二层是最终路径必须Path.GetFullPath后检查前缀是否在 uploads 根目录内用StartsWith判断时注意/uploads2这种前缀混淆要带着目录分隔符比较。上传侧同理存储名一律服务端自己生成不接受客户端传的物理路径。4.4 下载大文件内存暴涨整个文件 ReadAllBytes 读进内存现象下载一个 1GB 文件服务器内存直接上涨 1GB多个用户同时下载内存溢出。更糟的是应用池或进程直接崩溃重启后又复现。原因代码里写了var bytes System.IO.File.ReadAllBytes(path); return File(bytes, contentType)。这个方法把整个文件一次性加载进内存是最典型的反面写法。另一个隐蔽写法是File.ReadAllText读文本文件再转字节不仅吃内存还多了一次编码转换。解决改用FileStream或PhysicalFile流式输出框架在写响应时边读边写内存占用稳定在缓冲区大小。检查线上代码时搜ReadAllBytes和ReadAllText出现在文件下载路径里的直接改。顺带提一句byte[]在 .NET 里有 2GB 上限超过 2GB 的文件用ReadAllBytes会直接抛异常这不是内存问题是数组大小限制所以大文件路径上流式是唯一正解。4.5 并发上传同名文件互相覆盖现象两个用户同时上传同名文件后提交的覆盖先提交的。如果文件名是客户端传来的原名这种覆盖是静默的没有报错等用户发现文件内容不对时原始内容已经没了。原因FileMode.Create对同名文件是覆盖写。两个请求同时打开同一个文件路径后写入的会截断先写入的内容最终文件是两个进程交错写入的混合体更危险的是文件大小对不上但没人察觉。解决服务端存储名一律用Guid.NewGuid()生成不接受客户端原始文件名作为存储名从根源上消灭同名冲突。如果业务上需要保留原文件名存元数据数据库里建一张file_meta表字段为Id、StorageName、OriginalName、UploadTime。下载时按Id找文件按OriginalName设置Content-Disposition。这样并发上传不会撞文件下载的文件名还是用户期望的名字。5. 进阶给上传下载服务加上秒传校验与下载限速基础功能跑通后接踵而来的是体验问题同一个文件反复上传浪费时间多个大文件下载占满出口带宽。这两个问题分别对应秒传和限速。5.1 秒传客户端先发哈希命中直接返回秒传的原理客户端上传前先计算文件哈希发给服务端服务端查这个哈希是否已存在存在就直接返回已存在标记前端视为上传成功实际没有传输字节。[HttpPost(upload-check)] public async TaskIActionResult CheckFile([FromBody] FileCheckRequest request) { var hash request.Hash; var existing await _fileMetaRepo.FindByHashAsync(hash); if (existing ! null existing.Size request.Size) { return Ok(new { exists true, url $/files/{existing.StorageName} }); } return Ok(new { exists false }); } public record FileCheckRequest(string Hash, long Size);逻辑说明Hash用 SHA-256不是 MD5。哈希命中后先比对文件大小双条件都匹配才判定已存在把碰撞误判概率再降一截。数据库里Hash字段要建唯一索引否则并发请求同一个哈希会查出重复记录。秒传的边界是哈希一样的两个文件内容几乎一定相同但对几乎不放心的话文件落盘后仍会做一次完整校验只是这个校验挪到后台异步跑不阻塞上传响应。5.2 下载限速自己动手写节流.NET 框架没有现成的下载限速中间件常见做法是在流式写入的循环里主动做节流。var buffer new byte[81920]; while ((read await stream.ReadAsync(buffer.AsMemory(0, buffer.Length))) 0) { var sw Stopwatch.StartNew(); await Response.Body.WriteAsync(buffer.AsMemory(0, read)); sw.Stop(); var expectedMs (double)read / _bytesPerSecond * 1000; var delayMs expectedMs - sw.Elapsed.TotalMilliseconds; if (delayMs 0) await Task.Delay(TimeSpan.FromMilliseconds(delayMs)); }逻辑说明_bytesPerSecond是限速目标比如 1MB/s 就是1024 * 1024。每次写入 80KB用Stopwatch测实际写入耗时和理论耗时对比差多少就Delay多少。这个算法的缺点是延时不够均匀TCP 缓冲区会导致突发写入但对内部系统的带宽控制已经够用。下载限速通常用在内部系统防止几个大文件下载占满出口带宽影响其他业务。公网场景一般不做服务端限速交给 CDN 去扛。文件功能上线前我习惯用 curl 手工验证一遍断点续传发两个 Range 请求分别拿文件的两段合并后做哈希比对。这个习惯是血泪经验换来的文件服务的 Bug 往往出现在边界而不是主路径。每次改完上传下载相关代码先跑一遍 200MB 文件的上传下载往返再验证一次中断续传最后才提交。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询