断点续传需客户端记录偏移量并服务端支持追加写入,HttpClient不自动管理进度;须用Content-Range头、分片上传、唯一fileId及线程安全写入。

断点续传必须自己管理上传状态,HttpClient 不会自动记住进度
很多人以为用 HttpClient 发个 POST 就能续传,其实它连“上次传到哪了”都不管。断点续传的核心是:你得在客户端记录已上传的字节偏移量,并在服务端支持从指定位置追加写入。否则网络一抖、页面一刷,就得重头来。
常见错误现象:HttpRequestException 后直接重试整个文件;服务端返回 200 OK 但文件内容重复或错乱;上传进度条跳变甚至倒退。
- 上传前先调用服务端
/upload/status?fileId=xxx获取已上传大小(服务端需持久化该值) - 用
FileStream的Seek(offset, SeekOrigin.Begin)跳过已传部分,再读取剩余数据 - 每次上传分片建议控制在 2–10 MB,太小增加 HTTP 开销,太大重试成本高
- 别用
MemoryStream加载整个大文件——OutOfMemoryException很快找上门
HttpRange 和 Content-Range 是关键协议字段,但 C# 默认不生成
C# 的 HttpClient 不会自动加 Content-Range 头,也不解析 Accept-Ranges 响应。断点续传依赖这个头告诉服务端:“我要传第 1048576 到 2097151 字节”。服务端也得按 RFC 7233 返回 206 Partial Content 或 200 OK 并带 Content-Range。
使用场景:浏览器原生支持 fetch + Range 请求,但 C# 客户端要手动构造。
- 上传时必须显式设置请求头:
request.Headers.Add("Content-Range", "bytes 1048576-2097151/10485760") - 服务端收到后,要用
FileStream以FileMode.OpenOrCreate和FileAccess.Write打开文件,并Seek(offset, SeekOrigin.Begin) - 注意
Content-Length必须等于本次上传的字节数(不是总大小),否则服务端可能截断或拒绝
分片上传的 fileId 必须全局唯一且稳定,不能靠文件名推导
用户改个文件名、重命名再上传,如果还用 Path.GetFileName() 当 fileId,服务端就认不出这是同一个文件,断点信息全丢。更糟的是多个用户传同名文件,状态互相覆盖。
性能影响:用 MD5/SHA256 计算文件哈希作 fileId 看似稳妥,但大文件首次上传前就得完整读一遍——卡顿明显。
- 推荐方案:客户端生成 UUID 作为临时
fileId,首次上传前先POST /upload/init携带文件元信息(大小、名称、哈希前 N 字节),服务端存并返回确认 ID - 哈希不必全量计算:取开头 64KB + 结尾 64KB 做
SHA256,平衡唯一性与速度 - 服务端需清理超时未完成的
fileId状态(如 24 小时无更新)
并发上传分片时,FileStream 写入必须加锁或用独立文件句柄
多个线程同时往同一个 FileStream 写,哪怕都 Seek 到不同位置,也会因缓冲区、系统缓存或 NTFS 元数据竞争导致内容错位。Windows 下尤其容易出现“部分字节丢失”或“末尾多出零字节”。
容易踩的坑:用 lock(fileStream) —— 错,FileStream 不是线程安全的,且锁粒度太大拖慢整体速度。
- 每个分片上传任务单独打开
FileStream,用FileMode.OpenOrCreate+FileAccess.Write+FileShare.Read - 写入前
fs.Seek(offset, SeekOrigin.Begin),然后fs.Write(buffer, 0, length),最后fs.Flush() - 服务端合并分片时,不要简单
File.AppendAllBytes(),而要用FileStream按序读各分片再顺序写目标文件
C# 断点续传最易被忽略的点是:服务端状态存储和客户端偏移量校验必须严格一致。比如客户端说“我已传 10MB”,服务端磁盘上对应文件却只有 9.8MB,后续所有续传都会错位——这种问题往往上线后才暴露,且难以复现。


















