使用HttpClient的ReadAsStringAsync方法并带有进度报告

4

有没有一种方法可以获取 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();
            }
        }
    }
}

这个字符串有多长?如果响应大小足够大以需要进度信息(即大于几兆字节),那么你可能不应该将它作为String读取。 - Dai
另外,您究竟需要什么样的进度信息?对于响应大小小于~10KB(即几个TCP数据包/以太网帧),不可能获得数字百分比进度数字,因为它将在一次操作中从0%跳转到100%。 - Dai
@Dai 字符串大小在3MB到10MB之间。 - Alejandro
1
没有 Content-Length 头部,就无法指示任何百分比进度。 - Dai
1
这些都不相关。Content-Length是必需的,没有它你就完了。 - Dai
显示剩余5条评论
1个回答

6
有没有一种方法可以获取 ReadAsStringAsync() 方法的进度? 我只是在获取网站的HTML内容并解析它。
是和不是。
HttpClient 不会从底层网络栈中公开计时和进度信息,但是您可以通过使用 HttpCompletionOption.ResponseHeadersRead、Content-Length 标头和使用您自己的 StreamReader(当然是异步的)读取响应来获取一些信息。
请注意,响应标头中的 Content-Length 将指向解压缩之前压缩的内容长度,而不是原始内容长度,这使事情变得复杂,因为今天可能大多数 Web 服务器都会使用 gzip 压缩(作为 Content-Encoding 或 Transfer-Encoding)提供 HTML(和静态内容),因此 Content-Length 标头将无法告诉您解压缩后内容的长度。不幸的是,虽然 HttpClient 可以为您执行自动 GZip 解压缩,但它不会告诉您解压缩后的内容长度。
但是,您仍然可以向方法的消费者报告某些类型的进度,下面是一个示例。 您应该使用.NET惯用的 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,否则您不应该using块中包装HttpClient,因为这可能会导致系统资源枯竭,特别是在服务器应用程序中。
    • (造成这种问题的原因很复杂,并且也可能因您的.NET实现而异(例如,我相信Xamarin的HttpClient没有这个问题),但我不会在此讨论细节)。
    • 因此,您可以安全地忽略任何有关未处置HttpClient的代码分析警告。这是始终处置您创建或拥有的任何IDisposable对象规则的少数例外之一。
    • 由于HttpClient是线程安全的,这是一个static方法,因此考虑使用已缓存的静态实例。
  • 您也不需要在HttpResponseMessage.Content上包装using块,因为Content对象是由HttpResponseMessage所拥有的。

正如我在上面的评论中所说,Content-Length是可用的。我猜我应该选择ReadAsStreamAsync? - Alejandro
@Alejandro 我已更新我的答案以考虑 Content-Length - Dai
你有太多的拼写错误。我已经把它们改正了,但你也应该对答案进行检查。 - Alejandro
@Alejandro 我在答案中提供的代码仅作为示例,不应该被直接复制到生产环境中。你绝不能盲目地从StackOverflow或其他网站上复制粘贴代码。 - Dai
我不是在谈论我自己,而是在谈论未来的参考。我并不真正需要任何ReadAsStringAsync的示例,我的问题是关于ReadAsStringAsync而不是streamer。我接受这个答案,因为我发现除了使用streamer之外,没有使用ReadAsStringAsync的解决方案。无论如何,随你的便。 - Alejandro

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