HttpClient分片下载需手动设置Range头、预分配文件空间并Seek写入、调大连接限制、用ResponseHeadersRead避免内存溢出、本地记录进度实现断点续传。

用 HttpClient 发起多个 Range 请求必须手动设置 Range 头
HTTP 分片下载本质是并发发多个带 Range 头的 GET 请求,每个请求只取文件一段。C# 的 HttpClient 不会自动帮你拆分或加头,得自己算字节范围、手动塞进 HttpRequestMessage。
常见错误是直接对同一 URL 开多个 GetAsync 却没设 Range,结果全下完整文件,白忙活。
- 先用 HEAD 请求拿到
Content-Length,确认文件总大小 - 按线程数等分区间,比如 100MB 文件开 4 线程 → 每段 25MB,起始偏移分别是 0、25_000_000、50_000_000、75_000_000
- 构造
HttpRequestMessage,用Headers.Add("Range", "bytes=0-24999999")(注意是闭区间) - 务必检查响应状态码是
206 Partial Content,不是200—— 否则说明服务器不支持分片或头没生效
FileStream 写入多段时必须用 Seek 定位,不能顺序追加
各线程下载完自己的数据块后,得写回同一个文件的对应位置。如果都用 FileMode.Append,所有数据会挤在文件末尾,顺序全乱。
正确做法是打开文件时用 FileMode.Create 预分配空间(SetLength),再用 Seek 跳到指定 offset 写入。
- 提前创建空文件:
using var fs = new FileStream(path, FileMode.Create); fs.SetLength(totalSize); - 每个线程拿到自己的
offset后,用fs.Seek(offset, SeekOrigin.Begin)定位 - 写入前加锁或用
FileStream自带的线程安全写法(如传FileOptions.Asynchronous+WriteAsync) - 别用
StreamWriter—— 它是文本层,会编码转换,破坏二进制数据
并发数不是越多越好,HttpClient 默认连接限制会卡死
默认 HttpClient 通过 ServicePointManager.DefaultConnectionLimit 控制最大并发连接数(.NET Framework 是 2,.NET Core/5+ 是 10)。开 20 个线程发请求,可能有 10 个一直阻塞在连接池排队。
现象是:前几个分片很快完成,后面十几个长时间无响应,甚至超时。
- 改连接限制:
ServicePointManager.DefaultConnectionLimit = 100;(仅 .NET Framework) - .NET Core 及以后用
HttpClientHandler.MaxConnectionsPerServer,初始化HttpClient时传入 - 更稳妥的是复用一个
HttpClient实例(它本就是线程安全的),别每个线程 new 一个 - 观察
HttpCompletionOption.ResponseHeadersRead—— 下载大文件时设这个,避免把整个响应体缓存在内存里
断点续传要靠本地记录已下载区间,别依赖服务器返回的 Content-Range
服务器返回的 Content-Range: bytes 0-24999999/104857600 只反映本次响应范围,不表示“前面那段我已经下好了”。真正断点续传得自己维护一个已成功写入的区间列表(比如存 JSON 到临时文件)。
否则程序中断重跑,又从头开始下,浪费带宽还拖慢整体进度。
- 每次成功写入一段后,把
[start, end]记进本地.progress.json - 启动时读这个文件,跳过已存在的区间,只发起缺失部分的
Range请求 - 注意文件系统写入可能不原子,建议用
File.Replace更新进度文件 - 别用
DateTime或文件大小判断是否完成 —— 大小可能因压缩、重定向变化,不可靠
分片下载真正的麻烦点不在并发本身,而在如何让每段数据准确落到文件该去的位置、以及怎么扛住网络波动和进程意外退出。细节错一点,文件就损坏,且很难排查。









