HttpClient 和 HttpWebRequest 哪个性能更好、更安全,连接也更少?

47

我发现一个单一的HttpClient可以被多个请求共享。如果共享,并且请求是发送到同一个目的地,多个请求可以重用连接。而WebRequest每次请求都需要重新创建连接。

我还查阅了一些使用HttpClient的其他方式的文档示例。

以下文章概括了高速NTLM身份验证连接共享:HttpWebRequest.UnsafeAuthenticatedConnectionSharing

我尝试过的可能实现如下:

A)

private WebRequestHandler GetWebRequestHandler()
{
    CredentialCache credentialCache = new CredentialCache();
    credentialCache.Add(ResourceUriCanBeAnyUri, "NTLM", CredentialCache.DefaultNetworkCredentials);
    WebRequestHandler handler = new WebRequestHandler
    {
        UnsafeAuthenticatedConnectionSharing = true,
        Credentials = credentialCache
    };

    return handler;
}

using (HttpClient client = new HttpClient(GetWebRequestHandler(), false))
{
}

B)

using (HttpClient client = new HttpClient)
{
}

C)

HttpWebRequest req = (HttpWebRequest)WebRequest.Create("some uri string")

我希望能得到帮助,了解该采取哪种方法才能实现最大性能,最小化连接并确保不影响安全性。


HttpClient是城里新来的酷小子,据说是最好的,支持异步/任务,并且比其他工具更加便携(还有WebClient)。然而,它需要.NET 4.5+。话虽如此,如果使用得当,在原始性能方面,你不应该看到太大的差异。 - Simon Mourier
7
如果你正在使用 HttpClient,建议阅读这篇文章:YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE。文章会帮助你更好地使用 HttpClient,避免不必要的稳定性问题。 - George Vovos
毫无疑问,应该选择HttpClient。除了它可以处理连接池和内置的异步/等待之外,通过使用Handlers,它还提供更多的灵活性,并且使用HttpClient编写单元测试也更加容易。 - Duy
3
HttpClient旨在在应用程序的整个生命周期中仅被实例化一次并重复使用。特别是在服务器应用程序中,对于每个请求创建新的HttpClient实例会在高负载下消耗可用的套接字数量。这将导致SocketException错误。示例:https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client - SergeyT
先看这个链接:https://learn.microsoft.com/en-us/archive/blogs/timomta/controlling-the-number-of-outgoing-connections-from-httpclient-net-core-or-full-framework DefaultConnectionLimit参数可能会导致客户端瓶颈。 - Larry
3个回答

42
如果在异步操作中使用它们,无论是哪一个都可以提高性能,因为它不会阻塞资源等待响应,而且您将获得良好的吞吐量。
由于HttpClient内置了异步方法,而且你不必担心编写Begin/End方法,所以它比HttpWebRequest更受欢迎。
基本上,当你使用异步调用(使用这两个类之一),它不会阻塞资源等待响应,任何其他请求都会利用资源进行进一步的调用。
还要注意的一点是,不要在“using”块中使用HttpClient,以便让同一资源可以被用于其他网络请求的重复利用。
更多信息请参见以下线程: Do HttpClient and HttpClientHandler have to be disposed?

2
是的,我自己也刚学会如何处理 HttpClient。看起来这是当前的热门讨论话题。简而言之,不要将其 dispose! - trnelson
2
HttpWeRequest 也支持异步操作,因此这个答案已经过时了。 - Alex from Jitbit

9

这是我的ApiClient,它只创建一次HttpClient。请将此对象注册为单例到您的依赖注入库中。它是无状态的,因此安全可重用。请勿为每个请求重新创建HTTPClient。尽可能多地重用Httpclient。

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);   
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

使用方法:

using ( var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

11
应该使用await而不是.Result来调用这些方法。 - Snak

0
  1. 你的实现'A'存在问题。从GetWebRequestHandler()返回的实例的生命周期很短(也许只是为了举例而已?)。如果这是有意为之的,那么就会抵消在HttpClient构造函数的第二个参数中传递false的作用。false的值告诉HttpClient不要释放底层的HttpMessageHandler(这有助于扩展,因为它不会关闭请求的端口)。当然,这是在假设HttpMessageHandler的生命周期足够长,以便您利用不打开/关闭端口的好处的情况下。这对服务器的可扩展性有很大的影响。因此,我建议选择'D'选项。

  2. 还有一种你没有列出来的选项'D' - 将Httpclient实例设置为static并在所有api调用中重复使用。从内存分配和GC的角度来看,这更加高效 - 以及客户端端口的开放。您不必为HttpClient实例(及其所有底层对象)分配内存的开销,因此也避免了通过GC进行清理的开销。

请参考我提供的关于类似问题的答案 - 在WebAPI客户端中每次调用创建新的HttpClient的开销是多少?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接