用 HttpClient 分片下载需复用实例、设 Range 头为 bytes=start-end、检查 206 状态码;响应流须 FileStream 偏移写入,禁用 AppendAllBytes;多线程应每分片独用 FileStream,避免竞态。

怎么用 HttpClient 发起带 Range 头的分片请求
分片下载本质就是让服务器只返回文件某一段字节,关键在发请求时加 Range 请求头。C# 里最稳妥的方式是用 HttpClient 手动设置,而不是依赖 DownloadFileAsync 这类黑盒方法——它不支持断点续传或自定义范围。
实操要点:
-
HttpClient实例必须复用(别每次新建),否则可能触发连接池耗尽或 TIME_WAIT 爆增 - 务必设置
client.DefaultRequestHeaders.Range,不是拼字符串到Headers.Add;后者容易被忽略或覆盖 - 范围格式严格为
bytes=start-end(如bytes=0-1023),end是包含的,且不能超过文件总大小(否则服务器返回 416) - 响应状态码必须检查:成功是
206 Partial Content,不是200;收到200说明服务器没支持分片,得 fallback
示例片段:
client.DefaultRequestHeaders.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 1023);
如何安全读取 HttpContent 的原始字节并写入文件偏移位置
拿到响应后,不能直接 content.ReadAsStringAsync() 或 ReadAsByteArrayAsync()——它们会把整个流缓存进内存,大文件直接 OOM。必须用流式读取 + FileStream 的 Position 或 Write 偏移写入。
常见错误现象:
- 用
File.AppendAllBytes:它每次打开文件追加,无法写到指定 offset,且频繁打开关闭损耗大 - 用
FileStream.Write但没提前设置Position:字节全堆在文件开头,覆盖已有内容 - 没处理
HttpContent.ReadAsStreamAsync()返回的流是否支持Seek:HTTP 响应流通常不支持,所以只能顺序读、顺序写
正确做法是:打开 FileStream 时用 FileMode.Open + FileAccess.Write,再调 fs.Position = startOffset,然后 await stream.CopyToAsync(fs)。
怎么判断服务器是否真正支持分片(Accept-Ranges 不可靠)
Accept-Ranges: bytes 只是声明“可能支持”,不代表本次请求真能分片。很多 CDN 或 Nginx 配置漏掉 add_header Accept-Ranges bytes,或者对某些路径禁用了范围请求。
更可靠的验证方式是实际发一个试探请求:
- 先 HEAD 请求目标 URL,看响应头有没有
Accept-Ranges,有则继续 - 再发一个真实
Range: bytes=0-0的 GET 请求,检查状态码是否为206且响应体长度为1 - 如果返回
200或416,说明不支持,得降级为整包下载
注意:某些服务器(如 IIS 默认配置)对小范围(如 0-0)会静默转成 200,所以最好试 0-1023 并比对 Content-Range 头是否匹配。
多线程并发分片时,FileStream 写入为什么崩了
多个 Task 同时往同一个 FileStream 写,即使各自写不同 offset,也会因底层缓冲、Position 竞态、或 Windows 文件锁机制出错——典型报错是 System.IO.IOException: The process cannot access the file 或写入错位。
根本原因不是“没加锁”,而是 FileStream 本身不是线程安全的写入对象。解决方案只有两个:
- 每个分片用独立的
FileStream(FileMode.Open+FileAccess.Write+fs.Position = offset),写完立刻Dispose;开销可控,且无竞争 - 用单个
FileStream但配全局object锁 +Position设置 +Write,性能差,还容易死锁,不推荐
额外提醒:别用 MemoryStream 缓冲全部分片再统一写——又回到内存爆炸的老路。
分片逻辑本身不难,难的是 HTTP 层的容错(比如 206 没返回却假装成功)、文件系统层的原子写(尤其断电 / 崩溃时部分分片已落盘)、以及多线程下磁盘 IO 的实际吞吐未必随线程数线性增长。这些地方不埋日志、不加重试、不验 MD5,上线后第一波并发就露馅。