HttpClient.DownloadAsync会因内存溢出禁用,大文件须用GetAsync+ResponseHeadersRead+流式分块下载,注意异常处理、进度计算、取消令牌传递、连接复用及DNS缓存。

HttpClient.DownloadAsync 会吃光内存,别直接用
它底层把整个响应体读进 byte[] 再返回,大文件(比如 500MB 的安装包)一调就 OOM。真要用,必须确认文件小且可控——比如下载几 KB 的 JSON 配置。
实际项目里,得自己控制流、分块读、边下边写磁盘。核心是用 HttpClient.GetAsync + HttpCompletionOption.ResponseHeadersRead,拿到 HttpResponseMessage 后立刻读取 Content.ReadAsStreamAsync(),再用 Stream.CopyToAsync 或手动循环 ReadAsync/WriteAsync。
- 必须设
response.EnsureSuccessStatusCode(),否则 4xx/5xx 状态码也会返回流,但内容是错误页 - 记得在
using块里释放HttpResponseMessage和响应流,不然连接池卡死 - 不要用
Stream.Length判断总大小——HTTP 响应可能没发Content-Length,尤其分块传输时
怎么安全获取文件总大小和实时进度
靠 HttpResponseMessage.Content.Headers.ContentLength,但它可能为 null。得先检查是否非空,再作为 long? 处理;如果为 null,就只能显示“未知大小”,进度条退化为 indeterminate 模式。
进度计算不是简单除法:要累计已写入磁盘的字节数,除以预估总大小。注意别用 Stream.Position——它在 CopyToAsync 中不可靠,尤其异步写入时。
- 用一个
long变量累加每次ReadAsync返回的实际字节数 - 更新 UI 进度前,确保在 UI 线程执行(WinForms 用
Control.Invoke,WPF 用Dispatcher.Invoke) - 避免高频更新——每 1% 或每 100ms 更新一次,别每次 read 都触发
取消下载和异常恢复的关键点
CancellationToken 必须传给 GetAsync、ReadAsync、WriteAsync 所有异步调用,漏一个就无法真正中断。
断点续传不是默认行为——HTTP 服务端必须支持 Range 请求,客户端得手动加请求头:request.Headers.Range = new RangeHeaderValue(resumedPosition, null),然后检查响应状态码是不是 206 Partial Content。别假设所有服务器都支持。
- 本地文件得用
FileMode.Append打开,并Seek到末尾位置 - 重试逻辑要区分:网络超时可重试,404 或 416(Range Not Satisfiable)就该停
- 记录已下载偏移量到临时文件或数据库,避免每次重启从头开始
HttpClient 实例要不要复用?
必须复用。每个 HttpClient 实例背后维护一个连接池,频繁 new 会导致端口耗尽(SocketException: Address already in use)。全局单例或 DI 注入是最稳妥做法。
但别在构造时设 Timeout——它不能被单次请求覆盖,会导致所有请求被同一超时约束。应该用 CancellationTokenSource.WithCancellation(timeoutMs) 控制单次请求。
- 别在 using 里 new HttpClient,这是经典反模式
- 如果需要不同 BaseAddress 或 Header,用
HttpRequestMessage设置,而不是换 client - DNS 缓存问题:长时间运行的服务建议设置
HttpClientHandler.PooledConnectionLifetime(如 2 分钟),避免 DNS 变更不生效
流式下载的难点不在代码长度,而在边界条件:服务端不守规范、网络抖动、磁盘满、用户狂点取消……这些地方多加日志,比堆炫技的进度动画重要得多。


















