最稳妥方案是用 httpclient 配合 progress 分块读取响应流:先 getasync(responseheadersread) 获取响应头,再 readasstreamasync() 得到非缓冲流,用 copytoasync 传入自维护累计字节数的 progress 实例,并在 ui 线程安全更新进度条;需处理 content-length 缺失、取消下载、异常清理等细节。

用 HttpClient 下载文件并实时更新进度条最稳妥
直接用 WebClient 虽然简单,但已标记为过时(.NET 6+),且不支持取消、超时精细控制;HttpClient 是现代首选,配合 Progress<t></t> 可自然对接 UI 进度条。关键不是“能不能”,而是“怎么让进度真正反映下载量”——必须基于响应流分块读取,不能靠 Content-Length 猜。
常见错误是调用 GetByteArrayAsync 或 GetStringAsync,它们会等整个响应体加载完才返回,进度条只能卡在 0% 或瞬间跳到 100%。
- 务必使用
GetAsync获取HttpResponseMessage,再调用Content.ReadAsStreamAsync() - 用
Stream.CopyToAsync的重载,传入IProgress<long></long>实例来报告已写入字节数 - UI 线程更新进度条必须用
Dispatcher.Invoke(WPF)或Control.Invoke(WinForms)
Progress<long></long> 怎么和实际下载字节挂钩
很多人传 new Progress<long>(v => progressBar.Value = (int)v)</long>,结果进度条乱跳甚至崩溃——因为 v 是**本次回调写入的字节数**,不是累计值。必须自己维护一个累加变量。
示例核心逻辑:
long totalBytes = 0;
var progress = new Progress<long>(bytes => {
totalBytes += bytes;
double percentage = totalBytes * 100.0 / contentLength;
progressBar.Value = (int)Math.Min(percentage, 100);
});</long>
注意:contentLength 来自 response.Content.Headers.ContentLength,但某些服务器(如分块传输、动态生成文件)可能不返回该头,此时需降级为“未知总长”,进度条改用 Marquee 模式或只显示已下载字节数。
下载大文件时内存暴涨?必须禁用响应缓冲
默认情况下 HttpClient 会把整个响应体缓存在内存里,下载 1GB 文件就吃掉 1GB 内存。这不是 bug,是设计行为。
- 创建
HttpClient时传入new HttpClientHandler { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate } - 调用
GetAsync时传入HttpCompletionOption.ResponseHeadersRead,确保响应头一到就返回,不等 Body - 后续所有流操作都基于
response.Content.ReadAsStreamAsync()返回的流,别转成MemoryStream
漏掉 ResponseHeadersRead,ReadAsStreamAsync 返回的仍是缓冲流,白忙活。
取消下载和异常处理不能只靠 try-catch
HttpRequestException 覆盖网络错误,但用户点“取消”需要主动中断。用 CancellationToken 是唯一可靠方式。
- UI 层创建
CancellationTokenSource,绑定按钮点击事件调用Cancel() -
GetAsync和CopyToAsync都要传入该 token - 捕获
OperationCanceledException并检查e.CancellationToken.IsCancellationRequested,避免把超时或连接断开误判为用户取消 - 下载失败后记得清理已创建的空文件或不完整文件,否则下次续传逻辑会混乱
进度条的真实感,取决于你是否愿意拆开 HTTP 流、亲手数每一个字节——而不是依赖某个封装好的“带进度下载”方法。服务器不给 Content-Length、网络中途断连、杀毒软件劫持下载流……这些细节才是落地时真正卡住人的地方。











