使用HttpClient进行异步文件下载

8
我有一个服务,它会通过POST请求返回一个csv文件。我想使用异步技术下载该文件。虽然我能够得到这个文件,但我的代码还存在一些问题和疑问:
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!");
            }
        }

    }
}

请查看http://msdn.microsoft.com/en-us/library/hh191443.aspx,以回答您的异步问题。 - Mike Burdick
1个回答

12
  1. "这真的是异步的吗?"

大多数情况下是的。在DownloadDataForStockAsync()方法中,操作尚未完成时,在await response.Content.ReadAsStreamAsync()语句处返回。

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

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

  1. 即使以块格式发送内容,是否有一种方法可以知道其长度?想想进度条)。

假设服务器在标头中包含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"
}
  1. 如何最好地监视进度,以防止程序退出,直到所有工作都完成。

有很多种方法。我要指出的是任何合理的方法都需要您更改 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计数器加1,并在每个任务上附加一个继续操作以减少计数器。当计数器达到零时,事件被设置,允许调用Wait()返回。


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