带有 HttpClient 的进度条
Posted
技术标签:
【中文标题】带有 HttpClient 的进度条【英文标题】:Progress bar with HttpClient 【发布时间】:2013-12-18 15:09:15 【问题描述】:我有一个文件下载功能:
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(url);
InMemoryRandomAccessStream randomAccessStream = new InMemoryRandomAccessStream();
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename, CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
DataWriter writer = new DataWriter(fs.GetOutputStreamAt(0));
writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());
await writer.StoreAsync();
//current.image.SetSource(randomAccessStream);
writer.DetachStream();
await fs.FlushAsync();
如何实现进度条功能? 也许我可以写到目前为止的作家字节?还是什么?
附注我无法使用 DownloadOperation(后台传输),因为来自服务器的数据请求证书 - 而且此功能在 DownloadOperations 中不存在。
【问题讨论】:
Windows.Web.Http.HttpClient
怎么样。那个支持进步。
Windows.Web.Http.HttpClient 在桌面上可用了吗?我以为它只是用于 Windows 商店应用程序。我从未见过任何人在现实生活中使用它。
Windows.Web.Http.HttpClient可以在ASP.Net中使用吗?
此代码示例是android/xamarin 独有的?
【参考方案1】:
从 .Net 4.5 开始:使用 IProgress<T>
从 .Net 4.5 开始,您可以使用 IProgress<T>
接口处理异步进度报告。您可以使用HttpClient
编写用于下载文件的扩展方法,可以像这样调用其中progress
是IProgress<float>
的实现,用于您的进度条或其他UI 内容:
// Seting up the http client used to download the data
using (var client = new HttpClient())
client.Timeout = TimeSpan.FromMinutes(5);
// Create a file stream to store the downloaded data.
// This really can be any type of writeable stream.
using (var file = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
// Use the custom extension method below to download the data.
// The passed progress-instance will receive the download status updates.
await client.DownloadAsync(DownloadUrl, file, progress, cancellationToken);
实施
此扩展方法的代码如下所示。请注意,此扩展依赖于另一个扩展,用于处理带有进度报告的异步流复制。
public static class HttpClientExtensions
public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default)
// Get the http headers first to examine the content length
using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead))
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync())
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (progress == null || !contentLength.HasValue)
await download.CopyToAsync(destination);
return;
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
// Use extension method to report progress while downloading
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(1);
带有用于真实进度报告的流扩展:
public static class StreamExtensions
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default)
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
【讨论】:
这不允许添加证书 使用HttpClientHandler
创建HttpClient
,并应用正确的证书选项,就像您在问题中所做的那样
很好的答案。我所做的唯一修改是将cancellationToken 添加到您的HttpClientExtensions DownloadAsync 方法中的GetAsync 和CopyToAsync(非进度支持)调用中。【参考方案2】:
这是一个独立的类,它将根据this SO answer 上的 TheBlueSky 和 eriksendc 上的代码进行下载并报告进度百分比this GitHub comment.
public class HttpClientDownloadWithProgress : IDisposable
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
public async Task StartDownload()
_httpClient = new HttpClient Timeout = TimeSpan.FromDays(1) ;
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
do
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
isMoreToRead = false;
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
continue;
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
while (isMoreToRead);
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
public void Dispose()
_httpClient?.Dispose();
用法:
var downloadFileUrl = "http://example.com/file.zip";
var destinationFilePath = Path.GetFullPath("file.zip");
using (var client = new HttpClientDownloadWithProgress(downloadFileUrl, destinationFilePath))
client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) =>
Console.WriteLine($"progressPercentage% (totalBytesDownloaded/totalFileSize)");
;
await client.StartDownload();
结果:
7.81% (26722304/342028776)
8.05% (27535016/342028776)
8.28% (28307984/342028776)
8.5% (29086548/342028776)
8.74% (29898692/342028776)
8.98% (30704184/342028776)
9.22% (31522816/342028776)
【讨论】:
必须删除以下内容才能获得进度更新:if (readCount % 100 == 0) 知道为什么它不加载标题总大小吗? @Nevaran 唯一的原因可能是服务器实际上并未返回该标头。尝试使用 Postman 对目标 URL 执行 GET,并查看它是否包含Content-Length
标头。
我不确定该怎么做——尽管它是我自己的 Google Drive 文件,如果这有助于解决问题
这是使用 System.web.http 还是 System.net.http ?【参考方案3】:
最好的方法是使用Windows.Web.Http.HttpClient
而不是System.Net.Http.HttpClient
。第一个支持进步。
但如果出于某种原因您想坚持使用 System.Net,则需要实施自己的进度。
删除DataWriter
,删除InMemoryRandomAccessStream
并将HttpCompletionOption.ResponseHeadersRead
添加到GetAsync
调用,这样它会在收到标头后立即返回,而不是在收到整个响应时返回。即:
// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
url,
HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename,
CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
// Read from the web.
IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
buffer = await inputStream.ReadAsync(
buffer,
buffer.Capacity,
InputStreamOptions.None);
if (buffer.Length == 0)
// There is nothing else to read.
break;
// Report progress.
totalBytesRead += buffer.Length;
System.Diagnostics.Debug.WriteLine("Bytes read: 0", totalBytesRead);
// Write to file.
await fs.WriteAsync(buffer);
inputStream.Dispose();
fs.Dispose();
【讨论】:
谢谢,这样就可以了,但是有没有办法获得我将收到的总字节数?设置 ProgressBar 的最大值 为什么不使用 ProgressMessageHandler msdn.microsoft.com/en-us/library/… ? @thund 你会在这里找到处理程序和相关的类nuget.org/packages/Microsoft.AspNet.WebApi.Client/5.0.0 您可以使用PostAsync
和System.Net.Http.StreamContent
。您使用自己实现的流初始化StreamContent
,每次在流中调用 Stream.Read() 时,您都可以获得近似上传进度,因为您知道已读取了多少数据。
您应该通过查看响应中的 Content-Length
标头来获取总字节数。即:response.Content.Headers.ContentLength
【参考方案4】:
以下代码显示了必须针对 HttpClient
api 执行的操作以获取下载进度的最小示例。
HttpClient client = //...
// Must use ResponseHeadersRead to avoid buffering of the content
using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
// You must use as stream to have control over buffering and number of bytes read/received
using (var stream = await response.Content.ReadAsStreamAsync())
// Read/process bytes from stream as appropriate
// Calculated by you based on how many bytes you have read. Likely incremented within a loop.
long bytesRecieved = //...
long? totalBytes = response.Content.Headers.ContentLength;
double? percentComplete = (double)bytesRecieved / totalBytes;
// Do what you want with `percentComplete`
上面并没有告诉你如何处理流,如何报告过程,或者尝试提供对原始问题中的代码的直接解决方案。但是,对于希望在他们的代码中应用进步的未来读者来说,这个答案可能更容易获得。
【讨论】:
尽管有点不完整,但这是我能找到的唯一有效答案。 Windows.Web.Http 仅适用于 Windows,它也是一个 UWP 库,基本上是 .net 的噩梦反乌托邦版本【参考方案5】:与上述@René Sackers 解决方案相同,但添加了取消下载的功能
class HttpClientDownloadWithProgress : IDisposable
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private readonly CancellationToken? _cancellationToken;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, CancellationToken? cancellationToken = null)
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
_cancellationToken = cancellationToken;
public async Task StartDownload()
_httpClient = new HttpClient Timeout = TimeSpan.FromDays(1) ;
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
do
int bytesRead;
if (_cancellationToken.HasValue)
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, _cancellationToken.Value);
else
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
isMoreToRead = false;
continue;
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
while (isMoreToRead);
//the last progress trigger should occur after the file handle has been released or you may get file locked error
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
public void Dispose()
_httpClient?.Dispose();
【讨论】:
【参考方案6】:René Sackers 版本非常好,但可能会更好。具体来说,它有一个微妙的竞争条件,由在流关闭之前触发 TriggerProgressChanged 引起。修复方法是在显式处理流后触发事件。下面的版本包含了上述更改,继承自 HttpClient 并添加了对取消令牌的支持。
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public class HttpClientWithProgress : HttpClient
private readonly string _DownloadUrl;
private readonly string _DestinationFilePath;
public event ProgressChangedHandler ProgressChanged;
public HttpClientWithProgress(string downloadUrl, string destinationFilePath)
_DownloadUrl = downloadUrl;
_DestinationFilePath = destinationFilePath;
public async Task StartDownload()
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
public async Task StartDownload(CancellationToken cancellationToken)
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
await DownloadFileFromHttpResponseMessage(response);
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
long totalBytesRead = 0L;
long readCount = 0L;
byte[] buffer = new byte[8192];
bool isMoreToRead = true;
using (FileStream fileStream = new FileStream(_DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
do
int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
isMoreToRead = false;
continue;
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
while (isMoreToRead);
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
【讨论】:
【参考方案7】:这是我对 René Sackers 答案的变体。主要区别:
更实用的风格。 只有一种方法而不是整个对象。 可以取消下载 public async static Task Download(
string downloadUrl,
string destinationFilePath,
Func<long?, long, double?, bool> progressChanged)
using var httpClient = new HttpClient Timeout = TimeSpan.FromDays(1) ;
using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using var contentStream = await response.Content.ReadAsStreamAsync();
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
static double? calculatePercentage(long? totalDownloadSize, long totalBytesRead) => totalDownloadSize.HasValue ? Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2) : null;
using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
do
var bytesRead = await contentStream.ReadAsync(buffer);
if (bytesRead == 0)
isMoreToRead = false;
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
throw new OperationCanceledException();
continue;
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesRead += bytesRead;
readCount++;
if (readCount % 100 == 0)
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
throw new OperationCanceledException();
while (isMoreToRead);
可以这样调用:
// Change this variable to stop the download
// You can use a global variable or some kind of state management
var mustStop = false;
var downloadProgress = (long? _, long __, double? progressPercentage) =>
if (progressPercentage.HasValue)
progressBar.Value = progressPercentage.Value;
// In this example only the variable is checked
// You could write other code that evaluates other conditions
return mustStop;
;
SomeClass.Download("https://example.com/bigfile.zip", "c:\downloads\file.zip", downloadProgress);
【讨论】:
如何在进度条上调用和更新此方法?谢谢! 编辑答案以包含使用示例【参考方案8】:嗯,您可以让另一个线程检查正在写入的流的当前大小(您还可以将预期的文件大小传递给它),然后相应地更新进度条。
【讨论】:
如果你能提供一个例子我会很高兴 很遗憾我没有 Win8 副本,所以我无法测试您的功能。然而,如果你想让事情变得相当简单,你可以使文件名和文件大小全局化,让一个带有循环和线程睡眠的后台工作人员定期检查文件大小,并更新进度条。然而,这不是一个非常优雅的解决方案。 我认为这行不通。在ReadAsByteArrayAsync
返回之前,不会向流中写入任何内容。【参考方案9】:
这是 René Sackers 答案的修改版本,具有以下功能更改:
http 客户端没有被释放(因为它不应该被释放) 更好的进度处理 回调创建httpRequest(自定义标头支持) 利用 ArrayPool 减少内存占用 自动事件订阅+取消订阅以防止事件处理程序导致内存泄漏用法:
await DownloadWithProgress.ExecuteAsync(HttpClients.General, assetUrl, downloadFilePath, progressHandler, () =>
var requestMessage = new HttpRequestMessage(HttpMethod.Get, assetUrl);
requestMessage.Headers.Accept.TryParseAdd("application/octet-stream");
return requestMessage;
);
我想我不是唯一需要自定义标题的人,所以我想我会分享这个重写
实施:
public delegate void DownloadProgressHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public static class DownloadWithProgress
public static async Task ExecuteAsync(HttpClient httpClient, string downloadPath, string destinationPath, DownloadProgressHandler progress, Func<HttpRequestMessage> requestMessageBuilder = null)
requestMessageBuilder ??= GetDefaultRequestBuilder(downloadPath);
var download = new HttpClientDownloadWithProgress(httpClient, destinationPath, requestMessageBuilder);
download.ProgressChanged += progress;
await download.StartDownload();
download.ProgressChanged -= progress;
private static Func<HttpRequestMessage> GetDefaultRequestBuilder(string downloadPath)
return () => new HttpRequestMessage(HttpMethod.Get, downloadPath);
internal class HttpClientDownloadWithProgress
private readonly HttpClient _httpClient;
private readonly string _destinationFilePath;
private readonly Func<HttpRequestMessage> _requestMessageBuilder;
private int _bufferSize = 8192;
public event DownloadProgressHandler ProgressChanged;
public HttpClientDownloadWithProgress(HttpClient httpClient, string destinationFilePath, Func<HttpRequestMessage> requestMessageBuilder)
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
_requestMessageBuilder = requestMessageBuilder ?? throw new ArgumentNullException(nameof(requestMessageBuilder));
public async Task StartDownload()
using var requestMessage = _requestMessageBuilder.Invoke();
using var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
await DownloadAsync(response);
private async Task DownloadAsync(HttpResponseMessage response)
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, true))
do
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
isMoreToRead = false;
ReportProgress(totalDownloadSize, totalBytesRead);
continue;
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
ReportProgress(totalDownloadSize, totalBytesRead);
while (isMoreToRead);
ArrayPool<byte>.Shared.Return(buffer);
private void ReportProgress(long? totalDownloadSize, long totalBytesRead)
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
【讨论】:
经过 7 年的提问 - 这仍然是实际的 =)以上是关于带有 HttpClient 的进度条的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Apache HttpClient 4 获取文件上传的进度条?
Android学习笔记--使用Apache HttpClient实现网络下载效果,附带进度条显示