使用 HttpClient 进行异步文件下载

Posted

技术标签:

【中文标题】使用 HttpClient 进行异步文件下载【英文标题】:Using HttpClient for Asynchronous File Downloads 【发布时间】:2015-01-10 01:40:58 【问题描述】:

我有一项服务可以将 csv 文件返回给 POST 请求。我想使用异步技术下载所述文件。虽然我可以获得该文件,但我的代码有几个突出的问题和疑问:

1) 这真的是异步的吗?

2) 有没有办法知道内容的长度,即使它是以分块格式发送的?想想进度条)。

3) 我怎样才能最好地监控进度,以便在所有工作完成之前推迟程序退出。

using System;
using System.IO;
using System.Net.Http;

namespace TestHttpClient2

    class Program
    
        /*
         * Use Yahoo portal to access quotes for stocks - perform asynchronous operations.
         */

        static string baseUrl = "http://real-chart.finance.yahoo.com/";
        static string requestUrlFormat = "/table.csv?s=0&d=0&e=9&f=2015&g=d&a=4&b=5&c=2000&ignore=.csv";

        static void Main(string[] args)
        
            while (true) 
            
                Console.Write("Enter a symbol to research or [ENTER] to exit: ");
                string symbol = Console.ReadLine();
                if (string.IsNullOrEmpty(symbol))
                    break;
                DownloadDataForStockAsync(symbol);
            
        

        static async void DownloadDataForStockAsync(string symbol)
        
            try
            
                using (var client = new HttpClient())
                
                    client.BaseAddress = new Uri(baseUrl);
                    client.Timeout = TimeSpan.FromMinutes(5);
                    string requestUrl = string.Format(requestUrlFormat, symbol);

                    //var content = new KeyValuePair<string, string>[] 
                    //    ;
                    //var formUrlEncodedContent = new FormUrlEncodedContent(content);

                    var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
                    var sendTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
                    var response = sendTask.Result.EnsureSuccessStatusCode();
                    var httpStream = await response.Content.ReadAsStreamAsync();

                    string OutputDirectory = "StockQuotes";

                    if (!Directory.Exists(OutputDirectory))
                    
                        Directory.CreateDirectory(OutputDirectory);
                    

                    DateTime currentDateTime = DateTime.Now;
                    var filePath = Path.Combine(OutputDirectory, string.Format("1:D4_2:D2_3:D2_4:D2_5:D2_6:D2_7:D3_0.csv",
                        symbol,
                        currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
                        currentDateTime.Hour, currentDateTime.Minute, currentDateTime.Second, currentDateTime.Millisecond
                        ));

                    using (var fileStream = File.Create(filePath))
                    using (var reader = new StreamReader(httpStream))
                    
                        httpStream.CopyTo(fileStream);
                        fileStream.Flush();
                    
                
            
            catch (Exception ex)
            
                Console.WriteLine("Error, try again!");
            
        

    

【问题讨论】:

根据您的异步问题查看msdn.microsoft.com/en-us/library/hh191443.aspx 【参考方案1】:
    “这真的是异步的吗?”

是的,主要是。 DownloadDataForStockAsync() 方法将在操作完成之前返回,在 await response.Content.ReadAsStreamAsync() 语句处。

主要的例外是在方法的末尾附近,您调用Stream.CopyTo()。这不是异步的,因为它是一个潜在的冗长操作,可能会导致明显的延迟。但是,在控制台程序中您不会注意到,因为方法的延续是在线程池中执行的,而不是在原始调用线程中执行的。

如果您打算将此代码移至 GUI 框架,例如 Winforms 或 WPF,则应将语句更改为 await httpStream.CopyToAsync(fileStream);

    有没有办法知道内容的长度,即使它是以分块格式发送的?想想进度条)。

假设服务器在标题中包含Content-Length(并且应该),是的。这应该是可能的。

请注意,如果您使用的是HttpWebRequest,则响应对象将有一个ContentLength 属性直接为您提供此值。你在这里使用HttpRequestMessage,我不太熟悉。但据我所知,您应该能够像这样访问Content-Length 值:

long? contentLength = response.Content.Headers.ContentLength;

if (contentLength != null)

    // use value to initialize "determinate" progress indication

else

    // no content-length provided; will need to display progress as "indeterminate"

    我怎样才能最好地监控进度,以便在所有工作完成之前推迟程序退出。

有很多方法。我要指出,任何合理的方法都需要您更改DownloadDataForStockAsync() 方法,使其返回Task 而不是void。否则,您无权访问已创建的任务。不过,无论如何你都应该这样做,所以这没什么大不了的。 :)

最简单的方法是保留一份您启动的所有任务的列表,然后在退出前等待它们:

static void Main(string[] args)

    List<Task> tasks = new List<Task>();

    while (true) 
    
        Console.Write("Enter a symbol to research or [ENTER] to exit: ");
        string symbol = Console.ReadLine();
        if (string.IsNullOrEmpty(symbol))
            break;
        tasks.Add(DownloadDataForStockAsync(symbol));
    

    Task.WaitAll(tasks);

当然,这需要您明确维护每个Task 对象的列表,包括那些已经完成的对象。如果您打算让它运行很长时间并处理大量符号,那可能会令人望而却步。在这种情况下,您可能更喜欢使用 CountDownEvent 对象:

static void Main(string[] args)

    CountDownEvent countDown = new CountDownEvent();

    while (true) 
    
        Console.Write("Enter a symbol to research or [ENTER] to exit: ");
        string symbol = Console.ReadLine();
        if (string.IsNullOrEmpty(symbol))
            break;

        countDown.AddCount();
        DownloadDataForStockAsync(symbol).ContinueWith(task => countdown.Signal()) ;
    

    countDown.Wait();

这只是为您创建的每个任务增加CountDownEvent 计数器,并为每个任务附加一个延续以减少计数器。当计数器达到零时,事件被设置,允许对Wait() 的调用返回。

【讨论】:

以上是关于使用 HttpClient 进行异步文件下载的主要内容,如果未能解决你的问题,请参考以下文章

使用 HttpClient 进行异步文件下载时的线程问题

异步httpclient(httpasyncclient)的使用与总结

HttpClient5.0,如何在异步模式下使用gzip?

在 PCL 中使用 HttpClient 进行异步调用

(办公)访问其他系统接口httpClient,异步访问

SpringBoot项目使用hutool工具进行HttpClient接口调用的处理(文件上传)