【问题标题】:Deciding between HttpClient and WebClient在 HttpClient 和 WebClient 之间做出决定
【发布时间】:2013-12-30 01:49:43
【问题描述】:

我们的网络应用在 .Net Framework 4.0 中运行。 UI 通过 ajax 调用来调用控制器方法。

我们需要使用供应商提供的 REST 服务。我正在评估在 .Net 4.0 中调用 REST 服务的最佳方式。 REST 服务需要基本身份验证方案,并且它 可以返回 XML 和 JSON 格式的数据。不需要上传/下载大量数据,将来我也看不到任何东西。我查看了一些用于 REST 消费的开源代码项目,并没有发现任何价值来证明项目中的额外依赖是合理的。开始评估WebClientHttpClient。我从 NuGet 下载了 .Net 4.0 的 HttpClient。

我搜索了WebClientHttpClient之间的区别,this site提到单个HttpClient可以处理并发调用,它可以重用解析的DNS、cookie配置和身份验证。我还没有看到我们可能因差异而获得的实际价值。

我做了一个快速的性能测试,以了解WebClient(同步调用)、HttpClient(同步和异步)的执行情况。结果如下:

对所有请求使用相同的 HttpClient 实例(最小 - 最大)

WebClient 同步:8 毫秒 - 167 毫秒
HttpClient 同步:3 毫秒 - 7228 毫秒
HttpClient 异步:985 - 10405 毫秒

为每个请求使用一个新的HttpClient(最小值 - 最大值)

WebClient 同步:4 毫秒 - 297 毫秒
HttpClient 同步:3 毫秒 - 7953 毫秒
HttpClient 异步:1027 - 10834 毫秒

代码

public class AHNData
{
    public int i;
    public string str;
}

public class Program
{
    public static HttpClient httpClient = new HttpClient();
    private static readonly string _url = "http://localhost:9000/api/values/";

    public static void Main(string[] args)
    {
       #region "Trace"
       Trace.Listeners.Clear();

       TextWriterTraceListener twtl = new TextWriterTraceListener(
           "C:\\Temp\\REST_Test.txt");
       twtl.Name = "TextLogger";
       twtl.TraceOutputOptions = TraceOptions.ThreadId | TraceOptions.DateTime;

       ConsoleTraceListener ctl = new ConsoleTraceListener(false);
       ctl.TraceOutputOptions = TraceOptions.DateTime;

       Trace.Listeners.Add(twtl);
       Trace.Listeners.Add(ctl);
       Trace.AutoFlush = true;
       #endregion

       int batchSize = 1000;

       ParallelOptions parallelOptions = new ParallelOptions();
       parallelOptions.MaxDegreeOfParallelism = batchSize;

       ServicePointManager.DefaultConnectionLimit = 1000000;

       Parallel.For(0, batchSize, parallelOptions,
           j =>
           {
               Stopwatch sw1 = Stopwatch.StartNew();
               GetDataFromHttpClientAsync<List<AHNData>>(sw1);
           });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                Stopwatch sw1 = Stopwatch.StartNew();
                GetDataFromHttpClientSync<List<AHNData>>(sw1);
            });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                using (WebClient client = new WebClient())
                {
                   Stopwatch sw = Stopwatch.StartNew();
                   byte[] arr = client.DownloadData(_url);
                   sw.Stop();

                   Trace.WriteLine("WebClient Sync " + sw.ElapsedMilliseconds);
                }
           });

           Console.Read();
        }

        public static T GetDataFromWebClient<T>()
        {
            using (var webClient = new WebClient())
            {
                webClient.BaseAddress = _url;
                return JsonConvert.DeserializeObject<T>(
                    webClient.DownloadString(_url));
            }
        }

        public static void GetDataFromHttpClientSync<T>(Stopwatch sw)
        {
            HttpClient httpClient = new HttpClient();
            var response = httpClient.GetAsync(_url).Result;
            var obj = JsonConvert.DeserializeObject<T>(
                response.Content.ReadAsStringAsync().Result);
            sw.Stop();

            Trace.WriteLine("HttpClient Sync " + sw.ElapsedMilliseconds);
        }

        public static void GetDataFromHttpClientAsync<T>(Stopwatch sw)
        {
           HttpClient httpClient = new HttpClient();
           var response = httpClient.GetAsync(_url).ContinueWith(
              (a) => {
                 JsonConvert.DeserializeObject<T>(
                    a.Result.Content.ReadAsStringAsync().Result);
                 sw.Stop();
                 Trace.WriteLine("HttpClient Async " + sw.ElapsedMilliseconds);
              }, TaskContinuationOptions.None);
        }
    }
}

我的问题

  1. REST 调用在 3-4 秒内返回,这是可以接受的。调用 REST 服务是在控制器方法中启动的,这些方法从中调用 ajax 调用。首先,调用在不同的线程中运行,并且 不会阻止用户界面。那么,我可以坚持使用同步调用吗?
  2. 上面的代码是在我的localbox中运行的。在产品设置、DNS 和代理中 将涉及查找。使用HttpClient 比使用WebClient 有什么优势吗?
  3. HttpClient 的并发性是否优于 WebClient?从测试结果中,我看到WebClient 同步调用表现更好。
  4. 如果我们升级到 .Net 4.5,HttpClient 会是更好的设计选择吗?性能是关键的设计因素。

【问题讨论】:

  • 您的测试对GetDataFromHttpClientAsync 不公平,因为它首先运行,其他调用受益于潜在的缓存数据(无论是在本地计算机上还是在您和目的地之间的任何透明代理上)和会更快。此外,在正确的条件下,var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result; 可能会由于您耗尽线程池线程而导致死锁。您永远不应该阻塞依赖于 ThreadPool 线程中的线程池的活动,您应该 await 改为将线程返回到池中。
  • 带有 Web API 客户端的 HttpClient 非常适合 JSON/XML REST 客户端。
  • @Scott Chamberlain - 感谢您的回复。由于所有测试调用都在 Parallel.Foreach 中运行,因此无法保证哪个会先运行。此外,如果对服务的第一次调用来自 GetDataFromHttpClientAsync ,则来自 GetDataFromHttpClientAsync 的所有后续调用都应该受益于缓存并运行得更快。我没有在结果中看到这一点。 rgd await,我们还在用4.0。我同意你的观点,同步方式的 HttpClient 会导致死锁,我将这个选项排除在我的设计考虑之外。
  • 这里说几句HttpClient和WebClient的区别:blogs.msdn.com/b/henrikn/archive/2012/02/11/…
  • docs.microsoft.com/en-us/dotnet/api/… 建议在新开发中使用 HttpClient 而不是 WebClient。 .NET Framework 和 .NET Core 都是如此。

标签: c# .net rest webclient dotnet-httpclient


【解决方案1】:

HttpClient 是较新的 API,它具有以下优点:

  • 具有良好的异步编程模型
  • 由 Henrik F Nielson 工作,他基本上是 HTTP 的发明者之一,他设计的 API 让您可以轻松地遵循 HTTP 标准,例如生成符合标准的标头
  • 在 .Net 框架 4.5 中,因此它对可预见的未来有一定程度的支持
  • 如果您想在其他平台(.Net 4.0、Windows Phone 等)上使用该库,它还具有 xcopyable/portable-framework 版本。

如果您正在编写一个对其他 Web 服务进行 REST 调用的 Web 服务,您应该希望对所有 REST 调用使用异步编程模型,这样您就不会遇到线程不足的问题。您可能还想使用支持异步/等待的最新 C# 编译器。

注意:它不是更高性能的 AFAIK。如果你创建一个公平的测试,它的性能可能有点相似。

【讨论】:

  • 如果它有办法切换代理,那就太疯狂了
  • 虽然这是一个老问题,但它出现在我的搜索中,所以我想我会指出微软的 documentation 用于 .NET 5 中的 WebClient 状态,“我们不建议您使用WebClient 类进行新开发。改为使用System.Net.Http.HttpClient 类。"
【解决方案2】:

HttpClientFactory

评估创建 HttpClient 的不同方式很重要,其中一部分是理解 HttpClientFactory。

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

我知道这不是一个直接的答案 - 但你最好从这里开始而不是在任何地方结束 new HttpClient(...)

【讨论】:

  • 值得强调的是,即使是 .NET Framework 中的 HttpWebRequest 和扩展的 WebClient use HttpClient 至少从 2018 年开始,所以这个问题基本上没有实际意义
【解决方案3】:

2020 年不受欢迎的观点:

对于 ASP.NET 应用程序我仍然更喜欢 WebClient 而不是 HttpClient,因为:

  1. 现代实现带有基于异步/等待任务的方法
  2. 内存占用更小,速度提高 2 到 5 倍(其他答案已经提到)
  3. 建议“重复使用单个 应用程序生命周期内的 HttpClient 实例”。但是 ASP.NET 没有“应用程序的生命周期”,只有请求的生命周期。当前对 ASP.NET 5 的指导是使用 HttpClientFactory,但它只能通过依赖注入来使用。有些人想要一个更简单的解决方案。
  4. 最重要的是,如果您像 MS 建议的那样在应用程序的整个生命周期中使用 HttpClient 的一个单例实例 - 它存在已知问题。例如 DNS 缓存问题 - HttpClient 只是忽略 TTL 并“永远”缓存 DNS。但是,有一些解决方法。如果您想了解更多关于 HttpClient 的问题和困惑,请阅读 MS github 上的this comment

【讨论】:

  • 鉴于 .NET Old 已被 .NET Core 取代,您是否使用 .NET Core 运行基准测试?到目前为止,HttpWebRequest 是 HttpClient 的包装器,因此 WebClient 本质上是 WebClient 的旧适配器
  • only lifetime of a request. 错了。在旧的 ASP.NET 堆栈中也可以使用 DI 容器来提供单例或作用域对象,只是更难使用。
  • @PanagiotisKanavos 是的,但您仍然无法控制应用程序的生命周期。并且平均“程序员乔”无论如何都不会费心创建静态/单例变量来缓存 HttpClient。
  • 应用程序的生命周期无关紧要,只有注入的 HttpClient 的——或者更确切地说,HttpClientHandler 的。这对于所有应用程序来说都是很容易做到的。并且 HttpWebRequest 确实 使用缓存的 HttpClientHandler(如果可用)。您应该重新运行基准测试。如果你的结果表明类的包装器比类本身更快或使用更少的内存,那么有问题
  • 此外,这个“.NET Old 已被 .NET Core 取代” - 它还没有取代它,.NET Framework 仍受支持,并将支持另外 10 个至少几年(基本上只要它是 Windows 的一部分)。但是我可能应该表明我的答案是针对 .NET Framework,而不是 Core
【解决方案4】:

首先,我不是 WebClient 与 HttpClient 的权威,特别是。其次,从上面的 cmets 来看,似乎 WebClient 是 Sync ONLY 而 HttpClient 两者都是。

我做了一个快速的性能测试,以了解 WebClient(同步调用)、HttpClient(同步和异步)的执行情况。这是结果。

我认为这在考虑未来时存在巨大差异,即长时间运行的进程、响应式 GUI 等(增加了您建议的框架 4.5 的好处——根据我的实际经验,在 IIS 上速度要快得多)

【讨论】:

  • WebClient 在最新的 .NET 版本中似乎确实具有异步功能。我想知道为什么它似乎在如此大规模上胜过 HttpClient。
  • 根据stackoverflow.com/a/4988325/1662973,它似乎是一样的,除了一个是另一个的抽象。也许,这取决于对象的使用/加载方式。最小时间确实支持 webclient 实际上是 HttpClient 的抽象的说法,因此存在毫秒级的开销。该框架可能在实际池化或处理 webclient 方面“偷偷摸摸”。
【解决方案5】:

也许您可以换一种方式来思考这个问题。 WebClientHttpClient 本质上是同一事物的不同实现。我建议在整个应用程序中使用IoC Container 实现Dependency Injection pattern。您应该构造一个比低级别 HTTP 传输具有更高抽象级别的客户端接口。您可以编写同时使用WebClientHttpClient 的具体类,然后使用IoC 容器通过config 注入实现。

这将允许您在HttpClientWebClient 之间轻松切换,以便您能够在生产环境中客观地进行测试。

所以问题如下:

如果我们升级到 .Net 4.5,HttpClient 会是更好的设计选择吗?

实际上可以通过使用 IoC 容器在两个客户端实现之间切换来客观回答。这是您可能依赖的示例界面,其中不包含有关 HttpClientWebClient 的任何详细信息。

/// <summary>
/// Dependency Injection abstraction for rest clients. 
/// </summary>
public interface IClient
{
    /// <summary>
    /// Adapter for serialization/deserialization of http body data
    /// </summary>
    ISerializationAdapter SerializationAdapter { get; }

    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <typeparam name="TRequestBody">The type of the request body if specified</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns></returns>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(Request<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Default timeout for http requests
    /// </summary>
    TimeSpan Timeout { get; set; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    Uri BaseUri { get; set; }

    /// <summary>
    /// Name of the client
    /// </summary>
    string Name { get; }
}

public class Request<TRequestBody>
{
    #region Public Properties
    public IHeadersCollection Headers { get; }
    public Uri Resource { get; set; }
    public HttpRequestMethod HttpRequestMethod { get; set; }
    public TRequestBody Body { get; set; }
    public CancellationToken CancellationToken { get; set; }
    public string CustomHttpRequestMethod { get; set; }
    #endregion

    public Request(Uri resource,
        TRequestBody body,
        IHeadersCollection headers,
        HttpRequestMethod httpRequestMethod,
        IClient client,
        CancellationToken cancellationToken)
    {
        Body = body;
        Headers = headers;
        Resource = resource;
        HttpRequestMethod = httpRequestMethod;
        CancellationToken = cancellationToken;

        if (Headers == null) Headers = new RequestHeadersCollection();

        var defaultRequestHeaders = client?.DefaultRequestHeaders;
        if (defaultRequestHeaders == null) return;

        foreach (var kvp in defaultRequestHeaders)
        {
            Headers.Add(kvp);
        }
    }
}

public abstract class Response<TResponseBody> : Response
{
    #region Public Properties
    public virtual TResponseBody Body { get; }

    #endregion

    #region Constructors
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response() : base()
    {
    }

    protected Response(
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    TResponseBody body,
    Uri requestUri
    ) : base(
        headersCollection,
        statusCode,
        httpRequestMethod,
        responseData,
        requestUri)
    {
        Body = body;
    }

    public static implicit operator TResponseBody(Response<TResponseBody> readResult)
    {
        return readResult.Body;
    }
    #endregion
}

public abstract class Response
{
    #region Fields
    private readonly byte[] _responseData;
    #endregion

    #region Public Properties
    public virtual int StatusCode { get; }
    public virtual IHeadersCollection Headers { get; }
    public virtual HttpRequestMethod HttpRequestMethod { get; }
    public abstract bool IsSuccess { get; }
    public virtual Uri RequestUri { get; }
    #endregion

    #region Constructor
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response()
    {
    }

    protected Response
    (
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    Uri requestUri
    )
    {
        StatusCode = statusCode;
        Headers = headersCollection;
        HttpRequestMethod = httpRequestMethod;
        RequestUri = requestUri;
        _responseData = responseData;
    }
    #endregion

    #region Public Methods
    public virtual byte[] GetResponseData()
    {
        return _responseData;
    }
    #endregion
}

Full code

HttpClient Implementation

您可以使用Task.Run 使WebClient 在其实现中异步运行。

依赖注入,如果做得好,有助于缓解必须预先做出低级决策的问题。最终,知道真正答案的唯一方法是在现场环境中尝试,看看哪一个效果最好。 WebClient 很可能对某些客户更有效,而HttpClient 可能对其他客户更有效。这就是为什么抽象很重要。这意味着可以快速换入代码,或通过配置更改代码,而无需更改应用的基本设计。

顺便说一句:还有许多其他原因,您应该使用抽象而不是直接调用这些低级 API 之一。一个巨大的问题是单元可测试性。

【讨论】:

  • 对于这个例子,为什么使用抽象而不是接口? (忽略默认实现)是否纯粹出于 GetResponseData() 定义的目的?还是我在这里遗漏了什么?
  • 我不明白这个问题
  • 我很好奇你为什么选择在这里使用摘要,而不是与“响应”对象(通用和非通用)的接口
  • WebClient 间接使用 HttpClient,因为 HttpWebRequest uses HttpClient internally 甚至在 .NET Framework 中,至少从 2018 年开始。WebClient 和 HttpWebRrequest 在这一点上都只是过时的兼容性包装器。 WebClient 确实有适当的异步方法,所以它不需要Task.Run
【解决方案6】:

我在 HttpClient、WebClient、HttpWebResponse 之间进行基准测试,然后调用 Rest Web Api

和结果 调用 Rest Web Api Benchmark

---------------------Stage 1  ---- 10 Request

{00:00:17.2232544} ====>HttpClinet
{00:00:04.3108986} ====>WebRequest
{00:00:04.5436889} ====>WebClient

---------------------Stage 1  ---- 10 Request--Small Size
{00:00:17.2232544}====>HttpClinet
{00:00:04.3108986}====>WebRequest
{00:00:04.5436889}====>WebClient

---------------------Stage 3  ---- 10 sync Request--Small Size
{00:00:15.3047502}====>HttpClinet
{00:00:03.5505249}====>WebRequest
{00:00:04.0761359}====>WebClient

---------------------Stage 4  ---- 100 sync Request--Small Size
{00:03:23.6268086}====>HttpClinet
{00:00:47.1406632}====>WebRequest
{00:01:01.2319499}====>WebClient

---------------------Stage 5  ---- 10 sync Request--Max Size

{00:00:58.1804677}====>HttpClinet    
{00:00:58.0710444}====>WebRequest    
{00:00:38.4170938}====>WebClient
    
---------------------Stage 6  ---- 10 sync Request--Max Size

{00:01:04.9964278}====>HttpClinet    
{00:00:59.1429764}====>WebRequest    
{00:00:32.0584836}====>WebClient

_____ WebClient 更快()

var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetHttpClient();
            CallPostHttpClient();
        }

        stopWatch.Stop();

        var httpClientValue = stopWatch.Elapsed;

        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetWebRequest();
            CallPostWebRequest();
        }

        stopWatch.Stop();

        var webRequesttValue = stopWatch.Elapsed;


        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {

            CallGetWebClient();
            CallPostWebClient();

        }

        stopWatch.Stop();

        var webClientValue = stopWatch.Elapsed;

//--------------函数

private void CallPostHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.PostAsync("PostJson", null);
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private void CallGetHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.GetAsync("getjson");
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private string CallGetWebRequest()
    {
        var request = (HttpWebRequest)WebRequest.Create("https://localhost:44354/api/test/getjson");

        request.Method = "GET";
        request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

        var content = string.Empty;

        using (var response = (HttpWebResponse)request.GetResponse())
        {
            using (var stream = response.GetResponseStream())
            {
                using (var sr = new StreamReader(stream))
                {
                    content = sr.ReadToEnd();
                }
            }
        }

        return content;
    }
    private string CallPostWebRequest()
    {

        var apiUrl = "https://localhost:44354/api/test/PostJson";


        HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(apiUrl));
        httpRequest.ContentType = "application/json";
        httpRequest.Method = "POST";
        httpRequest.ContentLength = 0;

        using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
        {
            using (Stream stream = httpResponse.GetResponseStream())
            {
                var json = new StreamReader(stream).ReadToEnd();
                return json;
            }
        }

        return "";
    }

    private string CallGetWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/getjson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.DownloadString(apiUrl);


        return json;
    }

    private string CallPostWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/PostJson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.UploadString(apiUrl, "");


        return json;
    }

【讨论】:

  • 见上面加布里埃尔的评论。简而言之,如果你创建一个 HttpClient 实例并重用它,HttpClient 会快得多。
  • 此外,HttpWebRequest 调用 .NET Core 中的 HttpClient。哪个是前进的唯一平台
  • 事实上,HttpWebRequests 甚至在 .NET Framework 中也使用了 HttpClient,尽管它有 bug。那GetResponse()calls HttpClient underneathblocks.GetAwaiter().GetResult()
猜你喜欢
  • 2021-02-15
  • 1970-01-01
  • 1970-01-01
  • 2021-01-11
  • 1970-01-01
  • 1970-01-01
  • 2017-07-25
  • 2019-06-17
  • 2016-05-31
相关资源
最近更新 更多