ASP.NET MVC 中的 HttpClient 单例实现
HttpClient Singleton Implementation in ASP.NET MVC
看完这篇blog post and thisofficial note on www.asp.net:
HttpClient is intended to be instantiated once and re-used throughout
the life of an application. Especially in server applications,
creating a new HttpClient instance for every request will exhaust the
number of sockets available under heavy loads. This will result in
SocketException errors.
我发现我们的代码在每次调用时都会处理 HttpClient。我正在更新我们的代码以便我们重用 HttClient,但我担心我们的实现而不是线程安全的。
这是新代码的当前草案:
对于单元测试,我们为 HttpClient 实现了一个包装器,消费者调用包装器:
public class HttpClientWrapper : IHttpClient
{
private readonly HttpClient _client;
public Uri BaseAddress
{
get
{
return _client.BaseAddress;
}
set
{
_client.BaseAddress = value;
}
}
public HttpRequestHeaders DefaultRequestHeaders
{
get
{
return _client.DefaultRequestHeaders;
}
}
public HttpClientWrapper()
{
_client = new HttpClient();
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, String userOrProcessName)
{
IUnityContainer container = UnityCommon.GetContainer();
ILogService logService = container.Resolve<ILogService>();
logService.Log(ApplicationLogTypes.Debug, JsonConvert.SerializeObject(request), userOrProcessName);
return _client.SendAsync(request);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing && _client != null)
{
_client.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
这是一个调用的服务:
public class EnterpriseApiService : IEnterpriseApiService
{
private static IHttpClient _client;
static EnterpriseApiService()
{
IUnityContainer container = UnityCommon.GetContainer();
IApplicationSettingService appSettingService = container.Resolve<IApplicationSettingService>();
_client = container.Resolve<IHttpClient>();
}
public EnterpriseApiService() { }
public Task<HttpResponseMessage> CallApiAsync(Uri uri, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeAccept = true)
{
IUnityContainer container = UnityCommon.GetContainer();
HttpRequestMessage request;
_client.BaseAddress = new Uri(uri.GetLeftPart(UriPartial.Authority));
if (addJsonMimeAccept)
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request = new HttpRequestMessage(method, uri.AbsoluteUri);
// Removed logic that built request with content, requestHeaders and method
return _client.SendAsync(request, UserOrProcessName);
}
}
我的问题:
- 这是重用 HttpClient 对象的合适方法吗?
- 是否为 EnterpriseApiService 的所有实例共享静态 _httpClient 字段(使用静态构造函数填充)?我想确认因为被实例方法调用。
- 调用 CallApiAsync() 时,当它对静态 HttpClient 进行更改时,例如“_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"))”,这些值是否会被另一个进程覆盖在调用最后一行“_client.SendAsync”之前?我担心在处理 CallApiAsync() 的过程中静态实例会被更新。
- 既然它正在调用 SendAsync(),我们能否保证将响应映射回正确的调用者?我想确认回复不会转到另一个来电者。
更新:
因为我已经删除了 USING 语句,并且 Garage Collection 不调用 Dispose,所以我将采用更安全的方法,即在该方法中创建一个新实例。即使在线程生命周期内重用 HttpClient 的实例,也需要对逻辑进行大量修改,因为该方法每次调用都会设置 HttpClient 属性。
你真的想要一个实例吗?
我认为您不希望在整个应用程序范围内使用一个实例。您希望每个线程一个实例。否则你不会得到很好的性能!此外,这将解决您的问题 #3 和 #4,因为没有两个线程会同时访问同一个 HttpClient。
你不需要单例
只需使用 Container.Resolve with the PerThreadLifetimeManager.
既然它正在调用 SendAsync(),我们能保证响应映射回正确的调用者吗?我想确认回复没有转到另一个来电者。
这将通过回调指针处理。它与将 HttpClient 用作单例无关。此处有更多详细信息 -
对于那些有幸使用 .NET Core 的人来说,这是相当简单的。
正如 John Wu 雄辩地指出的那样,您不需要一个单例本身,而是每个请求一个单例。因此,AddScoped<TService>()
方法就是您所追求的。
在您的 ConfigureServices(IServiceCollection services)
方法中:
services.AddScoped<HttpClient>();
消费:
public class HomeController
{
readonly HttpClient client;
public HomeController (HttpClient client)
{
this.client = client;
}
//rest of controller code
}
这是我用的
public abstract class BaseClient : IDisposable
{
private static object locker = new object();
private static volatile HttpClient httpClient;
protected static HttpClient Client
{
get
{
if (httpClient == null)
{
lock (locker)
{
if (httpClient == null)
{
httpClient = new HttpClient();
}
}
}
return httpClient;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (httpClient != null)
{
httpClient.Dispose();
}
httpClient = null;
}
}
}
它在扩展方法中的使用是这样的:
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
this HttpClient httpClient, string url, T data, string token, IDictionary<string, string> dsCustomHeaders = null)
{
ThrowExceptionIf.Argument.IsNull(httpClient, nameof(httpClient));
var dataAsString = JsonConvert.SerializeObject(data);
var httpReqPostMsg = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(dataAsString, Encoding.UTF8, "application/json")
};
httpReqPostMsg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpReqPostMsg.Headers.Add(Constants.TelemetryCorrelationKey, Utilities.GetRequestCorrelationId());
if (dsCustomHeaders != null) {
foreach (var keyValue in dsCustomHeaders)
{
httpReqPostMsg.Headers.Add(keyValue.Key, keyValue.Value);
}
}
return httpClient.SendAsync(httpReqPostMsg);
}
看完这篇blog post and thisofficial note on www.asp.net:
HttpClient is intended to be instantiated once and re-used throughout the life of an application. Especially in server applications, creating a new HttpClient instance for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors.
我发现我们的代码在每次调用时都会处理 HttpClient。我正在更新我们的代码以便我们重用 HttClient,但我担心我们的实现而不是线程安全的。
这是新代码的当前草案:
对于单元测试,我们为 HttpClient 实现了一个包装器,消费者调用包装器:
public class HttpClientWrapper : IHttpClient
{
private readonly HttpClient _client;
public Uri BaseAddress
{
get
{
return _client.BaseAddress;
}
set
{
_client.BaseAddress = value;
}
}
public HttpRequestHeaders DefaultRequestHeaders
{
get
{
return _client.DefaultRequestHeaders;
}
}
public HttpClientWrapper()
{
_client = new HttpClient();
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, String userOrProcessName)
{
IUnityContainer container = UnityCommon.GetContainer();
ILogService logService = container.Resolve<ILogService>();
logService.Log(ApplicationLogTypes.Debug, JsonConvert.SerializeObject(request), userOrProcessName);
return _client.SendAsync(request);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing && _client != null)
{
_client.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
这是一个调用的服务:
public class EnterpriseApiService : IEnterpriseApiService
{
private static IHttpClient _client;
static EnterpriseApiService()
{
IUnityContainer container = UnityCommon.GetContainer();
IApplicationSettingService appSettingService = container.Resolve<IApplicationSettingService>();
_client = container.Resolve<IHttpClient>();
}
public EnterpriseApiService() { }
public Task<HttpResponseMessage> CallApiAsync(Uri uri, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeAccept = true)
{
IUnityContainer container = UnityCommon.GetContainer();
HttpRequestMessage request;
_client.BaseAddress = new Uri(uri.GetLeftPart(UriPartial.Authority));
if (addJsonMimeAccept)
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request = new HttpRequestMessage(method, uri.AbsoluteUri);
// Removed logic that built request with content, requestHeaders and method
return _client.SendAsync(request, UserOrProcessName);
}
}
我的问题:
- 这是重用 HttpClient 对象的合适方法吗?
- 是否为 EnterpriseApiService 的所有实例共享静态 _httpClient 字段(使用静态构造函数填充)?我想确认因为被实例方法调用。
- 调用 CallApiAsync() 时,当它对静态 HttpClient 进行更改时,例如“_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"))”,这些值是否会被另一个进程覆盖在调用最后一行“_client.SendAsync”之前?我担心在处理 CallApiAsync() 的过程中静态实例会被更新。
- 既然它正在调用 SendAsync(),我们能否保证将响应映射回正确的调用者?我想确认回复不会转到另一个来电者。
更新: 因为我已经删除了 USING 语句,并且 Garage Collection 不调用 Dispose,所以我将采用更安全的方法,即在该方法中创建一个新实例。即使在线程生命周期内重用 HttpClient 的实例,也需要对逻辑进行大量修改,因为该方法每次调用都会设置 HttpClient 属性。
你真的想要一个实例吗?
我认为您不希望在整个应用程序范围内使用一个实例。您希望每个线程一个实例。否则你不会得到很好的性能!此外,这将解决您的问题 #3 和 #4,因为没有两个线程会同时访问同一个 HttpClient。
你不需要单例
只需使用 Container.Resolve with the PerThreadLifetimeManager.
既然它正在调用 SendAsync(),我们能保证响应映射回正确的调用者吗?我想确认回复没有转到另一个来电者。
这将通过回调指针处理。它与将 HttpClient 用作单例无关。此处有更多详细信息 -
对于那些有幸使用 .NET Core 的人来说,这是相当简单的。
正如 John Wu 雄辩地指出的那样,您不需要一个单例本身,而是每个请求一个单例。因此,AddScoped<TService>()
方法就是您所追求的。
在您的 ConfigureServices(IServiceCollection services)
方法中:
services.AddScoped<HttpClient>();
消费:
public class HomeController
{
readonly HttpClient client;
public HomeController (HttpClient client)
{
this.client = client;
}
//rest of controller code
}
这是我用的
public abstract class BaseClient : IDisposable
{
private static object locker = new object();
private static volatile HttpClient httpClient;
protected static HttpClient Client
{
get
{
if (httpClient == null)
{
lock (locker)
{
if (httpClient == null)
{
httpClient = new HttpClient();
}
}
}
return httpClient;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (httpClient != null)
{
httpClient.Dispose();
}
httpClient = null;
}
}
}
它在扩展方法中的使用是这样的:
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
this HttpClient httpClient, string url, T data, string token, IDictionary<string, string> dsCustomHeaders = null)
{
ThrowExceptionIf.Argument.IsNull(httpClient, nameof(httpClient));
var dataAsString = JsonConvert.SerializeObject(data);
var httpReqPostMsg = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(dataAsString, Encoding.UTF8, "application/json")
};
httpReqPostMsg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpReqPostMsg.Headers.Add(Constants.TelemetryCorrelationKey, Utilities.GetRequestCorrelationId());
if (dsCustomHeaders != null) {
foreach (var keyValue in dsCustomHeaders)
{
httpReqPostMsg.Headers.Add(keyValue.Key, keyValue.Value);
}
}
return httpClient.SendAsync(httpReqPostMsg);
}