在HttpClient和WebClient之间做出决定

时间:2013-12-11 21:20:42

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

我们的网络应用程序在.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实例(min - max)

  

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

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

  

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

代码

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-4s返回,这是可以接受的。呼叫REST 服务是在从中调用的控制器方法中启动的 ajax电话。首先,调用在不同的线程中运行 不会阻止UI。那么,我可以坚持使用同步电话吗?
  2. 以上代码在我的localbox中运行。在prod设置,DNS和代理 将涉及查找。使用HttpClient优于WebClient
  3. 有什么好处
  4. HttpClient并发性优于WebClient吗?从测试结果中,我看到WebClient同步调用效果更好。
  5. 如果我们升级到.Net 4.5,HttpClient会是更好的设计选择吗?性能是关键的设计因素。

7 个答案:

答案 0 :(得分:195)

我生活在F#和Web API世界中。

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

我知道我的意见只有一个,但是我只会建议您使用HttpClient进行任何工作。也许有一些方法可以利用System.Net.Http中出现的其他部分,而不直接使用该程序集,但我无法想象这在当时是如何工作的。

说到比较这两个

  • HttpClient比WebClient更接近HTTP。
  • HttpClient并不是Web客户端的完全替代品,因为有些内容包括报告进度,自定义URI方案以及WebClient提供的FTP调用 - 但是HttpClient没有。

enter image description here

如果您使用的是.NET 4.5,请使用Microsoft为开发人员提供的HttpClient的async优度。 HttpClient与HTTP的服务器端兄弟非常对称,即HttpRequest和HttpResponse。

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

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

<强>参考

C#5.0 Joseph Albahari

(Channel9 - Video Build 2013)

Five Great Reasons to Use the New HttpClient API to Connect to Web Services

WebClient vs HttpClient vs HttpWebRequest

答案 1 :(得分:55)

HttpClient是最新的API,它具有

的优点
  • 有一个很好的异步编程模型
  • 由Henrik F Nielson工作,他基本上是HTTP的发明者之一,他设计了API,因此您可以轻松地遵循HTTP标准,例如:生成符合标准的标题
  • 位于.Net framework 4.5中,因此对可预见的未来有一定程度的支持
  • 如果你想在其他平台上使用它,那么
  • 也有库的xcopyable / portable-framework版本 - .Net 4.0,Windows Phone等。

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

注意:它不是更高效的AFAIK。如果你创建一个公平的测试,它可能在某种程度上同样有效。

答案 2 :(得分:6)

HttpClientFactory

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

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

这不是我知道的直接答案-但是,最好从这里开始,而不是到处都以new HttpClient(...)结尾。

答案 3 :(得分:3)

首先,我不是WebClient与HttpClient的权威,特别是。其次,根据您上面的评论,似乎表明WebClient只是同步,而HttpClient是两者。

  

我做了一个快速的性能测试,以找到WebClient(同步调用),HttpClient(同步和异步)的执行方式。以下是结果。

我认为这对于未来的思考是一个巨大的差异,即长时间运行的流程,响应式GUI等等(增加你建议的框架4.5的好处 - 在我的实际经验中,在IIS上的速度要快得多)

答案 4 :(得分:2)

从2020年开始不受欢迎的观点:

当涉及到ASP.NET应用 时,与WebClient相比,我仍然更喜欢HttpClient,因为:

  1. 现代的实现带有基于异步/等待任务的方法
  2. 具有较小的内存占用空间,并且速度提高了2到5倍(其他答案已经提到了这一点)
  3. 建议“ 重复使用一个 ”,但是ASP.NET没有“应用程序的生命周期”,只有请求的生命周期。

答案 5 :(得分:1)

也许您可以以不同的方式考虑问题。 WebClientHttpClient本质上是同一事物的不同实现。我建议在整个应用程序中使用Dependency Injection pattern来实现IoC Container。您应该使用比低级别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可能对其他客户更好。这就是为什么抽象很重要的原因。这意味着无需更改应用程序的基本设计即可快速交换代码或通过配置更改代码。

答案 6 :(得分:0)

我在HttpClient,WebClient,HttpWebResponse之间建立了基准,然后调用Rest Web Api

和结果 致电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;
    }