設計代理服務的最佳做法
請遵循針對 StreamJsonRpc RPC 介面所記載的一般 指引和限制。
此外,下列指導方針適用於代理服務。
方法簽名
所有方法都應該採用 CancellationToken 參數作為其最後一個參數。 此參數通常應該 不 為選擇性參數,因此呼叫者不太可能不小心省略自變數。 即使預計該方法的實作是簡單的,提供 CancellationToken 仍允許客戶端在請求發送到伺服器之前取消自己的請求。 它也允許伺服器的實作演變成更昂貴的專案,而不需要更新 方法,以便稍後新增取消作為選項。
請考慮 避免在 RPC 介面上 以相同方法的多次重載。 雖然多載解析通常能夠運作(並且應該撰寫測試來驗證其確實有效),但這過程依賴於 嘗試 根據每個多載的參數類型來反序列化參數,導致初始機會例外作為選擇多載的一個常見部分。 由於我們想要將成功路徑中擲回的第一個機會例外狀況數目降到最低,因此最好只有一個具有指定名稱的方法。
參數和傳回型別
請記住,透過 RPC 交換的所有自變數和傳回值都只是 資料。 它們全都是透過電線串行化和傳送的。 您在這些數據類型上定義的任何方法只會在數據的本機複本上運作,而且無法與產生數據的 RPC 服務進行通訊。 此串行化行為的唯一例外是 異國類型,StreamJsonRpc 對其有特殊支援。
請考慮使用 ValueTask<T>
而非 Task<T>
作為方法的傳回類型,因為 ValueTask<T>
需要較少的配置。
使用非泛型品種時(例如,Task 和 ValueTask),它比較不重要,但 ValueTask 可能仍然比較好。
請留意 ValueTask<T>
的使用限制,如該 API 所述。 此 部落格文章 和 影片,也有助於選擇要用哪種類型。
自訂數據類型
請考慮將所有數據型別都定義為不可變,這可讓您在不複製的情況下更安全地跨進程共享數據,並協助強化取用者的想法,讓他們無法在不放置另一個 RPC 的情況下變更他們收到的數據來回應查詢。
使用 ServiceJsonRpcDescriptor.Formatters.UTF8時,請將數據類型定義為 class
,而不是 struct
,這可避免使用 Newtonsoft.Json 時(可能重複)Boxing 的成本。
Boxing 不會 使用 ServiceJsonRpcDescriptor.Formatters.MessagePack 時發生,因此如果您認可到該格式器,結構可能是適合的選項。
請考慮在數據類型上實作 IEquatable<T> 和覆寫 GetHashCode() 和 Equals(Object) 方法,這可讓客戶端根據它是否等於另一次收到的數據,有效率地儲存、比較及重複使用收到的數據。
使用 DiscriminatedTypeJsonConverter<TBase> 支援使用 JSON 序列化多態類型。
收集
在 RPC 方法簽章中使用唯讀集合介面(例如,IReadOnlyList<T>),而不是具體型別(例如,List<T> 或 T[]
),這可讓您更有效率地還原串行化。
避免 IEnumerable<T>。
其缺少 Count
屬性會導致程式碼效率不佳,並暗示數據可能延遲生成,這不適用於 RPC 情境。
改用 IReadOnlyCollection<T> 表示無序集合或 IReadOnlyList<T> 表示有序集合。
請考慮 IAsyncEnumerable<T>。 任何其他類型的集合或 IEnumerable<T> 將會導致整個集合在一則訊息中被傳送。 使用 IAsyncEnumerable<T> 允許建立小型的初始訊息,並使接收者能夠從集合中異步地列舉並取得所需數量的項目。 深入瞭解這個新穎的模式。
觀察者模式
請考慮在介面中使用 觀察者設計模式。 這是一種讓用戶端訂閱數據的簡單方式,避免了傳統事件模型中存在的許多陷阱,這些陷阱會在下一節中描述。
觀察者模式可能如下所示:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
上述使用的 IDisposable 和 IObserver<T> 類型是 StreamJsonRpc 中 特殊 類型的兩種,因此它們會收到特別封送處理的行為,而不是僅序列化為數據。
事件
基於數個原因,事件在使用 RPC 時可能會出現問題,因此建議您改用上述觀察者模式。
請記住,服務無法查看當服務與客戶端位於個別進程中時,用戶端所附加的事件處理程式數目。 JsonRpc 通常只會連接一個負責將事件傳播給客戶端的處理程式。 客戶端可能會在遠端連接零個或多個的處理程式。
大部分的 RPC 用戶端在剛開始連線時,通常不會設定事件處理程式。 務必避免引發第一個事件,直到用戶端在介面上呼叫「Subscribe*」方法,以表明對接收事件的興趣及準備就緒。
如果您的事件指出狀態變更(例如,新增專案至集合),請考慮在用戶端訂閱時,引發所有過去的事件,或在事件參數中描述所有目前的數據,使其看起來像是新的,從而協助它們僅靠事件處理程式碼來「同步」。
如果用戶端可能想要對數據或通知的子集表示興趣,請考慮在上述的 “Subscribe*” 方法上接受額外的自變數,以減少轉送這些通知所需的網路流量和 CPU。
如果您也公開事件以接收變更通知,或主動勸阻用戶端與事件搭配使用,請考慮不提供傳回目前值的方法。 一個客戶端訂閱一個用於獲取數據的事件,並呼叫一個方法來取得當前值,這使得用戶端可能會與該數值的變化發生競賽,可能會錯過變更事件,或不知道如何協調一個線程上的變更事件與在另一個線程上獲得的值。 對於任何介面而言,這項考慮是一般問題,而不只是當它透過 RPC 時。
命名慣例
- 在 RPC 介面上使用
Service
後綴和I
簡單的前綴。 - 請勿針對 SDK 中的類別使用
Service
後綴。 您的連結庫或 RPC 包裝函式應該使用名稱來確切描述其用途,避免「服務」一詞。 - 請避免介面或成員名稱中的「遠端」一詞。 請記住,中介服務在本地場景中的應用理想上應當與遠程場景一樣頻繁。
版本相容性問題
我們希望任何對其他擴充套件公開或通過 Live Share 公開的代理服務都能前向和後向相容,意味著我們應假設客戶端可能比服務更舊或更新,並且功能應大致等同於兩個適用版本中較小的那個。
首先,讓我們來檢視破壞性變更術語:
二進位相容性中斷變更:API 變更會導致針對舊版組件編譯的其他受管理的代碼無法在執行時綁定至新的版本。 範例包括:
- 變更現有公共成員的簽章。
- 重新命名公用成員。
- 移除公共類型。
- 將抽象成員加入至類型,或任何成員加入至介面。
但下列 不會 二進位重大變更:
- 將非抽象成員加入至類別或結構。
- 將完整的(非抽象)介面實作新增至現有的類型。
通訊協定中斷變更:變更某些數據類型或 RPC 方法呼叫的序列化格式,使遠端協作方無法正確解碼並處理它。 範例包括:
- 將必要的參數新增至 RPC 方法。
- 從先前保證不為空值的資料型別中移除一個成員。
- 增加一個要求:方法呼叫必須先於其他既有作業。
- 在控制該成員中數據串行化名稱的欄位或屬性上加入、移除或變更屬性。
- (MessagePack):變更現有成員的 DataMemberAttribute.Order 屬性或
KeyAttribute
整數。
但下列 不會 通訊協定中斷性變更:
- 將選擇性成員加入至數據類型。
- 將成員新增至 RPC 介面。
- 將選擇性參數新增至現有的方法。
- 將代表整數或浮點數的參數類型變更為長度或精確度較大的參數類型(例如,
int
為long
或float
double
)。 - 重新命名參數。 在技術上,這會對於使用 JSON-RPC 具名參數的客戶端造成影響,但是使用 ServiceJsonRpcDescriptor 的客戶端預設使用位置參數,因而不會受到參數名稱變更的影響。 這與用戶端 原始程式碼 是否使用具名自變數語法無關,參數重新命名將是 中斷來源 變更。
行為中斷性變更:變更代理服務的實作,以新增或變更行為,讓較舊的用戶端可能會故障。 範例包括:
- 不再初始化先前一律初始化之數據類型的成員。
- 在之前可能成功完成的條件下拋出例外。
- 傳回錯誤碼與先前傳回的錯誤碼不同。
但以下 並非 中斷行為的變更:
- 拋出新的例外狀況類型(因為所有例外狀況都包裝在 RemoteInvocationException 中)。
需要重大變更時,可以透過註冊並提供新的服務標識符,安全地進行變更。 此暱稱可以使用相同的名稱,但版本號碼較高。 如果沒有二進位中斷變更,原始 RPC 介面 可能會 可重複使用。 否則,請為新的服務版本定義新的介面。 為避免中斷舊用戶端,請繼續註冊、提供及支援舊版。
除了將成員新增至 RPC 介面之外,我們想要避免所有這類破壞性變更。
將成員新增至 RPC 介面
請勿 不要 將成員新增至 RPC 用戶端回呼介面 ,因為許多用戶端可能會實作該介面,而且新增成員會導致載入這些類型但未實作新介面成員時擲回 CLR 擲回 TypeLoadException。 如果您必須新增成員以叫用 RPC 用戶端回呼目標,請定義新的介面(可能衍生自原始介面),然後遵循以遞增版本號碼來提供代理服務的標準程式,並提供具有指定更新用戶端介面類型的描述元。
您 可能會 將成員新增至定義代理服務的 RPC 介面。 這不是協定中斷性的變更,而僅對於實作該服務的人來說是一項二進位中斷變更,但大概您也會更新該服務來實作新成員。 由於 我們的指引 是,除了代理服務本身之外,任何人都不應該實作 RPC 介面(而測試應該使用模擬架構),因此將成員新增至 RPC 介面不應中斷任何人。
這些新成員應該有 xml 檔批注,可識別哪一個服務版本會先新增該成員。 如果較新的用戶端在未實作 方法的較舊服務上呼叫 方法,該用戶端就可以攔截 RemoteMethodNotFoundException。 但該用戶端可以(且可能應該)預測失敗,並避免第一次呼叫。 將成員新增至現有服務的最佳做法包括:
- 如果這是您服務版本中的第一次變更:當您新增成員並宣告新的描述元時,請在服務代號上提升小版本。
- 除了 舊版本之外,請更新您的服務以註冊及提供新版本。
- 如果您有透過經紀服務的客戶端,請更新客戶端以請求較新版本,如果較新版本返回空值,則請求較舊的版本。