ASP.NET MVC 中的 HttpClient 单例实现

Posted

技术标签:

【中文标题】ASP.NET MVC 中的 HttpClient 单例实现【英文标题】:HttpClient Singleton Implementation in ASP.NET MVC 【发布时间】:2016-10-24 21:54:31 【问题描述】:

阅读blog post 和www.asp.net 上的官方说明后:

HttpClient 旨在被实例化一次并在整个过程中重复使用 应用程序的生命周期。特别是在服务器应用程序中, 为每个请求创建一个新的 HttpClient 实例将耗尽 重负载下可用的插座数量。这将导致 SocketException 错误。

我发现我们的代码在每次调用时都会处理 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 属性。

【问题讨论】:

仅供参考:HttpClient is threadsafe 【参考方案1】:

你真的想要一个实例吗?

我认为您不希望在应用程序范围内使用一个实例。您希望每个线程一个实例。否则你不会得到很好的表现!此外,这将解决您的问题 #3 和 #4,因为不会有两个线程同时访问同一个 HttpClient。

你不需要单身

只需使用Container.Resolve 和PerThreadLifetimeManager。

【讨论】:

这是一个很好的问题。我找到的文章给出了使用 ConsoleApplication 的示例,它们设置为应用程序范围的静态。我不确定这是否适用于 Web 应用程序。 ***.com/a/14592419/812013 此处突出显示的问题怎么样?【参考方案2】:

对于那些有幸使用.NET Core 的人来说,这相当简单。

正如John Wu 雄辩地陈述的那样,您不想要一个单例本身,而是每个请求一个单例。因此,AddScoped&lt;TService&gt;() 方法就是您所追求的。

在您的ConfigureServices(IServiceCollection services) 方法中:

services.AddScoped<HttpClient>();

消费:

public class HomeController 

    readonly HttpClient client;

    public HomeController (HttpClient client)   
    
        this.client = client;
    

    //rest of controller code

【讨论】:

【参考方案3】:

既然调用的是 SendAsync(),我们是否保证响应被映射回正确的调用者?我想确认回复不会发给其他来电者。

这将通过回调指针来处理。它与使用 HttpClient 作为单例无关。更多细节在这里 - https://***.com/a/42307650/895724

【讨论】:

【参考方案4】:

这是我用的

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

【讨论】:

以上是关于ASP.NET MVC 中的 HttpClient 单例实现的主要内容,如果未能解决你的问题,请参考以下文章

如何在使用 HttpClient 使用 Asp Net Web Api 的 Asp Net Mvc 中提供实时数据?

如何使用 .Net 4.0 中包含的 HttpClient 类将文件上传到在 IIS Express 中运行的 Asp.Net MVC 4.0 操作

ASP.NET MVC 4 异步子动作

无法使用 HttpClient 对 ASP.NET Web Api 服务进行身份验证

在 asp.net MVC 中设置整数标头值

asp.net mvc 文件跨域上传,接收返回结果