web-dev-qa-db-ja.com

非同期ファイルのダウンロードに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!");
            }
        }

    }
}
7
David Rogers
  1. 「これは本当に非同期ですか?」

はい、ほとんどです。 DownloadDataForStockAsync()メソッドは、操作が完了する前のawait response.Content.ReadAsStreamAsync()ステートメントに戻ります。

主な例外は、メソッドの終わり近くで、Stream.CopyTo()を呼び出します。これは非同期ではなく、操作が長くなる可能性があるため、顕著な遅延が発生する可能性があります。ただし、コンソールプログラムでは、メソッドの継続は元の呼び出しスレッドではなくスレッドプールで実行されるため、気付かないでしょう。

このコードをWinformsやWPFなどのGUIフレームワークに移動する場合は、ステートメントを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. すべての作業が完了するまでプログラムの終了を延期するために、どうすれば進行状況を最もよく監視できますか。

方法はたくさんあります。合理的な方法では、Taskではなくvoidを返すようにDownloadDataForStockAsync()メソッドを変更する必要があることを指摘します。そうしないと、作成されたタスクにアクセスできません。とにかくこれを行う必要があるので、それは大したことではありません。 :)

最も簡単なのは、開始するすべてのタスクのリストを保持し、それらを待ってから終了することです。

_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()の呼び出しが返されます。

10
Peter Duniho