使用HttpClient实现进度条

57

我有一个文件下载器函数:

        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怎么样?它支持进度。 - kiewic
5
Windows.Web.Http.HttpClient在桌面应用上是否已经可用?我觉得它只适用于Windows存储应用程序,而实际生活中我从未见过有人使用它。 - thund
Windows.Web.Http.HttpClient 可以在 ASP.Net 中使用吗? - tatigo
这个代码示例是专门针对Android / Xamarin的吗? - Ivandro Jao
11个回答

57
从 .Net 4.5 开始:使用 IProgress 自从 .Net 4.5 版本以后,你可以使用 IProgress 接口来处理异步进度报告。你可以编写一个扩展方法来使用 HttpClient 下载文件,可以像下面这样调用,其中 progress 是你进度条或其他 UI 元素的 IProgress 实现:
// 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(cancellationToken)) {

                // 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);
        }
    }
}

这不允许添加证书。 - Developer
5
请使用正确的证书选项创建HttpClient,如您在问题中所做的那样,使用HttpClientHandler - Bruno Zell
1
很好的答案。我所做的唯一修改是在 HttpClientExtensions 中的 DownloadAsync 方法中添加 cancellationToken 到 GetAsync 和 CopyToAsync (非进度支持) 调用。 - Dave Jellison
1
有一个更简单的方法来完成这个任务:https://dev59.com/J2Ij5IYBdhLWcg3wEhUA#72481872 - caesay
我曾经遇到一个问题,即progress.Report过于频繁地触发,导致我的应用程序变慢,这篇文章帮助了我:https://dev59.com/x2Ik5IYBdhLWcg3wBqCW#19661381 - Bluescreenterror

50
这是一个自包含的类,它将执行下载操作,并基于TheBlueSky此 SO 答案中的代码以及eriksendc此 GitHub 评论中的代码返回进度百分比。
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)

1
为了获取进度更新,必须删除以下内容: 如果(readCount%100 == 0) - aherrick
你有任何想法为什么它没有加载头部的总大小吗? - Nevaran
@Nevaran 唯一的原因可能是服务器实际上没有返回该标头。尝试使用Postman对目标URL进行GET请求,并查看是否包含“Content-Length”标头。 - René Sackers
我不确定如何做到这一点 - 不过如果这有助于解决问题,那么这是来自我的Google Drive的文件。 - Nevaran
这是使用 System.Web.Http 还是 System.Net.Http? - gumuruh
显示剩余2条评论

37

最佳选择是使用Windows.Web.Http.HttpClient而不是System.Net.Http.HttpClient。前者支持进度。

但如果出于某些原因您想坚持使用System.Net,则需要实现自己的进度。

删除DataWriter,删除InMemoryRandomAccessStreamGetAsync调用中添加HttpCompletionOption.ResponseHeadersRead,以便在接收到标头时立即返回,而不是在整个响应接收完毕后再返回。例如:

// 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();

2
谢谢,这样可以了,但是有没有办法获取我将接收的总字节数?以设置ProgressBar的最大值。 - Developer
7
为什么不使用ProgressMessageHandler http://msdn.microsoft.com/en-us/library/system.net.http.handlers.progressmessagehandler(v=vs.118).aspx? - Darrel Miller
1
@thund,您可以在此处找到处理程序和相关类:http://www.nuget.org/packages/Microsoft.AspNet.WebApi.Client/5.0.0 - Darrel Miller
2
您可以使用 PostAsyncSystem.Net.Http.StreamContent。您可以使用自己实现的流初始化 StreamContent,并且每次在流中调用 Stream.Read() 时,您都可以获得一个近似的上传进度,因为您知道已读取多少数据。 - kiewic
4
你可以通过查看响应的 Content-Length 标头来获取总字节数。例如:response.Content.Headers.ContentLength - kiewic
显示剩余4条评论

10
实现上传和下载的最简单方法是使用ProgressMessageHandler,它在Microsoft.AspNet.WebApi.Client nuget软件包中提供。
注意:这个库最初名为System.Net.Http.Formatting,并且已更名Microsoft.AspNet.WebApi.Client。但是,这个库与ASP.Net无关,可以被任何寻找微软扩展HttpClient官方库的项目所使用。源代码在此处可用。 示例:
var handler = new HttpClientHandler() { AllowAutoRedirect = true };
var ph = new ProgressMessageHandler(handler);

ph.HttpSendProgress += (_, args) =>
{
    Console.WriteLine($"upload progress: {(double)args.BytesTransferred / args.TotalBytes}");
};

ph.HttpReceiveProgress += (_, args) =>
{
    Console.WriteLine($"download progress: {(double)args.BytesTransferred / args.TotalBytes}");
};

var client = new HttpClient(ph);
await client.SendAsync(...);

请注意,如果上传的是字节数组,则不会报告进度。请求消息内容必须是流。

但是,正如您所看到的,这个问题并没有询问关于ASP的内容。 - Developer
3
这与ASP无关。这个NuGet包含命名空间System.Net.Http.Formatting,它只是System.Net.Http的内容扩展。任何正在寻找官方HttpClient扩展的项目都可以(并且应该)使用它。 - caesay
3
迄今为止最简单和最简单的解决方案! - Ladislav
使用HttpCompletionOption.ResponseHeadersRead这个选项是不起作用的,对吗? - Emil

7
以下代码展示了针对 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`
    }
}

上述内容并未告诉您如何处理流,如何报告进度,也没有试图为原问题中的代码提供直接解决方案。但是,这个答案可能对未来希望在他们的代码中应用进度的读者更加易懂。

1
尽管有些不完整,但这是我能找到的唯一有效答案。Windows.Web.Http仅适用于Windows,并且它是一个UWP库,基本上是.NET的噩梦般的反乌托邦版本。 - user3190036
确保使用await stream.ReadAsync(...),否则它会一次性下载所有内容。至少在.NET Framework 4.6上是这样。 - undefined

5
这是对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);

这个方法应该如何调用并更新进度条?谢谢! - Feng Jiang
1
编辑了答案以包含使用示例。 - James Gordon
不错,必须将其转换为DotNetFramework 4.8。 - MC9000

5

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);
    }
}

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();
    }
}

0

这是René Sackers答案的修改版本,具有以下功能更改:

  • 不需要释放http客户端(因为它不应该被释放)
  • 更好的进度处理
  • 回调创建httpRequest(自定义标头支持)
  • 利用ArrayPool减少内存占用
  • 自动事件订阅和取消订阅以防止事件处理程序导致的内存泄漏

您还可以使用此Nuget软件包https://www.nuget.org/packages/Amusoft.Toolkit.Http获得所有优势。由于它支持net462及以上版本,这可能是最简单的方法。

用法:

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年的提问,这仍然是实际问题=) - Developer
这很好。只需要一个C# 7.3版本。 - MC9000
在哪里找到 Https.General?progressHandler 是 Windows 窗体上的进度条吗?我正在使用 .NET 6.0 中的这段代码!!!很棒的代码!!! - Tiago NET
@开发者 是的。不过考虑到进度通常是用户界面的需求,尽管下载时经常需要,但没有内置也是有道理的。哎,算了。 - Dbl

0
嗯,你可以创建另一个线程来检查当前正在写入的流的大小(同时将预期文件大小传递给它),然后相应地更新进度条。

如果您能提供一个例子,我会很高兴。 - Developer
很遗憾,我没有Win8副本,所以无法测试您的函数。但是,如果您想让事情变得简单一些,您可以将文件名和文件大小设置为全局变量,使用后台工作器带有循环和线程休眠来定期检查文件大小,并更新进度条。然而,这并不是一个非常优雅的解决方案。 - Rodrigo Silva
我认为那样做行不通。在ReadAsByteArrayAsync返回之前,流中没有任何内容被写入。 - kiewic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接