HttpClientFactory 使用說明 及 對 HttpClient 的回顧和對比

Coder.X發表於2019-08-05

HttpClient 日常使用及坑點:

在 C# 中,平時我們在使用 HttpClient 的時候,會將 HttpClient 包裹在 using 內部進行宣告和初始化,如:

using(var httpClient = new HttpClient())
{
    //other codes
}

至於為什麼?無外乎是:專案程式碼中就是這樣寫的,依葫蘆畫瓢/別人就是這樣用的/在微軟官方的 ASP.NET 教程中也是這麼幹的。

說的技術範點:當你使用繼承了 IDisposable 介面的物件時,建議在 using 程式碼塊中宣告和初始化,當 using 程式碼段執行完成後,會自動釋放該物件而不需要手動進行顯示 Dispose 操作。

但這裡,HttpClient 這個物件有點特殊,雖然繼承了 IDisposable 介面,但它是可以被共享的(或者說可以被複用),且執行緒安全。從專案經驗來看,倒是建議在整個應用的生命週期內,複用 HttpClient 例項,而不是每次 RPC 請求的時候就例項化一個。(之前在優化公司一個 web 專案的時候,也曾經因為 HttpClient 載過一次坑,後面我會進行簡述。)

我們先來用個簡單的例子做下測試,看為什麼不要每次 RPC 請求都例項化一個 HttpClient:

  public class Program
    {
        static void Main(string[] args)
        {
            HttpAsync();
            Console.WriteLine("Hello World!");
            Console.Read();
        }

        public static async void HttpAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                using (var client = new HttpClient())
                {
                    var result = await client.GetAsync("http://www.baidu.com");
                    Console.WriteLine($"{i}:{result.StatusCode}");
                }
            }
        }
    }

執行專案輸出結果後,通過 netstate 檢視下 TCP 連線情況:

e92bde19-eae0-41a4-83ed-08c2038fdd0f.png

  • 雖然專案已經執行結束,但是連線依然存在,狀態為 "TIME_WAIT"(繼續等待看是否還有延遲的包會傳輸過來。)。

    預設在 windows 下,TIME_WAIT 狀態將會使系統將會保持該連線 240s。

  • 這裡也就引出了我上面說的載過的一次坑:在高併發的情況下,連線來不及釋放,socket 被耗盡,耗盡之後就會出現喜聞樂見的一個錯誤:

#使用jemter壓測復現錯誤資訊:
Unable to connect to the remote serverSystem.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.

說白話:就是會出現“各種套接字問題”。(碼WCF的童鞋可能更加記憶尤新,問題追根溯源都是換湯不換藥。)

熊廠裡面能夠搜尋出來的解決方法,基本都是“減少超時時間”,但人為減少了超時時間會出現各種莫名其妙的錯誤。且無法避免伺服器遲早崩潰的問題。

可以通過登錄檔進行修改預設值:[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])

那麼如何處理這個問題?答案已經在上面說了,“複用 HttpClient”即可。如:

    public class Program
    {
        private static readonly HttpClient _client = new HttpClient();
        static void Main(string[] args)
        {
            HttpAsync();
            Console.WriteLine("Hello World!");
            Console.Read();
        }

        public static async void HttpAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                var result = await _client.GetAsync("http://www.baidu.com");
                Console.WriteLine($"{i}:{result.StatusCode}");
            }
        }
    }

232f7b9e-b9f3-4444-9697-e0108c3b09fe.png

  • 可以看到,原先 10 個連線變成了 1 個連線。(請不要在意兩次示例的目標 IP 不同---SLB 導致的,都是百度的ip)

  • 另外,因為複用了 HttpClient,每次 RPC 請求的時候,實際上還節約了建立通道的時間,在效能壓測的時候也是很明顯的提升。曾經因為這一舉動,將 web 專案的 TPS 從單臺 600 瞬間提升到了 2000+,頁面請求時間也從 1-3s 減少至 100-300ms,甚是讓測試組小夥伴膜拜(當然也包括了一些業務程式碼的細調。),但知道箇中緣由後,一個小改動帶來的專案效能提升。。。會讓人上癮:)

  • 至於如何建立一個靜態 HttpClient 進行復用,大家可以按專案實際來,如直接建立一個“全域性”靜態物件,或者通過各類 DI 框架來建立均可。

但這麼調整 HttpClient 的引用後,依然存在一些問題可能會影響到你的專案(尚未影響到我:P),如:

  • 因為是複用的 HttpClient,那麼一些公共的設定就沒辦法靈活的調整了,如請求頭的自定義。
  • 因為 HttpClient 請求每個 url 時,會快取該url對應的主機 ip,從而會導致 DNS 更新失效(TTL 失效了)

那麼有沒有辦法解決 HttpClient 的這些個問題?直到我遇到了 HttpClientFactory,瞬間寫程式碼幸福感倍升,也感慨新時代的童鞋們真的太幸福了,老一輩踩的坑可以“完美”規避掉了。

HttpClientFactory 優勢:

HttpClientFactory 是 ASP.NET CORE 2.1 中新增加的功能。

  • “完美”解決了我多年來遇到的這些坑,可以更加專注於業務程式碼。
  • HttpClientFacotry 很高效,可以最大程度上節省系統 socket。(“JUST USE IT AND FXXK SHUT UP”:P)
  • Factory,顧名思義 HttpClientFactory 就是 HttpClient 的工廠,內部已經幫我們處理好了對 HttpClient 的管理,不需要我們人工進行物件釋放,同時,支援自定義請求頭,支援 DNS 更新等等等。

    從微軟原始碼分析,HttpClient 繼承自 HttpMessageInvoker,而 HttpMessageInvoker 實質就是HttpClientHandler。

    HttpClientFactory 建立的 HttpClient,也即是 HttpClientHandler,只是這些個 HttpClient 被放到了“池子”中,工廠每次在 create 的時候會自動判斷是新建還是複用。(預設生命週期為 2min)

    還理解不了的話,可以參考 Task 和 Thread 的關係,以前碰到 HttpClient 這個問題的時候,就一直在想微軟什麼時候官方出一個 HttpClient 的 Factory,雖然時隔了這麼多年直到 .NET CORE 2.1 才出,但也很是興奮。

HttpClientFactory 使用方法:

藉助 ASP.NET CORE MVC,可以很方便的進行 HttpClient 的使用

實戰用法1:常規用法

統一在釋出專案中宣告和配置。

1. 在 Startup.cs 中進行註冊

  public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //other codes
            
            services.AddHttpClient("client_1",config=>  //這裡指定的name=client_1,可以方便我們後期服用該例項
            {
                config.BaseAddress= new Uri("http://client_1.com");
                config.DefaultRequestHeaders.Add("header_1","header_1");
            });
            services.AddHttpClient("client_2",config=>
            {
                config.BaseAddress= new Uri("http://client_2.com");
                config.DefaultRequestHeaders.Add("header_2","header_2");
            });
            services.AddHttpClient();

            //other codes
            services.AddMvc().AddFluentValidation();
        }
      }

2. 使用,這裡直接以 controller 為例,其他地方自行 DI

    public class TestController : ControllerBase
    {
        private readonly IHttpClientFactory _httpClient;
        public TestController(IHttpClientFactory httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<ActionResult> Test()
        {
            var client = _httpClient.CreateClient("client_1"); //複用在Startup中定義的client_1的httpclient
            var result = await client.GetStringAsync("/page1.html");

            var client2 = _httpClient.CreateClient(); //新建一個HttpClient
            var result2 = await client.GetStringAsync("http://www.site.com/XXX.html");

            return null;
        }
    }

實戰用法2:使用自定義類執行 HttpClientFactory 請求

1. 自定義 HttpClientFactory 請求類

public class SampleClient
{
    public HttpClient Client { get; private set; }
    
    public SampleClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        Client = httpClient;
    }
}

2. 在 Startup.cs 中 ConfigureService 方法中註冊 SampleClient,程式碼如下,

services.AddHttpClient<SampleClient>();

3. 呼叫:

public class ValuesController : Controller
{
    private readonly SampleClient  _sampleClient;;
  
    public ValuesController(SampleClient  sampleClient)
    {
        _sampleClient = sampleClient;
    }
  
    [HttpGet]
    public async Task<ActionResult> Get()
    {
        string result = await  _sampleClient.client.GetStringAsync("/");
        return Ok(result);
    }
}

實戰用法3:完全封裝 HttpClient 可以使用下面方法

1. 自定義 HttpClientFactory 請求類

public interface ISampleClient
{
    Task<string> GetData();
}
 
public class SampleClient : ISampleClient
{
    private readonly HttpClient _client;
 
    public SampleClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        _client = httpClient;
    }
 
    public async Task<string> GetData()
    {
        return await _client.GetStringAsync("/");
    }
}

2. 在 Startup.cs 中 ConfigureService 方法中註冊 SampleClient,程式碼如下,

services.AddHttpClient<ISampleClient, SampleClient>();

3. 呼叫:

public class ValuesController : Controller
{
    private readonly ISampleClient  _sampleClient;;
     
    public ValuesController(ISampleClient  sampleClient)
    {
        _sampleClient = sampleClient;
    }
     
    [HttpGet]
    public async Task<ActionResult> Get()
    {
        string result = await _sampleClient.GetData();
        return Ok(result);
    }
}

相關文章