HttpClient ReadAsStringAsync с прогрессом

#c# #asynchronous #async-await #stream #httpclient

#c# #асинхронный #асинхронный-ожидание #поток #httpclient

Вопрос:

Есть ли способ получить прогресс ReadAsStringAsync() метода? Я просто получаю HTML-содержимое веб-сайта и анализирую.

 public static async Task<returnType> GetStartup(string url = "http://")
{
    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("User-Agent",
            "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko");
        using (HttpResponseMessage response = await client.GetAsync(url))
        {
            using (HttpContent content = response.Content)
            {
                string result = await content.ReadAsStringAsync();
            }
        }
    }
}
 

Комментарии:

1. Какой длины строка? Если размер ответа достаточно велик, чтобы гарантировать информацию о ходе выполнения (т. Е. Размером более нескольких мегабайт), то вам, вероятно, не следует читать его как String .

2. Кроме того, какая именно информация о ходе выполнения вам нужна? Для ответа размером менее ~ 10 КБ (т. Е. Пары TCP-пакетов / ethernet-кадров) невозможно получить числовой показатель прогресса в процентах, потому что он будет прыгать с 0% до 100% за один раз.

3. @Dai размер строки составляет от 3 МБ до 10 МБ

4. Без Content-Length заголовка невозможно указать какой-либо процент прогресса.

5. Ни один из них не имеет отношения к делу. Content-Length требуется, без него вы SOL.

Ответ №1:

Есть ли способ получить прогресс ReadAsStringAsync() метода? Я просто получаю HTML-содержимое веб-сайта и анализирую.

Да и нет.

HttpClient не предоставляет информацию о времени и ходе выполнения из базового сетевого стека, но вы можете получить некоторую информацию, используя HttpCompletionOption.ResponseHeadersRead Content-Length заголовок и самостоятельно прочитав ответ самостоятельно StreamReader (конечно, асинхронно).

Обратите внимание, что Content-Length в заголовках ответа будет указываться длина сжатого содержимого до распаковки, а не длина исходного содержимого, что усложняет ситуацию, потому что, вероятно, большинство веб-серверов сегодня будут обслуживать HTML (и статический контент) со gzip сжатием (либо Content-Encoding или Transfer-Encoding ), поэтому Content-Length заголовок вам не скажетдлина распакованного содержимого. К сожалению, хотя вы HttpClient можете выполнить автоматическую распаковку GZip, она не сообщит вам, какова длина распакованного содержимого.

Но вы все равно можете сообщать о некоторых видах прогресса обратно потребителю вашего метода, см. Пример ниже. Вы должны сделать это с помощью .ЧИСТЫЙ идиоматический IProgress<T> интерфейс, а не создание собственного.

Вот так:

 private static readonly HttpClient _hc = new HttpClient()
{
    DefaultRequestHeaders =
    {
        { "User-Agent", "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko" }
    }
    // NOTE: Automatic Decompression is not enabled in this HttpClient so that Content-Length can be safely used. But this will drastically slow down content downloads.
};

public static async Task<T> GetStartupAsync( IProgress<String> progress, string url = "http://")
{
    progress.Report( "Now making HTTP request..." );

    using( HttpResponseMessage response = await client.GetAsync( url, HttpCompletionOption.ResponseHeadersRead ) )
    {
        progress.Report( "Received HTTP response. Now reading response content..." );

        Int64? responseLength = response.Content.Headers.ContentLength;
        if( responseLength.HasValue )
        {
            using( Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false) )
            using( StreamReader rdr = new StreamReader( responseStream ) )
            {
                Int64 totalBytesRead = 0;
                StringBuilder sb = new StringBuilder( capacity: responseLength.Value ); // Note that `capacity` is in 16-bit UTF-16 chars, but responseLength is in bytes, though assuming UTF-8 it evens-out.

                Char[] charBuffer = new Char[4096];
                while( true )
                {
                    Int32 read = await rdr.ReadAsync( charBuffer ).ConfigureAwait(false);
                    sb.Append( charBuffer, 0, read );

                    if( read === 0 )
                    {
                        // Reached end.
                        progress.Report( "Finished reading response content." );
                        break;
                    }
                    else
                    {
                        progress.Report( String.Format( CultureInfo.CurrentCulture, "Read {0:N0} / {1:N0} chars (or bytes).", sb.Length, resposneLength.Value );
                    }
                }
            }
        }
        else
        {
            progress.Report( "No Content-Length header in response. Will read response until EOF." );
            
            string result = await content.ReadAsStringAsync();
        }
       
        progress.Report( "Finished reading response content." );
    }
 

Примечания:

  • В общем, любой async метод или метод, возвращающий Task / Task<T> , должен быть назван с Async суффиксом, поэтому ваш метод должен быть назван GetStartupAsync , а не GetStartup .
  • Если у вас нет IHttpClientFactory доступного, вы не должны заключать a HttpClient в using блок, потому что это может привести к исчерпанию системных ресурсов, особенно в серверном приложении.
    • (Причины этого сложны и также могут отличаться в зависимости от вашего .СЕТЕВАЯ реализация (например, я считаю, что у Xamarin HttpClient нет этой проблемы), но я не буду вдаваться в подробности здесь).
    • Таким образом, вы можете безопасно игнорировать любое предупреждение анализа кода о том, что вы не утилизируете ваш HttpClient . Это одно из немногих исключений из правила о постоянном удалении любых IDisposable объектов, которые вы создаете или которыми владеете.
    • Поскольку HttpClient это потокобезопасный и это static метод, рассмотрите возможность использования вместо него кэшированного статического экземпляра.
  • Вам также не нужно заключать HttpResponseMessage.Content в using блок, поскольку Content объект принадлежит HttpResponseMessage .

Комментарии:

1. Как я уже сказал в комментарии выше, доступна длина содержимого. Я думаю, я должен пойти на ReadAsStreamAsync?

2. @Alejandro Я обновил свой ответ, чтобы учесть Content-Length .

3. У вас слишком много ошибок с опечатками. Я прояснил это, но вы также должны сделать это для ответа

4. @Alejandro Код в моем ответе предназначен только в качестве иллюстративного примера и не предназначен для копирования и вставки в производство. Вы никогда не должны слепо копировать и вставлять код из StackOverflow — или любого другого веб-сайта, если на то пошло.

5. Я не говорю обо мне, я говорю о будущих ссылках. На самом деле мне не нужна была иллюстрация ReadAsStringAsync, мой вопрос касался ReadAsStringAsync, а не streamer. Я принимаю ответ, поскольку выясняю, что с ReadAsStringAsync нет решения, кроме использования streamer. В любом случае, как вы пожелаете