ChatGPT解决这个技术问题 Extra ChatGPT

在 HttpClient 和 WebClient 之间做出决定 [关闭]

关闭。这个问题需要更加集中。它目前不接受答案。想改进这个问题?更新问题,使其仅通过编辑此帖子专注于一个问题。上个月关门。社区上个月审查了是否重新打开此问题并将其关闭:原始关闭原因未解决 改进此问题

我们的 Web 应用程序在 .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);
        }
    }
}

我的问题

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

您的测试对 GetDataFromHttpClientAsync 不公平,因为它首先运行,其他调用受益于潜在的缓存数据(无论是在本地计算机上还是在您和目标之间的任何透明代理上)并且速度会更快。此外,在正确的条件下,var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result; 可能会由于您耗尽线程池线程而导致死锁。您永远不应该阻塞依赖于 ThreadPool 线程中的线程池的活动,您应该改为 await 以便它将线程返回到池中。
带有 Web API 客户端的 HttpClient 非常适合 JSON/XML REST 客户端。
这里简单介绍一下 HttpClient 和 WebClient 的区别:blogs.msdn.com/b/henrikn/archive/2012/02/11/…
docs.microsoft.com/en-us/dotnet/api/… 建议使用 HttpClient 而不是 WebClient 进行新开发。 .NET Framework 和 .NET Core 都是如此。
这个问题至少从 2018 年开始就没有实际意义,因为即使是 .NET Framework 的 HttpWebRequest 以及扩展的 WebClient actually use HttpClient WebClient 和 HttpWebRequest 几年来只是 HttpClient 的兼容性包装器,而 .NET Framework 版本的套接字耗尽错误已在 .网络核心

3
3 revs, 2 users 61%

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

具有良好的异步编程模型

由 Henrik F Nielson 进行工作,他基本上是 HTTP 的发明者之一,他设计了 API,因此您可以轻松地遵循 HTTP 标准,例如生成符合标准的标头

在 .NET 框架 4.5 中,因此它对可预见的未来有一定程度的支持

如果您想在其他平台(.NET 4.0、Windows Phone 等)上使用它,还具有该库的 xcopyable/portable-framework 版本。

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

注意:AFAIK 的性能并不高。如果您创建一个公平的测试,它可能有点类似的性能。


如果它有办法切换代理,那将是疯狂的
虽然这是一个老问题,但它出现在我的搜索中,所以我想我会指出 Microsoft 为 .NET 5 中的 WebClient 提供的 documentation,“我们不建议您使用 WebClient用于新开发的类。改为使用 System.Net.Http.HttpClient 类。”
S
Simon_Weaver

HttpClientFactory

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

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

我知道这不是一个直接的答案 - 但你最好从这里开始而不是到处都是 new HttpClient(...)


值得强调的是,至少自 2018 年以来,即使是 .NET Framework 中的 HttpWebRequest 以及扩展的 WebClient use HttpClient,所以这个问题本质上是没有实际意义的
P
Peter Mortensen

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

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


鉴于 .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
P
Peter Mortensen

首先,具体来说,我不是 WebClient 与 HttpClient 的权威。其次,从您上面的评论来看,似乎表明 WebClient 仅是同步的,而 HttpClient 两者都是同步的。

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

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


WebClient 在最新的 .NET 版本中似乎确实具有异步功能。我想知道为什么它似乎在如此大规模上胜过 HttpClient。
根据stackoverflow.com/a/4988325/1662973,它似乎是相同的,除了一个是另一个的抽象这一事实。也许,这取决于对象的使用/加载方式。最小时间确实支持 webclient 实际上是 HttpClient 的抽象这一说法,因此存在毫秒级的开销。该框架可能是“偷偷摸摸地”如何真正汇集或处置 webclient。
C
Christian Findlay

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

这将使您能够轻松地在 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() 定义的目的?或者我在这里错过了什么?
我不明白这个问题
我很好奇您为什么选择在这里使用 Abstract,而不是与您的“响应”对象(通用和非通用)的接口
WebClient 间接使用 HttpClient,因为 HttpWebRequest uses HttpClient internally 甚至在 .NET Framework 中,至少从 2018 年开始。WebClient 和 HttpWebRrequest 在这一点上都只是过时的兼容性包装器。 WebClient 确实 具有适当的异步方法,因此它不需要 Task.Run
P
Peter Mortensen

我在 HttpClient、WebClient 和 HttpWebResponse 之间进行了基准测试,然后调用了 REST Web API。

结果:

调用 REST Web API 基准测试

---------------------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;

//-------------------------Functions

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。哪个是前进的唯一平台
事实上,即使在 .NET Framework 中,HttpWebRequests 也使用 HttpClient,尽管它存在错误。 GetResponse() calls HttpClient underneathblocks.GetAwaiter().GetResult()