我们的web应用程序运行在。net Framework 4.0中。UI通过Ajax调用调用控制器方法。

我们需要使用来自供应商的REST服务。我正在评估在。net 4.0中调用REST服务的最佳方式。REST服务需要一个基本的身份验证方案,它可以返回XML和JSON两种格式的数据。

对上传/下载大数据没有任何要求,我认为未来也不会有任何要求。我查看了一些用于REST消费的开源代码项目,并没有发现它们有任何价值来证明项目中的额外依赖。我开始评估WebClient和HttpClient。我从NuGet下载了。net 4.0的HttpClient。

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

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

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

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

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

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

Code

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

我的问题

The REST calls return in 3-4 seconds which is acceptable. Calls to REST service are initiated in the controller methods which gets invoked from Ajax calls. To begin with, the calls runs in a different thread and doesn't block the UI. So, can I just stick with synchronous calls? The above code was run in my localbox. In a production setup, DNS and proxy lookup will be involved. Is there an advantage of using HttpClient over WebClient? Is HttpClient concurrency better than WebClient? From the test results, I see WebClient synchronous calls perform better. Will HttpClient be a better design choice if we upgrade to .NET 4.5? Performance is the key design factor.


当前回答

说到ASP。我仍然更喜欢WebClient而不是HttpClient,因为:

The modern implementation comes with async/awaitable task-based methods Has smaller memory footprint and 2-5 times faster (other answers already mention that) It's suggested to "reuse a single instance of HttpClient for the lifetime of your application". But ASP.NET has no "lifetime of application", only lifetime of a request. The current guidance for ASP.NET 5 is to use HttpClientFactory, but it can only be used via dependency injection. Some people want a simpler solution. Most importantly, if you're using one singleton instance of HttpClient through the lifetime of the app like MS suggests - it has known issues. For example the DNS caching issue - HttpClient simply ignores the TTL and caches DNS "forever". There are workarounds, however. If you'd like to learn more about the issues and confusion with HttpClient just read this comment at Microsoft GitHub.

其他回答

HttpClient是较新的api,它的优点是

拥有良好的异步编程模型 Henrik F Nielson是HTTP的发明者之一,他设计的API让你很容易遵循HTTP标准,例如生成符合标准的头文件 是在.NET框架4.5中,所以它在可预见的未来有一定程度的支持 还有xcopyable/portable-framework版本的库,如果你想在其他平台上使用它- . net 4.0, Windows Phone等。

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

注意:AFAIK,它并不是性能更好。如果你创建一个公平的测试,它可能会有类似的表现。

首先,我并不是WebClient vs. HttpClient的权威。其次,从你上面的评论来看,似乎WebClient只是同步的,而HttpClient两者都是。

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

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

Perhaps you could think about the problem in a different way. WebClient and HttpClient are essentially different implementations of the same thing. What I recommend is implementing the Dependency Injection pattern with an IoC Container throughout your application. You should construct a client interface with a higher level of abstraction than the low level HTTP transfer. You can write concrete classes that use both WebClient and HttpClient, and then use the IoC container to inject the implementation via config.

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

像这样的问题:

如果我们升级到。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。运行使WebClient在其实现中异步运行。

Dependency Injection, when done well helps alleviate the problem of having to make low level decisions upfront. Ultimately, the only way to know the true answer is try both in a live environment and see which one works the best. It's quite possible that WebClient may work better for some customers, and HttpClient may work better for others. This is why abstraction is important. It means that code can quickly be swapped in, or changed with configuration without changing the fundamental design of the app.

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

我已经在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;
}

HttpClientFactory

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

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

我知道这不是一个直接的答案——但是你最好从这里开始,而不是到处都是新的HttpClient(…)。