我的团队维护了一个工具,负责对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
似乎在一个端点上工作得很好,而不是很多。
我已经研究了几个选项,不确定哪一个是最好的:
- 为每个请求创建一个新的
HttpClient
,并在该请求的持续时间内使用它。这意味着它将存活几秒钟,并可用于3次通话。这并不容易实现,但我担心每分钟创建和销毁数百个HttpClients
的开销 - 通过避免使用
BaseAddres
,并使用客户端使用SendAsync
传递HttpRequestMessages
,确定是否可以为不同的端点使用一个HttpClient
实例我还没能用这种方法算出饼干。为了避免让edit:cookie工作正常,因为它们是按域存储的HttpClient
存储cookie,我在HttpClientHandler
中将UseCookies
设置为false,并尝试通过HttpRequest
/ResponseMessages
中的头来管理cookie,但当UseCookies
设置为false
时,HttpClient
似乎只是剥离了cookie,因此我无法在请求之间传递cookie - 将几百个不同的
HttpClient
实例存储在某种字典中,并在请求到来时为每个Uri
提取相应的实例。不过,我不确定这方面的内存开销。此外,每个唯一的Uri
每5分钟只验证一次,所以我不确定每5分钟使用一次HttpClient
是否会保持不必要数量的端口打开 - 继续使用
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