如何用HttpClient实现流畅下载进度与速度统计并解决卡顿问题?
问题解决方案
一、在ProgressChanged事件中实现下载统计(速度、已下载、总大小)
在HttpClientDownloadWithProgress类中维护状态变量,结合时间差计算下载速度,同时从响应头提取文件总大小:
- 添加类内状态字段:
private long _lastBytesDownloaded; private DateTime _lastProgressTime; private long _totalBytes; - 开始下载时获取文件总大小:
var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); _totalBytes = response.Content.Headers.ContentLength ?? -1; // 无法获取则标记为-1 - 读取流时计算进度与速度,控制更新频率避免UI频繁刷新:
_lastProgressTime = DateTime.Now; _lastBytesDownloaded = 0; var buffer = new byte[32768]; // 32KB缓冲区,减少IO次数 int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await fileStream.WriteAsync(buffer, 0, bytesRead); _downloadedBytes += bytesRead; var currentTime = DateTime.Now; var timeDiff = currentTime - _lastProgressTime; // 每100ms更新一次进度,平衡实时性与性能 if (timeDiff.TotalMilliseconds >= 100) { var bytesDiff = _downloadedBytes - _lastBytesDownloaded; var speedBytesPerSec = bytesDiff / timeDiff.TotalSeconds; var speedKbPerSec = speedBytesPerSec / 1024; var speedMbPerSec = speedKbPerSec / 1024; ProgressChanged?.Invoke(new DownloadProgress { TotalBytes = _totalBytes, DownloadedBytes = _downloadedBytes, SpeedBytesPerSecond = speedBytesPerSec, SpeedKbPerSecond = speedKbPerSec, SpeedMbPerSecond = speedMbPerSec, ProgressPercentage = _totalBytes > 0 ? (double)_downloadedBytes / _totalBytes * 100 : -1 }); _lastBytesDownloaded = _downloadedBytes; _lastProgressTime = currentTime; } } // 下载完成后触发最后一次进度更新 ProgressChanged?.Invoke(new DownloadProgress { TotalBytes = _totalBytes, DownloadedBytes = _downloadedBytes, SpeedBytesPerSecond = 0, ProgressPercentage = 100 }); - 定义进度数据类:
public class DownloadProgress { public long TotalBytes { get; set; } public long DownloadedBytes { get; set; } public double SpeedBytesPerSecond { get; set; } public double SpeedKbPerSecond { get; set; } public double SpeedMbPerSecond { get; set; } public double ProgressPercentage { get; set; } }
二、修复下载卡顿问题
浏览器下载流畅但代码卡顿,通常是以下原因导致,对应修复方案:
- 缓冲区过小:将读取缓冲区从默认4KB改为32KB或64KB,减少IO交互次数(代码已在上一步体现)
- 系统代理干扰:禁用自动代理并启用GZIP压缩:
var handler = new HttpClientHandler { UseProxy = false, Proxy = null, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; var httpClient = new HttpClient(handler); - TCP连接频繁重建:添加保持连接请求头:
httpClient.DefaultRequestHeaders.Connection.Add("keep-alive"); - UI线程阻塞:确保下载全程用
await异步调用,绝对不要在UI线程使用Wait()或Result
三、修复进度条与Label不同步问题
进度更新事件在后台线程触发,直接操作UI控件会导致跨线程异常,必须切换到UI线程更新:
private void OnProgressChanged(DownloadProgress progress) { if (InvokeRequired) { Invoke(new Action<DownloadProgress>(OnProgressChanged), progress); return; } // 更新进度条 if (progress.TotalBytes > 0) { progressBar1.Maximum = (int)progress.TotalBytes; progressBar1.Value = (int)Math.Min(progress.DownloadedBytes, progress.TotalBytes); // 避免溢出 } // 更新Label显示 var downloadedSize = FormatFileSize(progress.DownloadedBytes); var totalSize = progress.TotalBytes > 0 ? FormatFileSize(progress.TotalBytes) : "未知"; var speedText = progress.SpeedMbPerSecond > 1 ? $"{progress.SpeedMbPerSecond:F2} MB/s" : $"{progress.SpeedKbPerSecond:F2} KB/s"; label1.Text = $"已下载: {downloadedSize}/{totalSize} | 速度: {speedText}"; } // 字节数转可读格式辅助方法 private string FormatFileSize(long bytes) { return bytes switch { < 1024 => $"{bytes} B", < 1024 * 1024 => $"{bytes / 1024:F2} KB", < 1024 * 1024 * 1024 => $"{bytes / (1024 * 1024):F2} MB", _ => $"{bytes / (1024 * 1024 * 1024):F2} GB" }; }
额外注意事项
- 不要过于频繁触发进度更新(建议100-200ms一次),避免UI线程过载
- 处理
Content-Length为null的情况(如动态生成文件),此时进度百分比显示为"未知"即可 - 复用
HttpClient实例,不要每次下载都创建新实例,避免端口耗尽
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

