在HttpClient和WebClient之间决定


218

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

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

我搜索了WebClient和之间的差异HttpClient该站点提到单个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调用中调用。首先,调用在不同的线程中运行,并且不会阻止UI。所以,我可以坚持同步通话吗?
  2. 上面的代码在我的localbox中运行。在产品设置中,将涉及DNS和代理查找。使用HttpClientover 有什么优势WebClient吗?
  3. HttpClient并发比WebClient?从测试结果中,我看到 WebClient同步调用的性能更好。
  4. HttpClient如果我们升级到.Net 4.5,将是更好的设计选择?性能是关键的设计因素。

5
您的测试不公平,GetDataFromHttpClientAsync因为它首先运行,其他调用会受益于潜在的缓存数据(无论是在本地计算机上还是在您与目标之间的任何透明代理中),而且速度会更快。另外,在正确的条件下,var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result;由于耗尽线程池线程,可能导致死锁。您绝不应该阻止依赖ThreadPool线程中线程池的活动,而应该阻止await它将线程返回到池中。
Scott Chamberlain

1
带有Web API客户端的HttpClient非常适合JSON / XML REST客户端。
科里·纳尔逊

@Scott Chamberlain-感谢您的回复。由于所有测试调用均在Parallel.Foreach中运行,因此无法保证哪个会先运行。同样,如果对服务的第一次调用是从GetDataFromHttpClientAsync进行的,则从GetDataFromHttpClientAsync进行的所有后续调用都应该受益于缓存并且运行速度更快。我没有看到结果。RGd等待中,我们仍在使用4.0。我同意您的观点,认为HttpClient以同步方式会导致死锁,因此我出于设计考虑不考虑该选项。
user3092913 2013年

@CoryNelson您能否详细说明为什么带有Web API客户端的HttpClient对于JSON / XML REST客户端而言是很棒的?
user3092913 2013年

2
这里有间的HttpClient和Web客户端的区别几句话:blogs.msdn.com/b/henrikn/archive/2012/02/11/...
JustAndrei

Answers:


243

我生活在F#和Web API领域。

Web API发生了很多好事,特别是以安全性的消息处理程序等形式。

我知道我只是一种意见,但我只建议在HttpClient以后的工作中使用。也许有某种方法可以直接利用其他System.Net.Http组件,而无需直接使用该程序集,但是我无法想象这将如何工作。

说到比较这两个

  • HttpClient比WebClient更接近HTTP。
  • HttpClient并不是Web客户端的完整替代品,因为WebClient提供了诸如报告进度,自定义URI方案和进行FTP调用之类的东西,但是HttpClient却没有。
+--------------------------------------------+--------------------------------------------+
|               WebClient                    |               HttpClient                   |
+--------------------------------------------+--------------------------------------------+
| Available in older versions of .NET        | .NET 4.5 only.  Created to support the     |
|                                            | growing need of the Web API REST calls     |
+--------------------------------------------+--------------------------------------------+
| WinRT applications cannot use WebClient    | HTTPClient can be used with WinRT          |
+--------------------------------------------+--------------------------------------------+
| Provides progress reporting for downloads  | No progress reporting for downloads        |
+--------------------------------------------+--------------------------------------------+
| Does not reuse resolved DNS,               | Can reuse resolved DNS, cookie             |
| configured cookies                         | configuration and other authentication     |
+--------------------------------------------+--------------------------------------------+
| You need to new up a WebClient to          | Single HttpClient can make concurrent      |
| make concurrent requests.                  | requests                                   |
+--------------------------------------------+--------------------------------------------+
| Thin layer over WebRequest and             | Thin layer of HttpWebRequest and           |
| WebResponse                                | HttpWebResponse                            |
+--------------------------------------------+--------------------------------------------+
| Mocking and testing WebClient is difficult | Mocking and testing HttpClient is easy     |
+--------------------------------------------+--------------------------------------------+
| Supports FTP                               | No support for FTP                         |
+--------------------------------------------+--------------------------------------------+
| Both Synchronous and Asynchronous methods  | All IO bound methods in                    |
| are available for IO bound requests        | HTTPClient are asynchronous                |
+--------------------------------------------+--------------------------------------------+

如果您使用的是.NET 4.5,请使用Microsoft提供给开发人员的HttpClient的异步优势。HttpClient与HTTP的服务器端兄弟非常对称,它们是HttpRequest和HttpResponse。

更新:使用新HttpClient API的5个理由:

  • 强类型标题。
  • 共享缓存,Cookie和凭据
  • 访问cookie和共享cookie
  • 控制缓存和共享缓存。
  • 将您的代码模块注入ASP.NET管道。清洁和模块化的代码。

参考

C#5.0约瑟夫·阿尔巴哈里(Joseph Albahari)

(Channel9 — Video Build 2013)

使用新的HttpClient API连接到Web服务的五个主要原因

WebClient与HttpClient与HttpWebRequest


4
应该提到的是,HttpClient也可用于.NET 4.0
Todd Menier 2015年

2
这并不能解释为什么WebClient似乎比HttpClient快几个数量级。也WebClient似乎有异步方法了。
2015年

8
@crush这是因为OP正在为每个单个请求创建一个新的HttpClient实例。相反,您应在应用程序的生存期内使用HttpClient的单个实例。请参阅stackoverflow.com/a/22561368/57369
Gabriel

6
值得注意的WebClient是,没有可用,.Net Core但是可用HttpClient
普拉纳夫·辛格

3
由于.Net Core 2.0 WebClient(在成千上万的其他API中)已恢复可用。
CoderBang

56

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

  • 具有良好的异步编程模型
  • 由基本上是HTTP的发明者之一的Henrik F Nielson进行研究,并且他设计了API,因此您可以轻松遵循HTTP标准,例如生成符合标准的标头
  • 在.Net Framework 4.5中,因此在可预见的将来它具有一定程度的支持
  • 如果要在其他平台(.Net 4.0,Windows Phone等)上使用它,它也具有库的xcopyable / portable-framework版本。

如果编写的Web服务正在对其他Web服务进行REST调用,则应该对所有REST调用都使用异步编程模型,以免遇到线程匮乏的情况。您可能还想使用具有异步/等待支持的最新C#编译器。

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


如果它有办法切换代理,那就

3

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

我进行了快速性能测试,以了解WebClient(同步调用),HttpClient(同步和异步)的性能。这是结果。

我认为在考虑未来时,即长时间运行的流程,响应性GUI等,这是一个巨大的差异(增加了框架4.5所建议的好处-以我的实际经验,这在IIS上要快得多)


4
WebClient确实在最新的.NET版本中具有异步功能。我想知道为什么它在如此大规模的情况下表现优于HttpClient。
2015年

1
根据stackoverflow.com/a/4988325/1662973的说法,除了一个是另一个的抽象这一事实外,似乎是相同的。也许,这取决于如何使用/加载对象。最短时间确实支持这样的说法,即webclient实际上是HttpClient的抽象,因此存在毫秒级的开销。该框架在如何真正合并或处置Web客户端方面可能是“鬼nea”的。
安东尼·霍恩

2

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

和结果Call Rest Web Api Benchmark

---------------------阶段1 ---- 10请求

{00:00:17.2232544} ====> HttpClinet

{00:00:04.3108986} ====> WebRequest

{00:00:04.5436889} ====> WebClient

---------------------阶段1 ---- 10请求-尺寸小

{00:00:17.2232544} ====> HttpClinet

{00:00:04.3108986} ====> WebRequest

{00:00:04.5436889} ====> WebClient

---------------------阶段3 ---- 10同步请求-小

{00:00:15.3047502} ====> HttpClinet

{00:00:03.5505249} ====> WebRequest

{00:00:04.0761359} ====> WebClient

---------------------阶段4 ---- 100同步请求-尺寸小

{00:03:23.6268086} ====> HttpClinet

{00:00:47.1406632} ====> WebRequest

{00:01:01.2319499} ====> WebClient

---------------------阶段5 ---- 10同步请求-最大大小

{00:00:58.1804677} ====> HttpClinet

{00:00:58.0710444} ====> WebRequest

{00:00:38.4170938} ====> WebClient

---------------------阶段6 ---- 10同步请求-最大大小

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

1
请参阅上方的加百列评论。简而言之,如果您创建一个HttpClient实例并重用它,则HttpClient会快得多。
LT Dan


1

也许您可以以不同的方式考虑问题。WebClient并且HttpClient是同一事物的本质不同的实现。我建议在整个应用程序中使用IoC容器实现依赖注入模式。您应该使用比低级别HTTP传输更高抽象级别的客户端接口。您可以编写同时使用和WebClientHttpClient然后使用IoC容器通过config注入实现。

这将使您可以轻松地进行切换HttpClientWebClient以便能够在生产环境中进行客观测试。

所以像这样的问题:

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

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

/// <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
}

完整代码

HttpClient实施

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

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


0

2020年的不受欢迎观点:

当涉及到ASP.NET应用程序时,我仍然更喜欢WebClientHttpClient因为:

  1. 现代实现带有异步/可等待的基于任务的方法
  2. 具有较小的内存占用空间,并且快2到5倍(其他答案已经提到过),特别是在您不能像其他评论者所建议的那样“ 在应用程序的生存期内重用HttpClient的单个实例的情况下。ASP.NET就是其中一种情况-没有“应用程序生存期”,只有请求的生存期。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.