Использование HttpClient для асинхронной загрузки файлов

У меня есть служба, которая возвращает 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!");
            }
        }

    }
}

1 ответ

Решение
  1. "Это действительно асинхронный?"

Да, в основном. DownloadDataForStockAsync() метод вернется до завершения операции, в await response.Content.ReadAsStreamAsync() заявление.

Основное исключение находится в конце метода, где вы вызываете Stream.CopyTo(), Это не асинхронно, и потому что это потенциально длительная операция может привести к заметным задержкам. Однако в консольной программе вы не заметите, потому что продолжение метода выполняется в пуле потоков, а не в исходном вызывающем потоке.

Если вы намереваетесь переместить этот код в среду графического интерфейса, такую ​​как 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 счетчик для каждой задачи, которую вы создаете, и прикрепляет продолжение к каждой задаче, чтобы уменьшить счетчик. Когда счетчик достигает нуля, событие устанавливается, позволяя Wait() возвращать.

Другие вопросы по тегам