把HttpClient換成IHttpClientFactory之後,放心多了

Code綜藝圈 發表於 2021-06-18

前言

關於HttpClient的使用,個人在很多場景都派上用場了,比如在Winform或後臺服務中用其呼叫介面獲取和上傳資料、微服務中用其進行各服務之間的資料共享等,到目前來看,似乎還沒有出現過什麼問題,但當我看到官方文件介紹使用方式時,再回顧之前專案的程式碼,只能說沒出問題比較慶幸。

官方文件介紹的大概意思如下:

HttpClient類使用比較簡單,但在某些情況下,許多開發人員卻並未正確使用該類;雖然此類實現 IDisposable,但在 using 語句中宣告和例項化它並非首選操作,因為釋放 HttpClient 物件時,基礎套接字不會立即釋放,這可能會導致套接字耗盡問題,最終可能會導致 SocketException 錯誤。要解決此問題,推薦的方法是將 HttpClient 物件建立為單一物件或靜態物件

看到這,有一點點小不安(因為有些專案就是用using的方式),雖然目前的併發量還不至於導致SocketException異常的發生,但優化得馬不停蹄的安排上;先來探探HttpClient,再來說說IHttpClientFactory。

正文

1. HttpClient好像一直沒用對

這裡建立了一個控制檯程式進行測試(.NetCore3.1),程式碼比較簡單,如下:

image-20210612233306352

注:程式碼中訪問的地址是用Nginx搭建在阿里雲上搭建的站點,沒有做負載,所以看Socket狀態比較直觀。

上面程式碼執行完成之後就退出了,理論情況來說,上面程式執行完畢之後,就不應該佔用資源啦,但通過netstat檢視,的確還有Socket被佔用,測試如下:

  • Windows 測試

    執行如下命令看結果:

    netstat -ano | findstr TIME_WAIT
    

    image-20210612234018012

  • Linux測試

    我的雲伺服器之前就把.NetCore執行環境安裝好了,所以建立一個目錄,將編譯之後的檔案通過Xftp將檔案傳到雲伺服器,執行以下命令即可:

    # 注意,這裡指定啟動的是dll檔案
    dotnet HttpClientConsoleDemo.dll
    

    image-20210613000123727

    檢視埠情況,執行以下命令即可:

    netstat -ant | grep TIME_WAIT # 檢視埠占用情況,找到狀態為TIME_WAIT
    

    image-20210613000449843

    TIME_WAIT 是主動關閉 TCP 連線的那一方出現的狀態,系統會在 TIME_WAIT 狀態下等待 2MSL(maximum segment lifetime )後才能釋放連線(埠),目的是為了在TCP 四次揮手關閉連線機制中,保證 ACK 重發和丟棄延遲資料。按照解釋來看,這種做法也算是合情合理,保證資料傳輸嘛,但的確就是佔用資源啦;具體關於網路的相關知識,小夥伴們再去查閱一下。

經過Windows和Linux的測試,大概差不多兩分鐘的時間Socket才完全被釋放,可想,如果是在高併發情況下,每臺機器的能開的連線數是有限的,使用這種方式進行服務和服務之間互動資料,那肯定會出現問題。

而從理論上來講,只要HttpClient繼承了IDisposable介面,using塊執行完就可以釋放掉資源,而HttpClient確實是間接繼承了IDisposable(直接繼承HttpMessageInvoker ,而HttpMessageInvoker繼承了IDisposable介面)。但在這裡,使用HttpClient分配的Socket埠沒有得到及時釋放,為避免這個問題,官方推薦用靜態變數的方式使用HttpClient。

2. HttpClient換種方式好多啦

按照官方建議,將HttpClient變數定義為靜態變數,程式碼如下:

image-20210613224709180

執行程式,執行netstat命令可以看到,資源佔用情況明顯減少了。

如果使用靜態變數,看似解決了對應的問題,但若想針對不同的請求設定不同的頭資訊時就顯得不太方便,至於其他問題我暫時還沒遇見過(官方說這種方式不支援 DNS 變更)。先忽略其他問題,在.NetCore2.1開始出現了IHttpClientFactory ,據說是可以解決之前HttpClient面臨的一些問題,所以有需要用HttpClient的場景,直接用IHttpClientFactory 就妥啦,不信就來試試。

3. IHttpClientFactory用起來很給力

IHttpClientFactory是在.NETCore 2.1 開始提供的,預設實現為 DefaultHttpClientFactory ,專門用於建立在應用程式中用到的 HttpClient例項,自動維護內部的HttpMessageHandler池及其生命週期。主要功能如下:

  • 支援命名化、型別化配置,集中管理配置,避免衝突;
  • 出站請求管道配置靈活,輕鬆實現對請求生命週期的管理;
  • 合理管理內部HttpMessageHanlder的生命週期,避免資源佔用和DNS重新整理等問題
  • 內建日誌記錄器

接下來就來看看IHttpClientFactory到底有多給力;

3.1 控制檯演示

由於內部需要使用了一些服務,並且是採用DI的形式注入的,所以首先要把依賴注入的相關的包引入進來,程式碼如下圖:

image-20210614211552521

先來簡單看一下需要註冊的服務,後續會好好說:

image-20210614212416641

執行程式,然後通過以下命令檢視埠占用情況;

netstat -ano | findstr TIME_WAIT    # 根據狀態去找
netstat -ano | findstr 47.113.204.41 # 根據IP去找

最終沒有見到很明顯的資源佔用情況,有沒有給力一點;還有一些比較常用的方式,在WebApi專案中一一演示(畢竟微服務中服務間通訊還是比較常用的)。

3.2 WebApi專案演示
  • 建立一個API專案,註冊上相關服務即可

    image-20210614234351869

  • 增加一個測試控制器和測試介面

    image-20210614234115603

  • 執行看結果

    image-20210614233646831

    在API專案中是不是使用很簡單,因為專案本身就內建了依賴注入相關功能,直接註冊上服務就可以使用;但這還體現不出有多給力,接下來繼續看看其他擴充套件方式的使用。

3.3 使用命名和型別模式區分不同HttpClient

使用步驟與3.2是一樣的,只是在註冊服務的時候不太一樣而已。

  • 命名模式

    註冊服務時程式碼如下:

    image-20210615001230877

    增加一個測試介面,執行結果和上面一樣,只是HttpClient例項帶的頭資訊不一樣啦,如下:

    image-20210615001538406

  • 型別模式

    其實型別模式原理和命名模式是一樣的,只是通過指定的型別名稱作為對應HttpClient的名稱,減少了單獨定義名稱的步驟,所以顯得比較方便,個人比較喜歡這樣用。

    首先定義一個業務處理類,直接使用HttpClient,程式碼如下:

    image-20210616174807761

    在Startup中註冊服務時程式碼如下:

    image-20210616175313251

    增加一個測試介面,執行結果和上面一樣,呼叫過程如下:

    image-20210616175603258

    進入TypeHttpClientService對應的方法,看到在註冊時設定的頭資訊已經生效了,如下:

    image-20210616175932756

    執行完畢後,正常返回結果。型別模式不用單獨為HttpClient起名,而是直接用指定型別的類名作為預設名,這樣就相對方便啦;摘取獲取型別名的原始碼如下:

    image-20210617091835065

    小擴充套件:自定義的管道類沒有在Startup中手動註冊,為什麼能直接注入使用?

    答案:在services.AddHttpClient註冊時,內部已經將對應Type註冊好了,摘取程式碼如下:

    image-20210617091437591

    除了能輕鬆區分不同HttpClient之外,如果需要在請求過程中加入其它公共業務處理,可以通過增加自定義管道邏輯輕鬆實現。

3.4 在請求管道中增加自定義管道邏輯
  • 先定義一個管道類(CustomDelegatingHandler),繼承DelegatingHandler,然後重寫SendAsync方法

    image-20210616231200149

  • 在Startup中註冊對應的服務,根據需要在HttpClient上加上自定義管道

    image-20210616231538671

  • 執行看效果,只有用到型別模式的HttpClient才會加入自定義管道,因為在配置的時候進行了針對性的配置

    image-20210616231900098

    這種方式是不是感覺和.NetCore的中介軟體管道很類似,編寫自定義管道其實就類似於在編寫一箇中介軟體(至於原理,後續單獨扒扒原始碼),輕鬆實現在請求前和響應後做相關業務處理,就像上面加的RequestID,對分析分散式事務及微服務呼叫鏈跟蹤都有很大的幫助,所以小夥伴可以根據自己的需要封裝自己想要的HttpClient。

4. IHttpClientFactory 搭配Polly就完美了

只要牽涉到網路,要想服務百分百穩定那是相當難,比如常遇到的網路不通、網路波動、遠端服務掛了、遠端服務響應慢等問題,都可能會影響使用者對系統的體驗,別慌,還記得之前說過專門為故障、彈性應變設計的Polly庫嗎(詳情請看Polly-故障處理和彈性應對很有一手),如果HttpClientFactory能和Polly搭配使用豈不美哉;

其實是我們也能結合HttpClientFactory和Polly自己封裝, 不過微軟已經把輪子造好了,拿來用即可,如下步驟:

4.1 引入對應版本的包

引入Microsoft.Extensions.Http.Polly包,這裡使用的是.NetCore3.1,所以引入對應的版本為3.X都行。

4.2 Startup中註冊服務的時候指定策略

image-20210617001659629

4.3 新增測試介面

image-20210617002008925

4.4 執行看效果

image-20210617001439859

在瀏覽器訪問介面的時候,對比控制檯列印的訊息,會按照設定的時間間隔進行重試,效果很明顯的。

對於IHttpClientFactory整合Polly的使用就先說到這,關於其他策略的演示,和單獨使用Polly是一樣的,相信小夥伴參照上面案例演示一定能搞定。對於Polly的其他策略使用,可以參考《Polly-故障處理和彈性應對很有一手》這篇文章,說的挺詳細。

總結

IHttpClientFactory的應用暫時就先說到這,上面提到的功能只是平時自己常用的,其他功能小夥伴可以去探究探究;後續單獨和小夥伴們一起扒扒原始碼,整理整理一下流程。

如果現有專案需要用到HttpClient,建議通過IHttpClientFactory的方式,用起來方便、靈活,主要是是能避免一些問題。

博文原始碼github地址:https://github.com/zyq025/DotNetCoreStudyDemo

一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~

把HttpClient換成IHttpClientFactory之後,放心多了