
AI聊天無敏感詞:技術原理與應用實踐
addSubscription(account_id, subscription_type) -> subscription_id
sendActivationReminderEmail(account_id) -> null
cancelSubscription(subscription_id, reason, immediate=True) -> null
getAccountDetails(account_id) -> {full data tree}
博主說,很多人發現為這個問題定義一個RPC API很容易,但用HTTP解決同樣的問題卻很掙扎,浪費了大量的時間和精力,而沒有為他們的項目帶來任何好處。我同意。一個原因是,在HTTP之上設計API是一項需要學習的技能,而且有很多選擇。
因為我們已經使用REST設計了許多API,所以對我們來說,用REST表達這個例子也同樣明顯。以下是我會怎么做的:
POST /accounts <headers> (username, contact_email, password)> -> account_URL
POST /subscriptions <headers> (account_URL, subscription_type) -> subscription_URL
POST /activation-reminder-outbox <headers> (account_URL) -> email_URL
POST /cancellations <headers> (subscription_URL, reason, immediate=True) -> cancellaton_URL
GET {account_URL} -> {full data tree}
客戶端提供給服務器的用戶名、聯系郵箱、密碼、account_URL以及其他數據都只是請求正文中的簡單JSON名稱/值對。我省略了請求頭的細節以及返回結果的方式,因為這些都在HTTP規范中有詳細解釋——實際上沒有選擇或決策要做。
客戶端和服務器之間在兩個方向上傳遞的所有標識符都是URL——API中沒有不是URL的標識符。每當一個資源包含對另一個資源的引用時,這個引用都是使用另一個資源的URL來表達的。這種技術被稱為超文本或超媒體——如果你的API不使用URL這種方式,那么它就不是在使用REST模型,因為超文本鏈接是REST區別于其他模型的一個標志性特征。RPC API也通過在一個實體中包含另一個實體的標識符來表達實體之間的關系,但這些標識符不是可以直接使用的URL,它們需要額外的信息。
REST所聲稱的優勢基本上就是萬維網本身的優勢,比如穩定性、一致性和普遍性。這些優勢在其他地方有所記錄,而且REST無論如何都是少數人的興趣,所以我們不會在這里過多地討論它們。一個例外是HTTP/REST模型固有的實體導向特性。這個特性特別值得關注,因為它已經被非REST模型的支持者廣泛討論和采用,比如gRPC和OpenAPI。 根據我的經驗,實體導向模型比簡單的RPC模型更簡單、更規則、更容易理解,并且隨著時間的推移更穩定。RPC API傾向于隨著一個接一個的程序添加而有機地增長,每個程序都實現了系統可以執行的一個動作。
實體導向模型為系統的行為提供了一個整體的組織結構。例如,我們大家都熟悉在線購物的實體模型,它有產品、購物車、訂單、賬戶等。如果這種能力僅用RPC程序來表達,將會導致一個長長的、無結構的程序列表,用于瀏覽產品目錄、將它們添加到購物車、結賬、跟蹤送貨和退貨。
這個列表很快就變得令人不知所措,并且在程序定義之間很難實現一致性。給列表帶來結構和秩序的一種方式是,使用一套標準的程序來模擬每種實體類型的所有行為。HTTP 本身是以實體為中心的,但你同樣可以將實體導向添加到 RPC 中,稍后會討論這一點。按實體類型對過程進行分組也是面向對象語言的關鍵思想之一。
在 OpenAPI 中,你定義了一些稱為路徑的東西。OpenAPI 路徑在 YAML 中看起來像這樣:
paths:
/pets/{petId}:
get:
operationId: getPetById
parameters:
- name: petId
in: path
required: true
description: 要檢索的寵物的 ID
schema:
type: string
定義了像這些路徑的 API 將 {petId} 的值暴露給客戶端,并要求客戶端使用適當的路徑定義,以便將 {petId} 值(和其他值)轉換成可用于 HTTP 請求的 URL。
以這種方式表達和使用 ID 是 REST 標志性的超文本鏈接的替代方案。
OpenAPI 將這些路徑中的變量稱為“參數”,路徑和 HTTP 方法的組合稱為“操作”——與 RPC 系統的術語相似。
**OpenAPI 使用帶有參數的 URL 模板可以被視為一種用自定義映射到 HTTP 的方式來表達類似 RPC 的概念。
在我看來,OpenAPI 有兩個基本特征可以解釋其成功。首先,OpenAPI 模型與傳統的 RPC 模型相似,大多數程序員對此都很熟悉和舒適。該模型還非常適合他們使用的編程語言的概念。第二個原因是它允許程序員定義這些RPC概念到HTTP請求的自定義映射。這第二個特性既帶來了好處也帶來了問題。主要好處是客戶端可以使用僅標準HTTP技術訪問API。這對于公共API尤其重要,因為這意味著API幾乎可以從所有編程語言和環境中訪問,而不需要客戶端采用任何額外的技術。一個缺點是它可能需要大量的努力來設計HTTP細節——看看網上所有關于你應該做什么和不應該做什么的指導,其中很多是相互矛盾的——以及消費者學習它的進一步努力。
你用OpenAPI描述的API的設計挑戰是定義URL路徑和HTTP方法的組合來表示你的“操作”和它們的“參數”。這可能是一項棘手的工作,因為有很多選項。對于大多數項目來說,這并不一定是時間和努力的好用途。由于這種方法導致的挫折感在Pascal Chambon的博客文章中充滿激情地描述了,我們之前提到過,這篇文章還提供了我們開頭提到的RPC示例。Chambon的文章包含了一些錯誤信息和誤解,而且對他的帖子的反應大多集中在糾正這些問題上,但Chambon的錯誤實際上為他的主要觀點提供了支持,即設計你自己的將RPC類概念映射到HTTP上是相當復雜和困難的。
對Chambon博客文章的回應中提出的大多數建議都提倡將REST作為Chambon和大多數人熟悉的RPC模型的替代品。這當然是一個選項——我們在文章開頭描述的簡單的REST示例是一個極簡主義者對如何做到這一點的看法。
Chambon的另一個選擇是保留他的基本RPC模型,但使用gRPC而不是OpenAPI來表達它。這避免了定義自定義API到HTTP的映射的復雜性。RPC模型的持久受歡迎程度超過了任何替代品,如果API設計者無論如何都要使用RPC類模型,那么他們應該權衡所有可用的技術來實現這一點。
gRPC用接口描述語言(IDL)表達一個RPC API,這種語言從DCE IDL、Corba IDL等RPC IDL的悠久傳統中受益。與OpenAPI使用URL路徑、它們的參數和與之一起使用的HTTP方法的方法相比,gRPC的IDL提供了一種更簡單、更直接的方式來定義遠程過程。gRPC 在底層使用 HTTP/2,但 gRPC 并未將任何 HTTP/2 暴露給 API 設計者或 API 用戶。gRPC 已經做出了所有關于如何在 HTTP 上層實現 RPC 模型的決策,因此您無需再做這些決策——這些決策已經內置在 gRPC 軟件和生成的代碼中。這使得 API 設計者和客戶端的生活更簡單。相比之下,OpenAPI 要求 API 設計者指定他們的特定 API 在 HTTP 上如何表達 RPC 模型的細節,API 的客戶端必須了解這些細節。OpenAPI 方法的一個重要優勢是它允許 API 客戶端使用標準的 HTTP 工具和技術,對于許多 API 設計者來說,這證明了付出的努力是值得的。
無論您的 API 如何使用 HTTP,您可能都希望為程序員使用各種語言創建客戶端編程庫。這些編程庫將采取程序的形式(可能被稱為函數或方法,這取決于編程語言)。gRPC 最吸引人的特性之一是它非常擅長生成直觀且高效的客戶端編程庫供程序員使用和執行。OpenAPI 也可以生成客戶端編程庫,但我發現 gRPC 版本更簡單、更明顯,可能是因為其 IDL 只需要表達 RPC 概念,而不需要同時描述這些概念到 HTTP 的映射。在gRPC中指定的API在服務器端也很容易實現。由于gRPC提供的框架、庫和代碼生成工具,創建gRPC方法的服務器實現可能比編寫一個解析傳入請求并調用正確實現函數的標準HTTP請求處理器更簡單,盡管有許多框架旨在幫助完成這項工作。
gRPC的另一個特點是性能良好。gRPC使用二進制負載,創建和解析都非常高效,并且利用HTTP/2高效管理連接。當然,你也可以不使用gRPC直接使用二進制負載和HTTP/2,但這需要你和你的客戶端掌握更多的技術。
gRPC還避免了即使是最好的基于HTTP的API也沒有實現整個HTTP協議的問題,這要求API提供者和客戶端弄清楚如何指定和學習特定API支持的HTTP的哪個子集。這對于REST和OpenAPI API都是一個問題。gRPC通過要求客戶端和服務器都采用實現完整gRPC協議的特殊軟件來避免這個問題。我們希望gRPC能夠成功地保持該協議至少25年的穩定,就像HTTP所做的那樣,以便在服務器升級和客戶端升級時客戶端不會中斷。
不管你是使用gRPC還是OpenAPI,以面向實體的方式使用RPC的關鍵在于將RPC方法定義限制為那些能夠輕松映射到每種資源類型的標準實體操作(創建、檢索、更新和刪除,通常稱為CRUD3,加上列表)。
要使用面向實體風格的RPC,你需要顛倒通常的RPC思考過程——不是從程序定義開始,而是首先定義你的資源類型,然后為這些類型上的常見實體操作以及任何你發現必要的額外操作制作RPC方法定義。
使用面向實體風格的RPC依賴于教導人們一種受限的使用模式。在實踐中,我們發現這樣設計的API有時是面向實體和面向過程概念的混合體,這削弱了一些好處。
每種技術都有其缺點和局限性。我們已經討論了一些OpenAPI的。
HTTP API的一個流行特性是客戶端可以使用它們,服務器也可以僅使用通用和廣泛可用的技術來實現它們。API調用可以簡單地通過在瀏覽器中輸入URL,或在終端窗口或bash腳本中發出cURL命令來輕松完成。程序員可以使用基本的HTTP庫訪問或實現HTTP API。相比之下,gRPC需要在客戶端和服務器上都使用特殊軟件。gRPC生成的代碼必須被整合到客戶端和服務器的構建過程中——這對一些人來說可能是繁瑣的,特別是那些習慣于在像JavaScript或Python這樣的動態語言中工作的人,因為在開發機器上,構建過程可能根本不存在。Google Cloud Endpoints產品使得可以通過HTTP和JSON訪問gRPC API,而無需特殊軟件,這恢復了許多客戶端的選項,但并非每個人都愿意或能夠使用Cloud Endpoints或找到或構建一個等效的產品。
編寫一個爬取整個REST API的機器人是很簡單的,就像瀏覽器或網絡爬蟲可以爬取整個HTML網絡一樣。你不能對RPC風格的API這樣做,無論它是使用gRPC還是OpenAPI描述的,因為RPC為每種實體類型提供了不同的API,需要定制軟件或元數據才能使用它。實際上,通常并不關鍵需要能夠編寫通用的API客戶端,盡管這可能是有用的。
HTTP API通常被代理以添加安全功能,執行輸入驗證,映射數據格式,并解決許多其他問題。這通常需要添加、移除或修改頭部,以及解析甚至修改正文。代理使用標準和自定義頭部的組合來實現這一點。這些功能通常使用像 Apigee Edge 這樣的產品來實現,這些產品不需要傳統的編程技能或可以輕松集成 gRPC 的那種軟件開發環境。我認為為 gRPC 進行這種代理操作會更加困難,并且我不清楚它是否被普遍實施。
使用面向實體的方法與 gRPC 一起使用,主要適用于新構建的項目——你不會發現它很容易地被改造到現有的 RPC API 中。
gRPC 沒有定義一個標準機制來防止兩個客戶端同時嘗試更新同一資源時數據丟失,因此如果你使用 gRPC,你可能需要自己發明一個。HTTP 為此目的定義了標準的 Etag 和 If-Match 頭部——我們設計的大多數 HTTP API 都使用這些頭部。
同樣,gRPC 也沒有定義進行部分更新的機制,因此你可能需要自己發明一個。HTTP 定義了一個方法——PATCH——用于部分更新,但沒有說明補丁應該是什么樣子或如何應用它。有兩個額外的 IETF 標準填補了 JSON 的這一空白:JSON 合并補丁和 JSON 補丁。前者使用起來更簡單,但并不處理所有情況,特別是數組的更新;后者處理更多情況,但使用起來更復雜。我最近構建的大多數HTTP API都實現了兩種標準,并讓客戶端選擇;Kubernetes API也是這樣工作的。
有一些API使用了與HTML Web相同的REST超文本模型。它們的目標是繼承HTML Web的核心特性,比如穩定性、一致性和普遍性。如果你已經知道如何以這種方式設計API,或者有動力去學習,那么這是一個不錯的選擇。這是我自己的偏好。
用OpenAPI描述的API基于與RPC類似的概念,但有一個自定義的映射到HTTP。這種方法允許客戶端僅使用常見的HTTP技術訪問生成的API,但它也為這些API增加了額外的設計選擇,這可能使它們更難設計和構建,也更難學習。
如果你正在考慮為API使用OpenAPI,你還應該考慮使用gRPC來設計和實現它的選項。兩者的基本API模型是可比的,而gRPC避免了發明自己的映射到HTTP的需要。
不管你是使用gRPC還是OpenAPI來構建你的API,如果你以面向實體的風格組織API,標準化你的程序名稱(例如,堅持使用動詞create、retrieve、update、delete和list),并施加其他命名約定,你可以獲得一些,但不是全部的REST API的好處。gRPC將帶來一些它自己的好處。使用gRPC特別有吸引力的情況,如果以下情況之一成立:
如果您選擇用gRPC替代OpenAPI或REST,您至少應該意識到在代理中增強或修復API行為的機會要有限得多,特別是那些使用Apigee Edge或其競爭對手實現的API管理工具。這取決于您打算如何以及在哪里使用gRPC,這可能是也可能不是一個問題。
和大多數設計挑戰一樣,有很多因素需要考慮,也有很多權衡要做。希望這次討論有助于解釋HTTP和RPC風格的API如何相互匹配。
特別感謝Nandan Sridhar和Marsh Gardiner對這篇文章的貢獻。