断点续传需服务端支持range请求,客户端用httpclient.sendasync手动发带range头的get请求,配合filestream.seek安全追加写入,并通过.downloading.json持久化进度;若服务端不支持则降级或报错。

如何用 HttpClient 发起带范围请求(Range)的下载
断点续传本质是服务端支持 Range 请求,客户端主动带上 Range: bytes=xxx- 头。C# 中必须用 HttpClient 手动构造请求,WebClient 不支持自定义头且无法复用连接。
关键点:不能直接调用 GetAsync 然后读响应流——那样会触发完整响应头校验,服务端若不返回 206 Partial Content 就失败。必须用 SendAsync + HttpRequestMessage 控制全过程。
- 先用
HEAD请求获取文件总大小(Content-Length)和是否支持断点(看响应是否有Accept-Ranges: bytes) - 本地检查已存在文件长度,设为
resumePos;若为 0 则走完整下载 - 构造
HttpRequestMessage,Method 设为GET,Headers.Add("Range", $"bytes={resumePos}-") - 用
HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead),避免提前读取 body 导致连接卡住
FileStream 写入时如何安全追加而不覆盖
续传不是“重新写文件”,而是从指定位置开始写入新数据。直接 File.Open(... FileMode.Append) 会把光标移到末尾,但无法控制偏移量;必须用 FileMode.OpenOrCreate + Seek。
常见错误:打开文件后没调用 Seek 就写,结果从开头覆盖;或用了 FileMode.Create 把已有内容全清空。
- 用
new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous) - 写入前必须
fs.Seek(resumePos, SeekOrigin.Begin),否则默认从 0 开始 - 务必检查
fs.Length是否等于resumePos,防止文件被外部修改导致错位 - 写完别忘了
fs.SetLength(finalSize)(如果服务端返回的Content-Range明确了总长度)
怎么持久化记录已下载进度(避免重启丢状态)
只靠文件长度不可靠:用户可能手动删改、磁盘满、程序崩溃时写了一半。必须单独存一个轻量元数据文件,比如 xxx.downloading.json。
这个文件要包含:url、localPath、totalSize(从 HEAD 得到)、downloaded(当前字节数)、lastModified(服务端 ETag 或 Last-Modified,用于校验文件是否变更)。
- 每次写入数据后,立即原子更新元数据文件:先写临时文件
.xxx.tmp,再File.Replace替换原文件 - 启动续传前,先读元数据,再验证
localPath文件是否存在且长度匹配downloaded,再校验ETag是否一致 - 不要用数据库或注册表——太重,且跨平台部署时路径/权限易出问题
- 元数据文件建议和目标文件同目录,命名加
.downloading后缀,下载完成立刻删除
为什么有时收到 200 而不是 206?服务端不配合怎么办
不是所有 HTTP 服务器都默认支持 Range。Nginx 默认开,IIS 需启用静态内容功能,而某些 CDN 或自研接口可能直接忽略 Range 头并返回完整 200 响应——这时你得自己截断数据,否则文件会重复叠加。
必须检查响应状态码和 Content-Range 头。如果收到 200,但你发了 Range,说明服务端不支持,只能降级为完整下载(或报错提示用户)。
- 响应中没有
Content-Range头 → 强制终止,不能继续写 - 有
Content-Range但格式不对(如bytes */12345)→ 表示服务端知道范围但拒绝本次请求,可能是认证过期或防盗链失败 - 响应
Content-Length和Content-Range的 end 值不一致 → 数据被中间代理篡改,停止续传 - 某些云存储(如早期阿里 OSS)需在 URL 加签名参数才支持 Range,单纯加 Header 没用
最麻烦的是服务端静默忽略 Range —— 它既不返回 206,也不报错,就当普通请求处理。这种只能靠对比首次 HEAD 的 Content-Length 和实际下载字节数来发现异常。











