这个问题几乎每隔一段时间就会有人问我一次:“ASP.NET到底能不能做大文件断点续传?有没有现成的示例项目?”我最初接触的时候也以为需要引入什么重量级框架或商业控件,后来自己从零搭了一个基于ASP.NET Core Web API的示例,发现核心思路并不复杂,关键是把分片、并发、状态记录和合并逻辑理清楚,再配好服务器和IIS的请求限制。下面我就把整个项目的设计思路、核心代码和踩过的坑完整拆一遍,包括前端用File API切片、Web Worker优化、服务端接收分片与合并,以及最终发布到IIS时那些绕不开的配置问题。
这篇文章重点面向两类人:一是还在用传统ASP.NET Web Forms、被“不能装载NTKO大文件上传控件,请确保使用IE浏览器”这类提示折磨的朋友,二是想从零实现或移植断点续传功能、但不确定后端怎么设计的.NET开发者。我会尽量把每一步为什么这样做讲清楚,而不是只丢一段能跑的代码。
1. 先搞清楚大文件上传为什么会失败
1.1 三个拦路虎:请求体限制、服务器超时、网络抖动
很多人第一次做大文件上传,都会发现一个诡异的现象:小文件随便传,一超过几百MB就报错,或者传到一半连接被重置。这不是ASP.NET的错,而是所有HTTP上传方案都会遇到的问题,只是不同技术栈暴露出来的错误形式不同。
第一个拦路虎是请求体大小限制。在传统ASP.NET中,web.config里的httpRuntime maxRequestLength默认只有4MB,也就是说超过4MB的请求直接被拒绝。就算你把这个值调大了,IIS层还有一个maxAllowedContentLength,默认30MB,它属于requestFiltering模块,会提前拦截超大请求,返回HTTP 404.13错误。这个错误很有迷惑性,光看状态码会以为是路由找不到,实际上是请求内容超限被IIS拦截了。
第二个拦路虎是超时。ASP.NET的executionTimeout默认是90秒,如果上传一个较大文件在90秒内没有完成,服务器会主动掐断请求。即使你把客户端超时设得很长,服务器也会在底层切断连接,表现为“已收到请求头但迟迟没有响应”。
第三个拦路虎是网络抖动。即便你把大小限制和超时都调到了“无限大”,只要上传过程中路由器重启、WiFi闪断、笔记本合盖,整个文件就得从头传。4G/5G环境下,一个2GB的文件很可能连续传十几次都失败。这个问题靠调参数解决不了,必须从传输模型上改。
1.2 示例项目要解决什么:分片、并发、断点续传
既然瓶颈在于“把一个完整的文件当作单个HTTP请求体发送”,那解决办法就是不让这个请求体太大、耗时太长。我们把文件切开,一片一片传,每一片都是一个独立的小请求,即使一片失败也只需要重传这一片。
这带来三个好处。第一,单次请求体大小可控,不用担心服务器限制和超时。第二,可以并发发送多个分片,利用带宽,上传速度比串行快不少。第三,天然支持断点续传——已经传过的分片不需要再传,只要服务端记录了哪些分片已经到达,下次继续传剩余分片即可。
这个示例项目就围绕这三件事展开:前端负责切片、并发控制、进度统计;服务端负责接收分片、记录已上传分片的索引、在所有分片都到达后完成合并。不依赖任何商业控件,不需要IE的ActiveX插件,一个现代浏览器就够了。如果你还在用老旧的NTKO控件,我建议尽快脱离,那套方案在如今的前端环境里简直寸步难行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 断点续传的设计思路:不是玄学,是“分而治之”
2.1 完整文件切成N片,记录每片状态
分片其实很简单,前端用File.slice(start, end)就能把文件切成若干Blob。真正需要设计的是“如何知道哪些分片已经上传成功”。我用的方案是这样的:
- 客户端读取文件内容,计算出一个唯一标识
fileId(通常用文件MD5或者“文件大小+文件名”的组合,但MD5更可靠)。 - 把文件按固定大小(比如2MB)切成
totalCount片。 - 每一片在服务端的标识为
{fileId}_{index}.part。 - 客户端上传前先请求服务端查询
fileId已经存在哪些分片索引。 - 上传某一片时,如果服务端发现该分片已经存在且大小一致,就直接返回成功,前端跳过。
这个方案里,fileId是断点续传的抓手,index是合并时的顺序依据。只要这两个信息不丢,无论浏览器刷新了多少次、网络断了几次,都可以接着传。
2.2 前端如何记录已上传分片
有人会问:断点续传需要把“已上传分片”存在哪里?我推荐以后端记录为准,前端只负责查询和展示。因为前端存储在localStorage里容易被清掉,而且换一台电脑就完全失效。后端在接收分片时,已经在磁盘上写入了对应的.part文件,这些文件本身就是上传状态。查询接口只需扫描目录,把存在的分片编号返回给前端。
这样设计还有一个好处:多端同步。同一个用户在公司传了一半,回家打开浏览器,只要fileId相同(比如文件名+大小+内容算出的MD5一致),就能继续上传剩余分片。当然,实际项目里把fileId和用户账号绑定更严谨,但示例项目可以暂不引入权限。
2.3 上传完成后的合并策略
当客户端收到所有分片都成功的响应后,调用一个merge接口触发服务端合并。合并流程很简单:先检查所有0到totalCount-1的分片文件是否存在且完整,然后按索引顺序读出来写入同一个目标文件。
合并时最容易踩的坑是内存爆炸。如果每片都读成byte[]再拼接,100GB的文件会在合并瞬间打爆服务器内存。正确做法是用固定大小缓冲区循环读取分片并写入目标流,比如每次读4MB,写4MB。示例项目里为了简洁直接用了File.ReadAllBytes,但实际生产环境务必改成流式读写。
3. 服务端实现:基于ASP.NET Core Web API的分片接收与合并
3.1 接口设计:UploadChunk / QueryStatus / Merge
我按REST风格设计了3个接口:上传分片、查询状态、触发合并。这几个接口足够支撑断点续传和秒传的完整逻辑。
code复制POST /api/upload/chunk // 上传单个分片,multipart/form-data
GET /api/upload/status/{fileId} // 查询某个文件已上传的分片索引
POST /api/upload/merge?fileId=xx&fileName=xx&totalCount=xx // 合并
分片上传接口接收的字段必须包含:FileId、Index、TotalCount、FileName和Data文件流。Index从0开始,`TotalCou
