使用HttpClient C#对许多不同网站进行快速web请求



我的团队维护了一个工具,负责对1000多个不同的客户端网站进行快速验证。该工具是一个Windows服务(.NET 4.5.2,C#),它从队列中读取请求,并对每个请求执行"运行状况检查"。它通常每分钟处理500多个请求,但也可以负责更多的请求。每个请求需要一两秒钟才能执行。

请求包含Uri和进行健康检查所需的凭据。健康检查是针对具有凭据的AUTH页面的POST(应用程序具有自定义身份验证,它不是基于标头的身份验证),然后对主页进行GET,并快速验证它是我们期望的主页。然后它转到应用程序中的状态页,并对此进行一些快速检查。GET请求必须使用auth-post中Set Cookie标头中的Cookie。

随着工具的扩展,我们一直遇到性能问题。它目前为每个帖子创建一个新的HttpWebRequest对象,并进入这个过程。有一个共享的CookieContainer,由第一个帖子填充,这样我们就可以进入主页,然后进入状态页。

我想将此服务更改为使用.NET 4.5中提供的HttpClient对象。我在网上读到的所有问题都表明,您希望避免快速创建和销毁HttpClients。您宁愿让一个实例在应用程序的生命周期内保持活动状态。我遇到的问题是,HttpClient似乎在一个端点上工作得很好,而不是很多。

我已经研究了几个选项,不确定哪一个是最好的:

  1. 为每个请求创建一个新的HttpClient,并在该请求的持续时间内使用它。这意味着它将存活几秒钟,并可用于3次通话。这并不容易实现,但我担心每分钟创建和销毁数百个HttpClients的开销
  2. 通过避免使用BaseAddres,并使用客户端使用SendAsync传递HttpRequestMessages,确定是否可以为不同的端点使用一个HttpClient实例我还没能用这种方法算出饼干。为了避免让HttpClient存储cookie,我在HttpClientHandler中将UseCookies设置为false,并尝试通过HttpRequest/ResponseMessages中的头来管理cookie,但当UseCookies设置为false时,HttpClient似乎只是剥离了cookie,因此我无法在请求之间传递cookieedit:cookie工作正常,因为它们是按域存储的
  3. 将几百个不同的HttpClient实例存储在某种字典中,并在请求到来时为每个Uri提取相应的实例。不过,我不确定这方面的内存开销。此外,每个唯一的Uri每5分钟只验证一次,所以我不确定每5分钟使用一次HttpClient是否会保持不必要数量的端口打开
  4. 继续使用HttpWebRequests。也许这种老方法在这种情况下仍然表现得更好

如果有人遇到过类似的问题,我希望能就如何处理这一问题提供一些意见。

谢谢!

为每个请求创建新的HttpClients的问题是HttpClientHandler将关闭底层TCP/IP连接。但是,如果您对一个主机的3个请求使用每个HttpClient,然后访问不同的主机,那么当您移动到新主机时,保持连接打开并没有帮助。因此,您可能不会看到每个主机一个客户端的性能问题。HttpClient本身是一个非常轻量级的对象。创建一个不会花太多钱。

然而,HttpClient只是将实际工作委托给HttpClientHandler,后者在幕后使用HttpWebRequest,因此不太可能有比直接使用HttpWebRequest更好的性能。

如果您正在寻找更好的性能,那么我建议您考虑将HttpClientHandler替换为新的WinHttpHandler,该WinHttpHandler绕过HttpWebRequest并直接进入Win32 API进行调用。

完整的源代码可用于GitHub上的WinHttpHandler,因此您可以确切地看到它是如何处理cookie和凭据的。

如果你能用WinHttpHandler获得更好的性能,我真的很想听听。

首先,您需要修改其中的哪些部分以满足您的需求?

var urisToCheck = new List<Uri>(); //get these somehow
//basic auth work?
var credentials = new NetworkCredential("user", "pass");
var handler = new HttpClientHandler { Credentials = credentials };
var client = new HttpClient(handler);
Parallel.ForEach(urisToCheck,
    async uri =>
    {
        var response = await client.GetAsync(uri.AbsoluteUri);
        //check for whatever you want here
    }
 );

这里是我的基本API客户端,它对每个请求使用相同的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;
}

将此对象作为singleton注册到依赖项注入库中。重用它是安全的,因为它是无状态的。

不要为每个请求重新创建HTTPClient

尽可能多地重用Httpclient

最新更新