如何使用带有进度条的 HttpClient 下载文件?

Dan*_*Lip 3 c# httpclient winforms .net-6.0

我创建了一个名为的新类SiteDownload,并添加了一些下载图像的链接:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

    public class SiteDownload
    {
        public static List<string> Sites()
        {
            List<string> list = new List<string>();

            list.Add("mysite.com/sites/default/files/1231105.gif");
            list.Add("mysite.com/sites/default/files/1231040.gif");

        return list;
        }

        public static async Task<List<Website>> ParallelDownload(IProgress<ProgressReport> progress, CancellationTokenSource cancellationTokenSource)
        {
            List<string> sites = Sites();
            List<Website> list = new List<Website>();
            ProgressReport progressReport = new ProgressReport();
            ParallelOptions parallelOptions = new ParallelOptions();
            parallelOptions.MaxDegreeOfParallelism = 8;
            parallelOptions.CancellationToken = cancellationTokenSource.Token;
            await Task.Run(() =>
            {
                try
                {
                    Parallel.ForEach<string>(sites, parallelOptions, (site) =>
                    {
                        Website results = Download(site);
                        list.Add(results);
                        progressReport.SitesDownloaded = list;
                        progressReport.PercentageComplete = (list.Count * 100) / sites.Count;
                        progress.Report(progressReport);
                        parallelOptions.CancellationToken.ThrowIfCancellationRequested();
                    });
                }
                catch (OperationCanceledException ex)
                {
                    throw ex;
                }
            });

            return list;
        }

        private static Website Download(string url)
        {
            Website website = new Website();
            WebClient client = new WebClient();
            website.Url = url;
            website.Data = client.DownloadString(url);
            return website;
        }

        public class Website
        {
            public string Url { get; set; }
            public string Data { get; set; }
        }

        public class ProgressReport
        {
            public int PercentageComplete { get; set; }
            public List<Website> SitesDownloaded { get; set; }
        }
    }
Run Code Online (Sandbox Code Playgroud)

form1

using System.Linq;
using System.Net;
using System.Threading.Tasks;
using static HttpClientFilesDownloader.SiteDownload;

namespace HttpClientFilesDownloader
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        void PrintResults(List<Website> results)
        {
            richTextBox1.Text = string.Empty;
            foreach (var item in results)
                richTextBox1.Text += $"{item.Url} downloaded: {item.Data.Length} characters long.{Environment.NewLine}";
        }

        void ReportProgress(object sender, ProgressReport e)
        {
            progressBar1.Value = e.PercentageComplete;
            label1.Text = $"Completed: {e.PercentageComplete} %";
            PrintResults(e.SitesDownloaded);
        }

        CancellationTokenSource cancellationTokenSource;
        private async void button1_Click(object sender, EventArgs e)
        {
            try
            {
                cancellationTokenSource = new CancellationTokenSource();
                Progress<ProgressReport> progress = new Progress<ProgressReport>();
                progress.ProgressChanged += ReportProgress;
                var watch = Stopwatch.StartNew();
                var results = await SiteDownload.ParallelDownload(progress, cancellationTokenSource);
                PrintResults(results);
                watch.Stop();
                var elapsed = watch.ElapsedMilliseconds;
                richTextBox1.Text += $"Total execution time: {elapsed}";
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                cancellationTokenSource.Dispose();
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (cancellationTokenSource != null)
                cancellationTokenSource.Cancel();
        }
    } 
}
Run Code Online (Sandbox Code Playgroud)

设计者

设计师

当我单击“开始”按钮时,没有任何反应。我没有看到progressBar任何进程,并且label1没有更新,RichTextBox 中也没有任何内容。它只是不下载图像。
我没有收到任何错误,只是无法下载。

我从这个网站上获取了这个例子,只是下载网站,我试图下载图像文件并将它们作为图像保存在硬盘上:

例子

我还需要像使用 webclient 一样添加标头:

webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0 Chrome");
Run Code Online (Sandbox Code Playgroud)

但不确定如何将标头添加到 HttpClient。

Jim*_*imi 6

HTTP 资源下载器的示例。此类旨在针对 .NET 6+,因为它使用Parallel.ForEachAsync()。record关键字需要 C# 9+。Nullable已启用

我尝试尽可能保留您在OP中使用的结构

要开始下载资源集合,请调用静态Download()方法,传递IProgress<ProgressReport>委托、表示资源 URL 的字符串集合和 CancellationTokenSource

ReportProgress()方法将一条记录封送到 UI 线程ProgressReport。它引用一条WebData记录,其中包含当前资源的 URL、图像(在本例中)字节、Completed状态以及资源由于某种原因无法下载时抛出的异常。如果同时取消下载,则异常原因将为The operation was canceled
它还以百分比的形式返回下载的总体进度。
请注意,当您取消操作时,进度过程也会完成,因为您可能想知道在取消操作之前哪些资源已完成以及哪些资源无法完成

注意:静态Download()方法不是线程安全的,即,您不能同时调用该方法,例如,同时下载多个资源列表(尽管可以轻松重构,使其成为非静态的)。在再次调用该方法之前
检查该属性。IsBusy

public class ResourceDownloader {

    private static readonly Lazy<HttpClient> client = new(() => {
        SocketsHttpHandler handler = CreateHandler(autoRedirect: true);

        var client = new HttpClient(handler, true) { Timeout = TimeSpan.FromSeconds(60) };
        client.DefaultRequestHeaders.Add("User-Agent", @"Mozilla/5.0 (Windows NT 10; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0");
        client.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
        client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate");
        // Keep true if you download resources from different collections of URLs each time
        // Remove or set to false if you use the same URLs multiple times and frequently
        client.DefaultRequestHeaders.ConnectionClose = true;
        return client;
    }, true);

    private static SocketsHttpHandler CreateHandler(bool autoRedirect)
    {
        return new SocketsHttpHandler() {
            AllowAutoRedirect = autoRedirect,
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
            CookieContainer = new CookieContainer(),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2) // <= Adjust as required
        };
    }

    public record WebData(string Url, byte[]? Data, bool Completed = true, Exception? Ex = null);
    public record ProgressReport(WebData Site, int PercentageComplete);

    private static object syncObj = new object();
    private static ConcurrentBag<WebData> processed = default!;
    private static int progressCount = 0;
    private static int totalCount = 0;

    public static bool IsBusy { get; internal set; } = false;

    public static async Task<List<WebData>> Download(IProgress<ProgressReport> progress, IList<string> sites, CancellationTokenSource cts)
    {
        IsBusy = true;
        processed = new ConcurrentBag<WebData>();
        progressCount = 0;
        totalCount = sites.Count;

        try {
            ParallelOptions options = new() {
                // If it's a single web site, set a value that doesn't get you black-listed
                // Otherwise, increase the value 
                MaxDegreeOfParallelism = 8,
                CancellationToken = cts.Token
            };

            await Parallel.ForEachAsync(sites, options, async (site, token) => {
                try {
                    var dataBytes = await client.Value.GetByteArrayAsync(site, token);
                    ReportProgress(progress, dataBytes, site, null);
                }
                catch (Exception ex) {
                    ReportProgress(progress, null, site, ex);
                }
            });
        }
         // To Debug / Log
        catch (TaskCanceledException) { Debug.Print("The operation was canceled"); }
        finally { IsBusy = false; }
        return processed.ToList();
    }

    private static void ReportProgress(IProgress<ProgressReport> progress, byte[]? data, string site, Exception? ex) {
        lock (syncObj) {
            progressCount += 1;
            var percentage = progressCount * 100 / totalCount;
            WebData webData = new(site, data, ex is null, ex);
            processed.Add(webData);
            progress.Report(new ProgressReport(webData, percentage));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以像这样设置一个表单:

  • 添加一个文本框(此处名为logger)以显示正在下载的资源的状态
  • 用于开始下载的按钮(名为btnStartDownload
  • 取消下载的按钮(名为btnStopDownload
  • ProgressBar(名为progressBar)用于显示总体进度

请注意,使用活动的(未配置的)调试器,您可能会收到抛出异常的通知,因此可以使用以下命令运行项目CTRL + F5

public partial class SomeForm : Form {
    public SomeForm() => InitializeComponent();

    internal List<string> Sites()
    {
        var list = new List<string>();

        list.Add("https://somesite/someresource.jpg");
        // [...] add more URLs
        return list;
    }

    IProgress<ResourceDownloader.ProgressReport>? downloadProgress = null;
    CancellationTokenSource? cts = null;

    private void Updater(ResourceDownloader.ProgressReport progress)
    {
        StringBuilder log = new(1024);
        if (progress.Site.Completed) {
            log.Append($"Success! \t {progress.Site.Url}\r\n");
        }
        else {
            log.Append($"Failed! \t {progress.Site.Url}\r\n");
            log.Append($"\tReason: {progress.Site.Ex?.Message}\r\n");
        }
        logger.AppendText(log.ToString());
        progressBar.Value = progress.PercentageComplete;
    }

    private async void btnStartDownload_Click(object sender, EventArgs e)
    {
        if (ResourceDownloader.IsBusy) return;
        var sites = Sites();

        // This collection will contain the status (and data) of all downloads in th end
        List<ResourceDownloader.WebData>? downloads = null;

        using (cts = new CancellationTokenSource()) {
            downloadProgress = new Progress<ResourceDownloader.ProgressReport>(Updater);
            downloads = await ResourceDownloader.Download(downloadProgress, sites, cts);
        }
    }

    private void btnStopDownload_Click(object sender, EventArgs e) => cts?.Cancel();
}
Run Code Online (Sandbox Code Playgroud)

它是这样工作的:

并行异步下载器