發表日期 3/10/2022, 2:30:44 PM
作者 | Oleksii Nikiforov
譯者 | 平川
策劃 | 丁曉昀
如今,基於雲、微服務或物聯網的應用程序通常依賴於通過網絡與其他係統通信。每個服務都在自己的進程中運行,並解決一組有限的問題。服務之間的通信是基於一種輕量級的機製,通常是一個 HTTP 資源 API。
從.NET 開發人員的角度來看,我們希望以可分發包的形式提供一種一緻的、可管理的方式來集成特定的服務。最好的方法是將我們開發的服務集成代碼以 NuGet 包的形式提供,並與其他人、團隊、甚至組織分享。在這篇文章中,我將分享在.NET 6 中創建和使用 HTTP 客戶端 SDK 的方方麵麵。
客戶端 SDK 在遠程服務之上提供瞭一個有意義的抽象層。本質上,它允許進行遠程過程調用(RPC)。客戶端 SDK 的職責是序列化一些數據,將其發送到遠端目的地,以及反序列化接收到的數據,並處理響應。
HTTP 客戶端 SDK 與 API 一同使用:
加速 API 集成過程;
提供一緻、標準的方法;
讓服務所有者可以部分地控製消費 API 的方式。
1
編寫一個 HTTP 客戶端 SDK
在本文中,我們將編寫一個完備的 Dad Jokes API 客戶端,為的是提供老爸笑話;讓我們來玩一玩。源代碼在 GitHub 上。
在開發與 API 一起使用的客戶端 SDK 時,最好從接口契約(API 和 SDK 之間)入手:
契約是基於你要集成的 API 創建的。我一般建議遵循健壯性原則和最小驚奇原則開發通用的 API。但如果你想根據自己的需要修改和轉換數據契約,也是完全可以的,隻需從消費者的角度考慮即可。HttpClient是基於 HTTP 進行集成的基礎。它包含你處理 HTTP 抽象時所需要的一切東西。
通常,HTTP API 會使用 JSON,這就是為什麼從.NET 5 開始,BCL 增加瞭System.Net.Http.Json命名空間。它為HttpClient和HttpContent提供瞭許多擴展方法,讓我們可以使用System.Text.Json進行序列化和反序列化。如果沒有什麼復雜的特殊需求,我建議你使用System.Net.Http.Json,因為它能讓你免於編寫模闆代碼。那不僅很枯燥,而且也很難保證高效、沒有 Bug。我建議你讀下 Steves Gordon 的博文“使用 HttpClient 發送和接收 JSON”:
小提示:你可以創建一些集中式的地方來管理端點 URL,像下麵這樣:
小提示:如果你需要處理復雜的 URI,請使用 Flurl。它提供瞭流暢的 URL 構建(URL-building)體驗:
接下來,我們必須指定所需的頭文件(和其他所需的配置)。我們希望提供一種靈活的機製來配置作為 SDK 組成部分的 HttpClient。在這種情況下,我們需要在自定義頭中提供證書,並指定一個眾所周知的“Accept”。小提示:將高層的構建塊暴露為 HttpClientExtensions。這更便於發現特定於 API 的配置。例如,如果你有一個自定義的授權機製,則 SDK 應提供支持(至少要提供相關的文檔)。
客戶端生命周期
為瞭構建DadJokesApiClient,我們需要創建一個HttpClient。如你所知,HttpClient實現瞭IDisposable,因為它有一個非托管的底層資源――TCP 連接。在一台機器上同時打開的並發 TCP 連接數量是有限的。這種考慮也帶來瞭一個重要的問題――“我應該在每次需要時創建HttpClient,還是隻在應用程序啓動時創建一次?”
HttpClient是一個共享對象。這就意味著,在底層,它是可重入和綫程安全的。與其每次執行時新建一個HttpClient實例,不如共享一個HttpClient實例。然而,這種方法也有一係列的問題。例如,客戶端在應用程序的生命周期內會保持連接打開,它不會遵守 DNS TTL 設置,而且它將永遠無法收到 DNS 更新。所以這也不是一個完美的解決方案。
你需要管理一個不定時銷毀連接的 TCP 連接池,以獲取 DNS 更新。這正是HttpClientFactory所做的。官方文檔將HttpClientFactory描述為“一個專門用於創建可在應用程序中使用的HttpClient實例的工廠”。我們稍後將介紹如何使用它。
每次從IHttpClientFactory獲取一個HttpClient對象時,都會返迴一個新的實例。但是,每個HttpClient都使用一個被IHttpClientFactory池化並重用的HttpMessageHandler,減少瞭資源消耗。處理程序的池化是值得的,因為通常每個處理程序都要管理其底層的 HTTP 連接。有些處理程序還會無限期地保持連接開放,防止處理程序對 DNS 的變化做齣反應。HttpMessageHandler有一個有限的生命周期。
下麵,我們看下在使用由依賴注入(DI)管理的HttpClient時,HttpClientFactory是如何發揮作用的。
2
消費 API 客戶端
在我們的例子中,消費 API 的一個基本場景是無依賴注入容器的控製台應用程序。這裏的目標是讓消費者以最快的方式來訪問已有的 API。
創建一個靜態工廠方法來創建一個 API 客戶端。
這樣,我們可以從控製台應用程序使用IDadJokesApiClient:
消費 API 客戶端:HttpClientFactory
下一步是將HttpClient配置為依賴注入容器的一部分。關於這一點,網上有很多不錯的內容,我就不做詳細討論瞭。Steve Gordon 也有一篇非常好的文章“ASP.NET Core 中的 HttpClientFactory”。
為瞭使用 DI 添加一個池化的HttpClient實例,你需要使用來自Microsoft.Extensions.Http的IServiceCollection.AddHttpClient。
提供一個自定義的擴展方法用於在 DI 中添加類型化的HttpClient。
使用擴展方法的方式如下:
如你所見,IHttpClientFactory可以在 ASP.NET Core 之外使用。例如,控製台應用程序、worker、lambdas 等。讓我們看下它運行:
有趣的是,由 DI 創建的客戶端會自動記錄發齣的請求,使得開發和故障排除都變得非常容易。
如果你操作日誌模闆的格式並添加SourceContext和EventId,就會看到HttpClientFactory自己添加瞭額外的處理程序。當你試圖排查與 HTTP 請求處理有關的問題時,這很有用。
最常見的場景是 Web 應用程序。下麵是.NET 6 MinimalAPI 示例:
3
擴展 HTTP 客戶端 SDK,通過 DelegatingHandler 添加橫切關注點
HttpClient 還提供瞭一個擴展點:一個消息處理程序。它是一個接收 HTTP 請求並返迴 HTTP 響應的類。有許多問題都可以錶示為橫切關注點。例如,日誌、身份認證、緩存、頭信息轉發、審計等等。麵嚮方麵的編程旨在將橫切關注點封裝成方麵,以保持模塊化。通常情況下,一係列的消息處理程序被鏈接在一起。第一個處理程序接收一個 HTTP 請求,做一些處理,然後將請求交給下一個處理程序。有時候,響應創建後會迴到鏈條上遊。
任務 :假如你需要從 ASP.NET Core 的HttpContext復製一係列頭信息,並將它們傳遞給Dad Jokes API客戶端發齣的所有外發請求。
我們想把一個DelegatingHandler“插入”到HttpClient請求管道中。對於非IttpClientFactory場景,我們希望客戶端能夠指定一個DelegatingHandler列錶來為HttpClient建立一個底層鏈。
這樣,在沒有 DI 容器的情況下,可以像下麵這樣擴展DadJokesApiClient:
另一方麵,在 DI 容器場景中,我們希望提供一個輔助的擴展方法,使用IHttpClientBuilder.Add HttpMessageHandler輕鬆插入HeaderPropagationMessageHandler。
擴展後的 MinimalAPI 示例如下所示:
有時,像這樣的功能會被其他服務所重用。你可能想更進一步,把所有共享的代碼都提取到一個公共的 NuGet 包中,並在 HTTP 客戶端 SDK 中使用它。
第三方擴展
我們可以編寫自己的消息處理程序,但.NET OSS 社區也提供瞭許多有用的 NuGet 包。以下是我最喜歡的。
彈性模式――重試、緩存、迴退等:很多時候,在一個係統不可靠的世界裏,你需要通過加入一些彈性策略來確保高可用性。幸運的是,我們有一個內置的解決方案,可以在.NET 中構建和定義策略,那就是 Polly。Polly 提供瞭與IHttpClientFactory開箱即用的集成。它使用瞭一個便捷的方法 IHttpClientBuilder.AddTransientHttpErrorPolicy。它配置瞭一個策略來處理 HTTP 調用的典型錯誤:HttpRequestExceptionHTTP 5XX 狀態碼(服務器錯誤)、HTTP 408 狀態碼(請求超時)。
例如,可以使用重試和斷路器模式主動處理瞬時錯誤。通常,當下遊服務有望自我糾正時,我們會使用重試模式。重試之間的等待時間對於下遊服務而言是一個恢復穩定的窗口。重試經常使用指數退避算法。這紙麵上聽起來不錯,但在現實世界的場景中,重試模式的使用可能過度瞭。額外的重試可能導緻額外的負載或峰值。在最壞的情況下,調用者的資源可能會被耗盡或過分阻塞,等待永遠不會到來的迴復,導緻上遊發生瞭級聯故障。這就是斷路器模式發揮作用的時候瞭。它檢測故障等級,並在故障超過閾值時阻止對下遊服務的調用。如果沒有成功的機會,就可以使用這種模式,例如,當一個子係統完全離綫或不堪重負時。斷路器的理念非常簡單,雖然你可能會以它為基礎構建一些更復雜的東西。當故障超過閾值時,調用就會斷開,因此,我們不是處理請求,而是實踐快速失敗的方法,立即拋齣一個異常。
Polly 真的很強大,它提供瞭一種組閤彈性策略的方法,見 PolicyWrap。
下麵是一個可能對你有用的策略分類:
設計可靠的係統可能是一項非常具有挑戰性的任務,我建議你自己研究下這個問題。這裏有一個很好的介紹――.NET 微服務架構電子書:實現彈性應用程序。(https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/)
OAuth2/OIDC中的身份認證:如果你需要管理用戶和客戶端訪問令牌,我建議使用 IdentityModel.AspNetCore。它可以幫你獲取、緩存和輪換令牌,詳情參見文檔。(https://identitymodel.readthedocs.io/en/latest/aspnetcore/overview.html)
4
測試 HTTP 客戶端 SDK
至此,對於設計和編寫 HTTP 客戶端 SDK,你應該已經比較熟悉瞭。剩下的工作就隻是寫一些測試來確保其行為符閤預期瞭。請注意,跳過廣泛的單元測試,編寫更多的集成或 e2e 來確保集成的正確性,或許也不錯。現在,我將展示如何對DadJokesApiClient進行單元測試。
如前所述,HttpClient是可擴展的。此外,我們可以用測試版本代替標準的HttpMessageHandler。這樣,我們就可以使用模擬服務,而不是通過網絡發送實際的請求。這種技術提供瞭大量的可能,因為我們可以模擬各種在正常情況下是很難復現的HttpClient行為。
我們定義一個可重用的方法,用於創建一個 HttpClient 模擬,並作為一個依賴項傳遞給DadJokesApiClient。
從這點來看,單元測試是個非常簡單的過程:
使用HttpClient是最靈活的方法。你可以完全控製與 API 的集成。但是,也有一個缺點,你需要編寫大量的樣闆代碼。在某些情況下,你要集成的 API 並不重要,所以你並不需要HttpClient、HttpRequestMessage、HttpResponseMessage所提供的所有功能。優點:
可以完全控製行為和數據契約。你甚至可以編寫一個“智能”API 客戶端,如果有需要的話,在特殊情況下,你可以把一些邏輯移到 SDK 裏。例如,你可以拋齣自定義的異常,轉換請求和響應,提供默認頭信息,等等。
可以完全控製序列化和反序列化過程。
易於調試和排查問題。堆棧容易跟蹤,你可以隨時啓動調試器,看看後台正在發生的事情。
缺點:
需要編寫大量的重復代碼。
需要有人維護代碼庫,以防 API 有變化和 Bug。這是一個繁瑣的、容易齣錯的過程。
5
使用聲明式方法編寫 HTTP 客戶端 SDK
代碼越少,Bug 越少。Refit 是一個用於.NET 的、自動化的、類型安全的 REST 庫。它將 REST API 變成一個隨時可用的接口。Refit 默認使用System.Text.Json作為 JSON 序列化器。
每個方法都必須有一個 HTTP 屬性,提供請求方法和相對應的 URL。
Refit 根據Refit.HttpMethodAttribute提供的信息生成實現IDadJokesApiClient接口的類型。
消費 API 客戶端:Refit
該方法與平常的HttpClient集成方法相同,但我們不是手動構建一個客戶端,而是使用 Refit 提供的靜態方法。
對於 DI 容器場景,我們可以使用Refit.HttpClientFactoryExtensions.AddRefitClient擴展方法。
用法如下:
注意,由於生成的客戶端其契約應該與底層數據契約相匹配,所以我們不再控製契約的轉換,這項職責被托付給瞭消費者。讓我們看看上述代碼在實踐中是如何工作的。MinimalAPI 示例的輸齣有所不同,因為我加入瞭 Serilog 日誌。
同樣,這種方法也有其優缺點:優點:
便於使用和開發 API 客戶端。
高度可配置。可以非常靈活地把事情做好。
不需要額外的單元測試。
缺點:
故障排查睏難。有時候很難理解生成的代碼是如何工作的。例如,在配置上存在不匹配。
需要團隊其他成員瞭解如何閱讀和編寫使用 Refit 開發的代碼。
對於中 / 大型 API 來說,仍然有一些時間消耗。感興趣的讀者還可以瞭解下 RestEase。
6
使用自動化方法編寫 HTTP 客戶端 SDK
有一種方法可以完全自動地生成 HTTP 客戶端 SDK。OpenAPI/Swagger 規範使用 JSON 和 JSON Schema 來描述 RESTful Web API。NSwag 項目提供的工具可以從這些 OpenAPI 規範生成客戶端代碼。所有東西都可以通過 CLI(通過 NuGet 工具、構建目標或 NPM 分發)自動化。
Dad Jokes API 不提供 OpenAPI,所以我手動編寫瞭一個。幸運的是,這很容易:
現在,我們希望自動生成 HTTP 客戶端 SDK。讓我們藉助 NSwagStudio。生成的IDadJokesApiClient類似下麵這樣(簡潔起見,刪除瞭 XML 注釋):
同樣,我們希望把類型化客戶端的注冊作為一個擴展方法來提供。
用法如下:
讓我們運行它,並欣賞本文最後一個笑話:
優點:
基於眾所周知的規範。
有豐富的工具和活躍的社區支持。
完全自動化,新 SDK 可以作為 CI/CD 流程的一部分在每次 OpenAPI 規範有變化時生成。
可以生成多種語言的 SDK。
由於可以看到工具鏈生成的代碼,所以相對來說比較容易排除故障。
缺點:
如果不符閤 OpenAPI 規範就無法使用。
難以定製和控製生成的 API 客戶端的契約。感興趣的讀者還可以瞭解下 AutoRest、Visual Studio Connected Services。
7
選擇閤適的方法
在這篇文章中,我們學習瞭三種不同的構建 SDK 客戶端的方法。簡單來說,可以遵循以下規則選用正確的方法:
我是一個簡單的人。我希望完全控製我的 HTTP 客戶端集成。使用手動方法。
我是個大忙人,但我仍然希望有部分控製權。使用聲明式方法。
我是個懶人。最好能幫我做。使用自動化方法。
決策圖如下:
8
總結
在這篇文章中,我們迴顧瞭開發 HTTP 客戶端 SDK 的不同方式。請根據具體的用例和需求選擇正確的方法,希望這篇文章能讓你有一個大概的瞭解,使你在設計客戶端 SDK 時能做齣最好的設計決策。感謝閱讀。
作者簡介:
Oleksii Nikiforov 是 EPAM Systems 的高級軟件工程師和團隊負責人。他擁有應用數學學士學位和信息技術碩士學位,從事軟件開發已有 6 年多,熱衷於.NET、分布式係統和生産效率,是 N+1 博客的作者。
https://www.infoq.com/articles/creating-http-sdks-dotnet-6/