Range 请求必须手动设置 Range
头,HttpClient 默认不发
HttpClient 不会自动拆分请求或加
Range头——哪怕你传了
HttpCompletionOption.ResponseHeadersRead。它只管发完整 GET,服务器返回 200 就完事。要实现分块下载,你得自己构造多个独立的
HttpRequestMessage,每个都显式设置
Range头,比如
"bytes=0-1048575"。
常见错误是误以为用
WebClient或
HttpClient.SendAsync加个参数就能“启用范围下载”,结果抓包一看全是 200,压根没触发 206 Partial Content。 必须用
HttpRequestMessage+
client.SendAsync()手动发请求
Range值格式严格:
"bytes={start}-{end}",不能多空格、不能缺单位、不能写成 "0~1024"服务器必须支持(响应头含
Accept-Ranges: bytes),否则返回 416 或 200 全量内容
分块大小不是越大越好,1–4MB
是多数场景的平衡点
单块太大(如 64MB),内存占用高、失败重试成本大、网络抖动时容易超时;太小(如 64KB),HTTP 头开销占比飙升,线程/连接调度压力大,实际吞吐反而下降。实测在千兆局域网和普通云对象存储上,
2MB分块通常比
512KB快 15–30%,又比
8MB更稳。
注意:分块大小需对齐文件系统或存储服务的最小读单元(如某些 CDN 要求 512KB 对齐),否则可能触发额外 IO 或降速。
起始偏移必须是整数,end可等于文件总长减 1(最后一块) 避免让块边界落在压缩流或加密块中间(如 ZIP 内部结构、AES-CBC 段),否则解压/解密失败 用
FileInfo.Length获取总大小前,先确认服务器返回了
Content-Length或通过 HEAD 请求预取
并发控制别硬写 Task.WhenAll
,用 SemaphoreSlim
限流更可靠
直接扔几十个
Task.Run(() => DownloadChunk(...))进
Task.WhenAll,极易打爆连接池、触发服务器限流(429)、或耗尽本地端口(
SocketException: Only one usage of each socket address...)。.NET 的
HttpClient连接池默认只允许 2–6 个并发连接(取决于 .NET 版本和配置)。
正确做法是用
SemaphoreSlim控制同时进行的请求数(建议 4–8),并复用同一个
HttpClient实例。 不要为每个分块 new 一个
HttpClient,会泄漏连接
SemaphoreSlim.WaitAsync()放在发送请求前,不是在回调里 记得 await
SemaphoreSlim.Release(),即使下载失败也要释放 超时统一设在
HttpRequestMessage.Properties["StartTime"]或用
CancellationTokenSource管理
合并文件时顺序错乱是高频坑,别依赖 Task
完成顺序
分块下载完成后,各
Task返回顺序完全不确定。如果按完成先后往文件里
Write,最终文件就是乱的——开头可能是第 5 块,中间夹着第 1 块。必须按逻辑偏移位置写入,而不是按执行顺序。
最简方案:每个分块下载完,把
byte[]和对应
startOffset存进线程安全集合(如
ConcurrentDictionary<long byte></long>),全部完成后遍历 offset 升序合并;更省内存的做法是打开
FileStream并用
fileStream.Position = startOffset定位写入。 写入前检查
FileStream.CanSeek == true,某些流(如
NetworkStream)不支持 用
fileStream.Write(byteArray, 0, byteArray.Length),别用
WriteAsync配合
Position——异步写入时
Position可能被其他线程改 最后一块长度可能小于分块大小,以响应头
Content-Range中的
*/{total} 为准,别信 byteArray.Length分块下载真正的复杂点不在发请求,而在错误恢复:断点续传需要记录已成功写入的偏移,而部分失败的块重试时,得确保不会覆盖已写正确的内容。这个状态管理很容易被忽略,但一旦网络不稳定,就变成反复下载、反复覆盖、文件损坏。
