RPC的出現(xiàn),就是為了解決這一問題。
RPC: 即我們常說的遠程過程調(diào)用,就是像調(diào)用本地方法一樣調(diào)用遠程方法,通信協(xié)議大多采用二進制方式。
常用的RPC框架有(標粗的是準備講解的):
-
gRPC
gRPC是一個現(xiàn)代的開源高性能遠程過程調(diào)用(RPC)框架,可以在任何環(huán)境中運行。它可以有效地連接數(shù)據(jù)中心內(nèi)和跨數(shù)據(jù)中心的服務(wù),支持負載均衡、跟蹤、健康檢查和身份驗證。它也適用于分布式計算,將設(shè)備、移動應(yīng)用程序和瀏覽器連接到后端服務(wù)---這是官方給的說明
-
openFeign
簡化HttpClient調(diào)用過程,讓net core開發(fā)變得更簡單。
-
Thrift
Thrift是一種接口描述語言和二進制通訊協(xié)議,它被用來定義和創(chuàng)建跨語言的服務(wù)。它被當作一個遠程過程調(diào)用(RPC)框架來使用,是由Facebook為“大規(guī)??缯Z言服務(wù)開發(fā)”而開發(fā)的。它通過一個代碼生成引擎聯(lián)合了一個軟件棧,來創(chuàng)建不同程度的、無縫的跨平臺高效服務(wù),可以使用C#、C++、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk。
-
Shuttler.net
Shuttler.Net是一個高性能分布式框架,如果你在使用老去的remoting,webservices分布式架構(gòu),或在使用新生的wcf,那么你也可以嘗試下Shuttler.Net。 如果你想開發(fā)自己的IM服務(wù)端和客戶端(或游戲大廳),你也可以使用Shuttler.Net,只需你制定報文協(xié)議即可,其他傳輸層Shuttler幫你搞定。Shuttler.Net核心組件Artery正如她的名字一樣:脈,基于Tcp的應(yīng)用棧,可以幫你傳輸任何能量,使你想唱就唱。
主要功能點包括:
-
分布式RPC,目前支持Tcp和Http(類REST風格)雙通道(見Demo:TcpRpcTest和HttpRpcTest): 可以多個RpcServer端和多個RpcClient端,其中client通過HashingAlgorithm根據(jù)Key計算出server。
-
分布式緩存系統(tǒng)(Memcached),包括MemcachedServer和MemcachedClient(見Demo:MemcachedTest): 可以多個MemcachedServer端和多個MemcachedClient端,其中client通過HashingAlgorithm根據(jù)Key計算出server。
-
IM協(xié)議棧,使用Shuttler.Net的Artery組件可以輕松實現(xiàn)一個IMServer端和IMClient端(見Demo:IMTest): IMTest中實現(xiàn)IM的登錄密碼校驗,通訊協(xié)議自己定義即可,協(xié)議Demo見Shuttler_Artery_Protocol。
-
?文章來源地址http://www.zghlxwxcb.cn/news/detail-760243.html
1. gRPC 詳細教程
官網(wǎng):https://grpc.io
gRPC 的主要優(yōu)點是:
-
現(xiàn)代高性能輕量級 RPC 框架。
-
協(xié)定優(yōu)先 API 開發(fā),默認使用協(xié)議緩沖區(qū),允許與語言無關(guān)的實現(xiàn)。
-
可用于多種語言的工具,以生成強類型服務(wù)器和客戶端。
-
支持客戶端、服務(wù)器和雙向流式處理調(diào)用。
-
使用 Protobuf 二進制序列化減少對網(wǎng)絡(luò)的使用。
這些優(yōu)點使 gRPC 適用于:
-
效率至關(guān)重要的輕量級微服務(wù)。
-
需要多種語言用于開發(fā)的 Polyglot 系統(tǒng)。
-
需要處理流式處理請求或響應(yīng)的點對點實時服務(wù)。
警告
若要將 ASP.NET Core gRPC用于 Azure 應(yīng)用服務(wù)或 IIS,則該 gRPC 具有額外的要求。 有關(guān)可以在何處使用 gRPC 的詳細信息,請參閱 .NET 支持的平臺上的 gRPC
?文章來源:http://www.zghlxwxcb.cn/news/detail-760243.html
1. 快速入門
本教程演示了如何創(chuàng)建 .NET Core gRPC客戶端和 ASP.NET Core gRPC 服務(wù)器。 最后會生成與 gRPC Greeter 服務(wù)進行通信的 gRPC 客戶端。
在本教程中,你將了解:
-
創(chuàng)建 gRPC 服務(wù)器。
-
創(chuàng)建 gRPC 客戶端。
-
使用 gRPC Greeter 服務(wù)測試 gRPC 客戶端。
?
創(chuàng)建gRPC服務(wù)
-
啟動 Visual Studio 2022 并選擇“創(chuàng)建新項目”。
-
在“創(chuàng)建新項目”對話框中,搜索
gRPC
。 選擇“ASP.NET Core gRPC 服務(wù)”,并選擇“下一步” 。 -
在“配置新項目”對話框中,為“項目名稱”輸入
GrpcGreeter
。 將項目命名為“GrpcGreeter”非常重要,這樣在復制和粘貼代碼時命名空間就會匹配。 -
選擇“下一頁”。
-
在“其他信息”對話框中,選擇“.NET 6.0 (長期支持)”,然后選擇“創(chuàng)建。
?
項目文件
GrpcGreeter 項目文件:
-
Protos/greet.proto
:在
.proto
文件中定義服務(wù)和消息 -
Services
文件夾:包含Greeter
服務(wù)的實現(xiàn)。 -
appSettings.json
:包含配置數(shù)據(jù),如 Kestrel 使用的協(xié)議。 -
Program.cs,其中包含:
-
gRPC 服務(wù)的入口點。
-
配置應(yīng)用行為的代碼。
-
?
創(chuàng)建gRPC 控制臺客戶端
-
打開 Visual Studio 的第二個實例并選擇“創(chuàng)建新項目”。
-
在“創(chuàng)建新項目”對話框中,選擇“控制臺應(yīng)用程序”,然后選擇“下一步” 。
-
在“項目名稱”文本框中,輸入“GrpcGreeterClient”,然后選擇“下一步” 。
-
在“其他信息”對話框中,選擇“.NET 6.0 (長期支持)”,然后選擇“創(chuàng)建”。
需要安裝的包
Grpc.Net.Client 2.50.0 Google.Protobuf 3.22.0 Grpc.Tools 2.50.0
?
添加 proto文件
-
在 gRPC 客戶端項目中創(chuàng)建 Protos 文件夾。
-
從 gRPC Greeter 服務(wù)將 Protos\greet.proto 文件復制到 gRPC 客戶端項目中的 Protos 文件夾 。
-
將
greet.proto
文件中的命名空間更新為項目的命名空間:option csharp_namespace = "GrpcGreeterClient";
?
-
編輯
GrpcGreeterClient.csproj
項目文件: -
添加具有引用 greet.proto 文件的
<Protobuf>
元素的項組:<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup>
?
?
創(chuàng)建 Greeter 客戶端
-
構(gòu)建客戶端項目,用于在
GrpcGreeterClient
命名空間中創(chuàng)建類型。
GrpcGreeterClient
類型是由生成進程自動生成的。 工具包
GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs
:用于填充、序列化和檢索請求和響應(yīng)消息類型的協(xié)議緩沖區(qū)代碼。
GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs
:包含生成的客戶端類。
-
使用以下代碼更新 gRPC 客戶端
Program.cs
文件。using System.Threading.Tasks; using Grpc.Net.Client; using GrpcGreeterClient; // 服務(wù)端的地址 using var channel = GrpcChannel.ForAddress("https://localhost:7042"); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine("Greeting: " + reply.Message); Console.WriteLine("Press any key to exit..."); Console.ReadKey();
?
本文中的代碼需要 ASP.NET Core HTTPS 開發(fā)證書來保護 gRPC 服務(wù)。 如果 .NET gRPC 客戶端失敗并顯示消息
The remote certificate is invalid according to the validation procedure.
或The SSL connection could not be established.
,則開發(fā)證書不受信任。 要解決此問題,請參閱
?
2. proto 文件
gRPC 使用協(xié)定優(yōu)先方法進行 API 開發(fā)。 默認情況下,協(xié)議緩沖區(qū) (protobuf) 用作接口定義語言 (IDL)。 .proto
文件包含:
-
gRPC 服務(wù)的定義。
-
在客戶端與服務(wù)器之間發(fā)送的消息。
?
?
.proto 添加到 C# 應(yīng)用
通過將 .proto
文件添加到 <Protobuf>
項組中,可將該文件包含在項目中:
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup>
?
默認情況下,<Protobuf>
引用將生成具體的客戶端和服務(wù)基類。 可使用引用元素的 GrpcServices
特性來限制 C# 資產(chǎn)生成。 有效 GrpcServices
選項如下:
Proto GrpcServices 選項
-
Both
(如果不存在,則為默認值) -
Server
-
Client
-
None
?
.proto 工具支持
需要工具包
-
在每次生成項目時按需生成。
-
不會添加到項目中或是簽入到源代碼管理中。
-
是包含在 obj 目錄中的生成工件。
服務(wù)器和客戶端項目都需要此包。 Grpc.AspNetCore
元包中包含對 Grpc.Tools
的引用。 服務(wù)器項目可以使用 Visual Studio 中的包管理器或通過將 <PackageReference>
添加到項目文件來添加 Grpc.AspNetCore
:
<PackageReference Include="Grpc.AspNetCore" Version="2.40.0" />
?
客戶端項目應(yīng)直接引用 Grpc.Tools
以及使用 gRPC 客戶端所需的其他包。 運行時不需要工具包,因此依賴項標記為 PrivateAssets="All"
:
<PackageReference Include="Google.Protobuf" Version="3.18.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.40.0" /> <PackageReference Include="Grpc.Tools" Version="2.40.0"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference>
?
生成的 C# 代碼
工具包會生成表示在所包含 .proto
文件中定義的消息的 C# 類型。
對于服務(wù)器端資產(chǎn),會生成抽象服務(wù)基類型。 基類型包含 .proto
文件中所含的所有 gRPC 調(diào)用的定義。 創(chuàng)建一個派生自此基類型并為 gRPC 調(diào)用實現(xiàn)邏輯的具體服務(wù)實現(xiàn)。 對于 greet.proto
(前面所述的示例),會生成一個包含虛擬 SayHello
方法的抽象 GreeterBase
類型。 具體實現(xiàn) GreeterService
會替代該方法,并實現(xiàn)處理 gRPC 調(diào)用的邏輯。
public class GreeterService : Greeter.GreeterBase { private readonly ILogger<GreeterService> _logger; public GreeterService(ILogger<GreeterService> logger) { _logger = logger; } public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } }
?
對于客戶端資產(chǎn),會生成一個具體客戶端類型。 .proto
文件中的 gRPC 調(diào)用會轉(zhuǎn)換為具體類型中的方法,可以進行調(diào)用。 對于 greet.proto
(前面所述的示例),會生成一個 GreeterClient
類型。 調(diào)用 GreeterClient.SayHelloAsync
以發(fā)起對服務(wù)器的 gRPC 調(diào)用。
// 端口號必須與gRPC 服務(wù)的端口一致 using var channel = GrpcChannel.ForAddress("https://localhost:7042"); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine("Greeting: " + reply.Message); Console.WriteLine("Press any key to exit..."); Console.ReadKey();
?
默認情況下,會為 <Protobuf>
項組中包含的每個 .proto
文件都生成服務(wù)器和客戶端資產(chǎn)。 若要確保服務(wù)器項目中僅生成服務(wù)器資產(chǎn),請將 GrpcServices
屬性設(shè)置為 Server
。
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup>
?
同樣,該屬性在客戶端項目中設(shè)置為 Client
。
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup>
?
?
3. ProtoBuf Message
消息是 Protobuf 中的主要數(shù)據(jù)傳輸對象, 它們在概念上類似于 .NET 類.
syntax = "proto3"; option csharp_namespace = "Contoso.Messages"; message Person { int32 id = 1; string first_name = 2 // FirstName string last_name = 3; }
?
前面的消息定義將三個字段指定為名稱/值對。 與 .NET 類型上的屬性類似,每個字段都有名稱和類型。 字段類型可以是 Protobuf 標量值類型(如 int32
),也可以是其他消息。
Protobuf 樣式命名風格 建議使用 underscore_separated_names
作為字段名稱。 為 .NET 應(yīng)用創(chuàng)建的新 Protobuf 消息應(yīng)遵循 Protobuf 樣式準則。 .NET 工具會自動生成使用 .NET 命名標準的 .NET 類型。 例如,first_name
Protobuf 字段生成 FirstName
.NET 屬性。
包括名稱,消息定義中的每個字段都有一個唯一的編號。 消息序列化為 Protobuf 時,字段編號用于標識字段。 序列化一個小編號比序列化整個字段名稱要快。 因為字段編號標識字段,所以在更改編號時務(wù)必小心。
生成應(yīng)用時,Protobuf 工具將從 .proto
文件生成 .NET 類型。 Person
消息生成 .NET 類:
public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
?
標量值類型
Protobuf 支持一系列本機標量值類型。 下表列出了全部本機標量值類型及其等效 C# 類型:
Protobuf 類型 | C# 類型 |
---|---|
double |
double |
float |
float |
int32 |
int |
int64 |
long |
uint32 |
uint |
uint64 |
ulong |
sint32 |
int |
sint64 |
long |
fixed32 |
uint |
fixed64 |
ulong |
sfixed32 |
int |
sfixed64 |
long |
bool |
bool |
string |
string |
bytes |
ByteString |
標量值始終具有默認值,并且該默認值不能設(shè)置為 null
。 此約束包括 string
和 ByteString
,它們都屬于 C# 類。 string
默認為空字符串值,ByteString
默認為空字節(jié)值。 嘗試將它們設(shè)置為 null
會引發(fā)錯誤。
日期和時間
本機標量類型不提供與 .NET 的
下表顯示日期和時間類型:
.NET 類型 | Protobuf 已知類型 |
---|---|
DateTimeOffset |
google.protobuf.Timestamp |
DateTime |
google.protobuf.Timestamp |
TimeSpan |
google.protobuf.Duration |
syntax = "proto3"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; message Meeting { string subject = 1; google.protobuf.Timestamp start = 2; google.protobuf.Duration duration = 3; }
?
C# 類中生成的屬性不是 .NET 日期和時間類型。 屬性使用 Google.Protobuf.WellKnownTypes
命名空間中的 Timestamp
和 Duration
類。 這些類提供在 DateTimeOffset
、DateTime
和 TimeSpan
之間進行轉(zhuǎn)換的方法。
Meeting meeting = new Meeting() { Start = DateTime.Now.ToTimestamp(), Duration = Duration.FromTimeSpan(TimeSpan.FromDays(1)) }; DateTime startTime = meeting.Start.ToDateTime(); DateTimeOffset time = meeting.Start.ToDateTimeOffset(); TimeSpan? duration = meeting.Duration?.ToTimeSpan();
?
備注
Timestamp
類型適用于 UTC 時間。DateTimeOffset
值的偏移量始終為零,并且DateTime.Kind
屬性始終為DateTimeKind.Utc
。
可為 null 的類型
C# 的 Protobuf 代碼生成使用本機類型,如 int
表示 int32
。 因此這些值始終包括在內(nèi),不能為 null
。
對于需要顯式 null
的值(例如在 C# 代碼中使用 int?
),Protobuf 的“已知類型”包括編譯為可以為 null 的 C# 類型的包裝器。 若要使用它們,請將 wrappers.proto
導入到 .proto
文件中,如以下代碼所示:
syntax = "proto3"; import "google/protobuf/wrappers.proto"; message Person { // ... google.protobuf.Int32Value age = 5; }
?
wrappers.proto
類型不會在生成的屬性中公開。 Protobuf 會自動將它們映射到 C# 消息中相應(yīng)的可為 null 的 .NET 類型。 例如,google.protobuf.Int32Value
字段生成 int?
屬性。 引用類型屬性(如 string
和 ByteString
)保持不變,但可以向它們分配 null
,這不會引發(fā)錯誤。
下表完整列出了包裝器類型以及它們的等效 C# 類型:
C# 類型 | 已知類型包裝器 |
---|---|
bool? |
google.protobuf.BoolValue |
double? |
google.protobuf.DoubleValue |
float? |
google.protobuf.FloatValue |
int? |
google.protobuf.Int32Value |
long? |
google.protobuf.Int64Value |
uint? |
google.protobuf.UInt32Value |
ulong? |
google.protobuf.UInt64Value |
string |
google.protobuf.StringValue |
ByteString |
google.protobuf.BytesValue |
?
字節(jié)
Protobuf 支持標量值類型為 bytes
的二進制有效負載。 C# 中生成的屬性使用 ByteString
作為屬性類型。
使用 ByteString.CopyFrom(byte[] data)
從字節(jié)數(shù)組創(chuàng)建新實例:
var data = await File.ReadAllBytesAsync(path); var payload = new PayloadResponse(); payload.Data = ByteString.CopyFrom(data);
?
使用 ByteString.Span
或 ByteString.Memory
直接訪問 ByteString
數(shù)據(jù)。 或調(diào)用 ByteString.ToByteArray()
將實例轉(zhuǎn)換回字節(jié)數(shù)組:
var payload = await client.GetPayload(new PayloadRequest()); await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
?
集合
列表
Protobuf 中,在字段上使用 repeated
前綴關(guān)鍵字指定列表。 以下示例演示如何創(chuàng)建列表:
message Person { // ... repeated string roles = 8; }
?
在生成的代碼中,repeated
字段由 Google.Protobuf.Collections.RepeatedField<T>
泛型類型表示。
public class Person { // ... public RepeatedField<string> Roles { get; } }
?
RepeatedField<T>
實現(xiàn)了
var person = new Person(); person.Roles.Add("user"); var roles = new [] { "admin", "manager" }; person.Roles.Add(roles); var list = roles.ToList();
?
?
字典
.NET
ProtoBuf復制
message Person { // ... map<string, string> attributes = 9; }
?
在生成的 .NET 代碼中,map
字段由 Google.Protobuf.Collections.MapField<TKey, TValue>
泛型類型表示。 MapField<TKey, TValue>
實現(xiàn)了 IDictionary。 與 repeated
屬性一樣,map
屬性沒有公共 setter。 項應(yīng)添加到現(xiàn)有集合中。
var person = new Person(); // 添加一項 person.Attributes["created_by"] = "James"; // 添加多項 var attributes = new Dictionary<string, string> { ["last_modified"] = DateTime.UtcNow.ToString() }; person.Attributes.Add(attributes);
?
?
?
?
?
4. 創(chuàng)建 gRPC 服務(wù)和方法
本文檔介紹如何以 C# 創(chuàng)建 gRPC 服務(wù)和方法。 包括:
-
如何在
.proto
文件中定義服務(wù)和方法。 -
使用 gRPC C# 工具生成的代碼。
-
實現(xiàn) gRPC 服務(wù)和方法。
?
創(chuàng)建新的 gRPC 服務(wù)
設(shè)置appsetting.json
"Kestrel": { "EndpointDefaults": { "Protocols": "Http2" } }
?
或者Program.cs 中配置如下代碼:
// Gprc 需要 Http2.0 builder.WebHost.UseKestrel(p => { p.ConfigureEndpointDefaults(opt => { opt.Protocols = HttpProtocols.Http2; }); });
?
引用包:
Grpc.AspNetCore 2.50.0
?
?
服務(wù)和消息是在 .proto
文件中定義的。 然后,C# 工具從 .proto
文件生成代碼。 對于服務(wù)器端資產(chǎn),將為每個服務(wù)生成一個抽象基類型,同時為所有消息生成類。
以下 .proto
文件:
-
定義
Greeter
服務(wù)。 -
Greeter
服務(wù)定義SayHello
調(diào)用。 -
SayHello
發(fā)送HelloRequest
消息并接收HelloReply
消息
syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
?
C# 工具生成 C# GreeterBase
基類型:
public abstract partial class GreeterBase { public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { throw new RpcException(new Status(StatusCode.Unimplemented, "")); } } public class HelloRequest { public string Name { get; set; } } public class HelloReply { public string Message { get; set; } }
?
默認情況下,生成的 GreeterBase
不執(zhí)行任何操作。 它的虛擬 SayHello
方法會將 UNIMPLEMENTED
錯誤返回到調(diào)用它的任何客戶端。 為了使服務(wù)有用,應(yīng)用必須創(chuàng)建 GreeterBase
的具體實現(xiàn):
public class GreeterService : GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" }); } }
?
ServerCallContext
提供服務(wù)器端調(diào)用的上下文。
服務(wù)實現(xiàn)已注冊到應(yīng)用。 如果服務(wù)由 ASP.NET Core gRPC 托管,則應(yīng)使用 MapGrpcService
方法將其添加到路由管道。
app.MapGrpcService<GreeterService>();
?
?
實現(xiàn) gRPC 方法
gRPC 服務(wù)可以有不同類型的方法。 服務(wù)發(fā)送和接收消息的方式取決于所定義的方法的類型。 gRPC 方法類型如下:
-
一元
-
服務(wù)器流式處理
-
客戶端流式處理
-
雙向流式處理
流式處理調(diào)用是使用 stream
關(guān)鍵字在 .proto
文件中指定的。 stream
可以放置在調(diào)用的請求消息和/或響應(yīng)消息中。
syntax = "proto3"; import "google/protobuf/empty.proto"; // 無參包 service ExampleService { // 無參方法 rpc GetPerson1(google.protobuf.Empty) returns (PersonResponse); // 一元 rpc UnaryCall (ExampleRequest) returns (ExampleResponse); // 服務(wù)器流式處理 rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse); // 客戶端流式處理 rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse); // 雙向流式處理 rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse); }
?
每個調(diào)用類型都有不同的方法簽名。 在具體實現(xiàn)中替代從抽象基本服務(wù)類型生成的方法,可確保使用正確的參數(shù)和返回類型。
?
一元方法
一元方法將請求消息作為參數(shù),并返回響應(yīng)。 返回響應(yīng)時,一元調(diào)用完成。
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { var response = new ExampleResponse(); return Task.FromResult(response); }
?
一元調(diào)用與 Web API 控制器上的操作最為相似。 gRPC 方法與操作的一個重要區(qū)別是,gRPC 方法無法將請求的某些部分綁定到不同的方法參數(shù)。 對于傳入請求數(shù)據(jù),gRPC 方法始終有一個消息參數(shù)。 通過在請求消息中設(shè)置多個值字段,仍可以將多個值發(fā)送到 gRPC 服務(wù):
message ExampleRequest { int32 pageIndex = 1; int32 pageSize = 2; bool isDescending = 3; }
?
服務(wù)器流式處理方法
服務(wù)器流式處理方法將請求消息作為參數(shù)。 由于可以將多個消息流式傳輸回調(diào)用方,因此可使用 responseStream.WriteAsync
發(fā)送響應(yīng)消息。 當方法返回時,服務(wù)器流式處理調(diào)用完成。
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { for (var i = 0; i < 5; i++) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1)); } }
?
服務(wù)器流式處理方法啟動后,客戶端無法發(fā)送其他消息或數(shù)據(jù)。 某些流式處理方法設(shè)計為永久運行。 對于連續(xù)流式處理方法,客戶端可以在不再需要調(diào)用時將其取消。 當發(fā)生取消時,客戶端會將信號發(fā)送到服務(wù)器,并引發(fā)
-
所有異步工作都與流式處理調(diào)用一起取消。
-
該方法快速退出。
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { while (!context.CancellationToken.IsCancellationRequested) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } }
?
?
客戶端流式處理方法
客戶端流式處理方法在該方法沒有接收消息的情況下啟動。 requestStream
參數(shù)用于從客戶端讀取消息。 返回響應(yīng)消息時,客戶端流式處理調(diào)用完成:
public override async Task<ExampleResponse> StreamingFromClient( IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context) { while (await requestStream.MoveNext()) { var message = requestStream.Current; // ... } return new ExampleResponse(); }
?
如果使用 C# 8 或更高版本,則可使用 await foreach
語法來讀取消息。 IAsyncStreamReader<T>.ReadAllAsync()
擴展方法讀取請求數(shù)據(jù)流中的所有消息:
public override async Task<ExampleResponse> StreamingFromClient( IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context) { await foreach (var message in requestStream.ReadAllAsync()) { // ... } return new ExampleResponse(); }
?
?
雙向流式處理方法
雙向流式處理方法在該方法沒有接收到消息的情況下啟動。 requestStream
參數(shù)用于從客戶端讀取消息。 該方法可選擇使用 responseStream.WriteAsync
發(fā)送消息。 當方法返回時,雙向流式處理調(diào)用完成:
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { await foreach (var message in requestStream.ReadAllAsync()) { await responseStream.WriteAsync(new ExampleResponse()); } }
?
前面的代碼:
-
發(fā)送每個請求的響應(yīng)。
-
是雙向流式處理的基本用法。
可以支持更復雜的方案,例如同時讀取請求和發(fā)送響應(yīng):
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { // 讀取后臺任務(wù)中的請求。 var readTask = Task.Run(async () => { await foreach (var message in requestStream.ReadAllAsync()) { // Process request. } }); // 發(fā)送響應(yīng),直到客戶端發(fā)出完成的信號。 while (!readTask.IsCompleted) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } }
?
在雙向流式處理方法中,客戶端和服務(wù)可在任何時間互相發(fā)送消息。 雙向方法的最佳實現(xiàn)根據(jù)需求而有所不同。
?
訪問 gRPC 請求標頭
請求消息并不是客戶端將數(shù)據(jù)發(fā)送到 gRPC 服務(wù)的唯一方法。 標頭值在使用 ServerCallContext.RequestHeaders
的服務(wù)中可用。
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { var userAgent = context.RequestHeaders.GetValue("user-agent"); // ... return Task.FromResult(new ExampleResponse()); }
?
?
多線程處理
實現(xiàn)使用多個線程的 gRPC 流式處理方法有一些重要的注意事項。
IAsyncStreamReader<TMessage>
和 IServerStreamWriter<TMessage>
一次只能由一個線程使用。 對于流式處理 gRPC 方法,多個線程無法使用 requestStream.MoveNext()
同時讀取新消息。 多個線程無法使用 responseStream.WriteAsync(message)
同時寫入新消息。
多個線程能夠與 gRPC 方法實現(xiàn)交互的一種安全方法是將生成方-使用者模式與
public override async Task DownloadResults(DataRequest request, IServerStreamWriter<DataResult> responseStream, ServerCallContext context) { var channel = Channel.CreateBounded<DataResult>(new BoundedChannelOptions(capacity: 5)); var consumerTask = Task.Run(async () => { // 從通道中消費消息并寫入響應(yīng)流 await foreach (var message in channel.Reader.ReadAllAsync()) { await responseStream.WriteAsync(message); } }); var dataChunks = request.Value.Chunk(size: 10); // 從多個線程向通道寫入消息 await Task.WhenAll(dataChunks.Select( async c => { var message = new DataResult { BytesProcessed = c.Length }; await channel.Writer.WriteAsync(message); })); // 完成寫作,等待消費者完成 channel.Writer.Complete(); await consumerTask; }
?
備注
雙向流式處理方法采用
IAsyncStreamReader<TMessage>
和IServerStreamWriter<TMessage>
作為自變量。 在彼此獨立的線程上使用這些類型是安全的。
?
5. gRPC 客戶端
.NET 客戶端調(diào)用 gRPC
-
配置 gRPC 客戶端以調(diào)用 gRPC 服務(wù)。
-
對一元、服務(wù)器流式處理、客戶端流式處理和雙向流式處理方法進行 gRPC 調(diào)用。
配置 gRPC 客戶端
需要安裝的包:
Grpc.Net.Client 2.50.0 Google.Protobuf 3.20.0 Grpc.Tools 2.50.0
?
?
gRPC 客戶端是從 .proto
文件生成的具體客戶端類型。 具體 gRPC 客戶端具有轉(zhuǎn)換為 .proto
文件中 gRPC 服務(wù)的方法。 例如,名為 Greeter
的服務(wù)生成 GreeterClient
類型(包含調(diào)用服務(wù)的方法)。
gRPC 客戶端是通過通道創(chuàng)建的。 首先使用 GrpcChannel.ForAddress
創(chuàng)建一個通道,然后使用該通道創(chuàng)建 gRPC 客戶端:
var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new Greet.GreeterClient(channel);
?
通道表示與 gRPC 服務(wù)的長期連接。 創(chuàng)建通道后,進行配置,使其具有與調(diào)用服務(wù)相關(guān)的選項。 例如,可在 GrpcChannelOptions
上指定用于調(diào)用的 HttpClient
、發(fā)收和接收消息的最大大小以及記錄日志,并將其與 GrpcChannel.ForAddress
一起使用。 有關(guān)選項的完整列表,請參閱 客戶端配置選項
。
var channel = GrpcChannel.ForAddress("https://localhost:5001"); var greeterClient = new Greet.GreeterClient(channel); var counterClient = new Count.CounterClient(channel);
?
配置 TLS
gRPC 客戶端必須使用與被調(diào)用服務(wù)相同的連接級別安全性。 gRPC 客戶端傳輸層安全性 (TLS) 是在創(chuàng)建 gRPC 通道時配置的。 如果在調(diào)用服務(wù)時通道和服務(wù)的連接級別安全性不一致,gRPC 客戶端就會拋出錯誤。
若要將 gRPC 通道配置為使用 TLS,請確保服務(wù)器地址以 https
開頭。 例如,GrpcChannel.ForAddress("https://localhost:5001")
使用 HTTPS 協(xié)議。 gRPC 通道自動協(xié)商由 TLS 保護的連接,并使用安全連接進行 gRPC 調(diào)用。
?
若要調(diào)用不安全的 gRPC 服務(wù),請確保服務(wù)器地址以 http
開頭。 例如,GrpcChannel.ForAddress("http://localhost:5000")
使用 HTTP 協(xié)議。 在 .NET Core 3.1 中,必須進行其他配置,才能使用 .NET 客戶端調(diào)用不安全的 gRPC 服務(wù)。
// Net Core 3.1 支持HTTP AppContext.SetSwitch( "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); var channel = GrpcChannel.ForAddress("http://localhost:5000"); var client = new Greet.GreeterClient(channel);
?
客戶端性能
通道及客戶端性能和使用情況:
-
創(chuàng)建通道成本高昂。 重用 gRPC 調(diào)用的通道可提高性能。
-
gRPC 客戶端是使用通道創(chuàng)建的。 gRPC 客戶端是輕型對象,無需緩存或重用。
-
可從一個通道創(chuàng)建多個 gRPC 客戶端(包括不同類型的客戶端)。
-
通道和從該通道創(chuàng)建的客戶端可由多個線程安全使用。
-
從通道創(chuàng)建的客戶端可同時進行多個調(diào)用。
GrpcChannel.ForAddress
不是創(chuàng)建 gRPC 客戶端的唯一選項。 如果要從 ASP.NET Core 應(yīng)用調(diào)用 gRPC 服務(wù),請考慮 gRPC 客戶端工廠集成。 gRPC 與 HttpClientFactory
集成是創(chuàng)建 gRPC 客戶端的集中式操作備選方案。
?
一元調(diào)用
一元調(diào)用從客戶端發(fā)送請求消息開始。 服務(wù)結(jié)束后,返回響應(yīng)消息。
var client = new Greet.GreeterClient(channel); var response = await client.SayHelloAsync(new HelloRequest { Name = "World" }); Console.WriteLine("Greeting: " + response.Message); // Greeting: Hello World
?
.proto
文件中的每個一元服務(wù)方法將在用于調(diào)用方法的具體 gRPC 客戶端類型上產(chǎn)生兩個 .NET 方法:異步方法和阻塞方法。 例如,GreeterClient
具有兩種調(diào)用 SayHello
的方法:
-
GreeterClient.SayHelloAsync
- 以異步方式調(diào)用Greeter.SayHello
服務(wù)。 敬請期待。 -
GreeterClient.SayHello
- 調(diào)用Greeter.SayHello
服務(wù)并阻塞,直至結(jié)束。 不要在異步代碼中使用。
?
服務(wù)器流式處理調(diào)用
服務(wù)器流式處理調(diào)用從客戶端發(fā)送請求消息開始。 ResponseStream.MoveNext()
讀取從服務(wù)流式處理的消息。 ResponseStream.MoveNext()
返回 false
時,服務(wù)器流式處理調(diào)用已完成。
var client = new Greet.GreeterClient(channel); using var call = client.SayHellos(new HelloRequest { Name = "World" }); while (await call.ResponseStream.MoveNext()) { Console.WriteLine("Greeting: " + call.ResponseStream.Current.Message); // "Greeting: Hello World" is written multiple times }
?
如果使用 C# 8 或更高版本,則可使用 await foreach
語法來讀取消息。 IAsyncStreamReader<T>.ReadAllAsync()
擴展方法讀取響應(yīng)數(shù)據(jù)流中的所有消息:
var client = new Greet.GreeterClient(channel); using var call = client.SayHellos(new HelloRequest { Name = "World" }); await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine("Greeting: " + response.Message); // "Greeting: Hello World" is written multiple times }
?
?
客戶端流式處理調(diào)用
客戶端無需發(fā)送消息即可開始客戶端流式處理調(diào)用 。 客戶端可選擇使用 RequestStream.WriteAsync
發(fā)送消息。 客戶端發(fā)送完消息后,應(yīng)調(diào)用 RequestStream.CompleteAsync()
來通知服務(wù)。 服務(wù)返回響應(yīng)消息時,調(diào)用完成。
var client = new Counter.CounterClient(channel); using var call = client.AccumulateCount(); for (var i = 0; i < 3; i++) { await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 }); } // 客戶端發(fā)完消息,通知服務(wù)端返回數(shù)據(jù) await call.RequestStream.CompleteAsync(); var response = await call; Console.WriteLine($"Count: {response.Count}"); // Count: 3
?
雙向流式處理調(diào)用
客戶端無需發(fā)送消息即可開始雙向流式處理調(diào)用 。 客戶端可選擇使用 RequestStream.WriteAsync
發(fā)送消息。 使用 ResponseStream.MoveNext()
或 ResponseStream.ReadAllAsync()
可訪問從服務(wù)流式處理的消息。 ResponseStream
沒有更多消息時,雙向流式處理調(diào)用完成。
var client = new Echo.EchoClient(channel); using var call = client.Echo(); Console.WriteLine("Starting background task to receive messages"); var readTask = Task.Run(async () => { await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine(response.Message); // Echo messages sent to the service } }); Console.WriteLine("Starting to send messages"); Console.WriteLine("Type a message to echo then press enter."); while (true) { var result = Console.ReadLine(); if (string.IsNullOrEmpty(result)) { break; } await call.RequestStream.WriteAsync(new EchoMessage { Message = result }); } Console.WriteLine("Disconnecting"); await call.RequestStream.CompleteAsync(); await readTask;
?
為獲得最佳性能并避免客戶端和服務(wù)中出現(xiàn)不必要的錯誤,請嘗試正常完成雙向流式調(diào)用。 當服務(wù)器已讀取請求流且客戶端已讀取響應(yīng)流時,雙向調(diào)用正常完成。 前面的示例調(diào)用就是一個正常結(jié)束的雙向調(diào)用。 在調(diào)用中,客戶端:
-
通過調(diào)用
EchoClient.Echo
啟動新的雙向流式調(diào)用。 -
使用
ResponseStream.ReadAllAsync()
創(chuàng)建用于從服務(wù)中讀取消息的后臺任務(wù)。 -
使用
RequestStream.WriteAsync
將消息發(fā)送到服務(wù)器。 -
使用
RequestStream.CompleteAsync()
通知服務(wù)器它已發(fā)送消息。 -
等待直到后臺任務(wù)已讀取所有傳入消息。
雙向流式處理調(diào)用期間,客戶端和服務(wù)可在任何時間互相發(fā)送消息。 與雙向調(diào)用交互的最佳客戶端邏輯因服務(wù)邏輯而異。
?
訪問 gRPC 標頭
gRPC 調(diào)用返回響應(yīng)頭。 HTTP 響應(yīng)頭傳遞與返回的消息不相關(guān)的調(diào)用的名稱/值元數(shù)據(jù)。
標頭可通過 ResponseHeadersAsync
進行訪問,它會返回元數(shù)據(jù)的集合。 標頭通常隨響應(yīng)消息一起返回;因此,必須等待它們返回。
var client = new Greet.GreeterClient(channel); using var call = client.SayHelloAsync(new HelloRequest { Name = "World" }); var headers = await call.ResponseHeadersAsync; var myValue = headers.GetValue("my-trailer-name"); var response = await call.ResponseAsync;
?
使用 ResponseHeadersAsync
時:
-
必須等待
ResponseHeadersAsync
的結(jié)果才能獲取標頭集合。 -
無需在
ResponseAsync
(或流式處理時的響應(yīng)流)之前訪問。 如果已返回響應(yīng),則ResponseHeadersAsync
立即返回標頭。 -
如果存在連接或服務(wù)器錯誤,并且 gRPC 調(diào)用未返回標頭,將引發(fā)異常。
?
配置截止時間
建議配置 gRPC 調(diào)用截止時間,因為它提供調(diào)用時間的上限。 它能阻止異常運行的服務(wù)持續(xù)運行并耗盡服務(wù)器資源。 截止時間對于構(gòu)建可靠應(yīng)用非常有效。
配置 CallOptions.Deadline
以設(shè)置 gRPC 調(diào)用的截止時間:
var client = new Greet.GreeterClient(channel); try { var response = await client.SayHelloAsync( new HelloRequest { Name = "World" }, deadline: DateTime.UtcNow.AddSeconds(5)); // Greeting: Hello World Console.WriteLine("Greeting: " + response.Message); } catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) { Console.WriteLine("Greeting timeout."); }
?
?
客戶端工廠
gRPC 與 HttpClientFactory
的集成提供了一種創(chuàng)建 gRPC 客戶端的集中方式。 它可用作配置獨立 gRPC 客戶端實例
(上一節(jié)內(nèi)容)的替代方法。
工廠具有以下優(yōu)勢:
-
提供了用于配置邏輯 gRPC 客戶端實例的中心位置。
-
可管理基礎(chǔ)
HttpClientMessageHandler
的生存期。 -
在 ASP.NET Core gRPC 服務(wù)中自動傳播截止時間和取消。
?
注冊 gRPC 客戶端
若要注冊 gRPC 客戶端,可在 Program.cs
中的應(yīng)用入口點處的
builder.Services.AddGrpcClient<UserService.UserServiceClient>(p => { p.Address = new Uri("http://localhost:5227"); });
?
gRPC 客戶端類型通過依賴項注入 (DI) 注冊為暫時性。 現(xiàn)在可以在由 DI 創(chuàng)建的類型中直接注入和使用客戶端。 ASP.NET Core MVC 控制器、SignalR 中心和 gRPC 服務(wù)是可以自動注入 gRPC 客戶端的位置:
[Route("[controller]/[action]")] [ApiController] public class UserController:ControllerBase { private readonly UserService.UserServiceClient _userClient; public UserController(UserService.UserServiceClient userClient) { _userClient = userClient; } [HttpGet] public IActionResult GetUserList() { return Ok(_userClient.GetUserList(new UserRequest())); } }
?
調(diào)用憑據(jù)
使用 AddCallCredentials
方法將身份驗證標頭添加到 gRPC 調(diào)用, 此方法在
builder.Services.AddHttpContextAccessor(); // 客戶端配置JWT builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(p => { // ... }); builder.Services.AddAuthorization(); builder.Services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) // 添加調(diào)用憑據(jù) .AddCallCredentials(async (context, metadata) => { var serviceProvider = builder.Services.BuildServiceProvider(); var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>(); // 獲取客戶端的token var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token"); if (string.IsNullOrWhiteSpace(accessToken)) { // 如果客戶端沒有拿到token ,直接從授權(quán)中心拿Token(這一步通常用不上) var channel = GrpcChannel.ForAddress("http://localhost:5015"); accessToken = new TokenService.TokenServiceClient(channel).GetToken(new() { UserName = "admin", Pwd = "123" }).AccessToken; channel.Dispose(); } if (!string.IsNullOrEmpty(accessToken)) { metadata.Add("Authorization", $"Bearer {accessToken}"); } }).ConfigureChannel(o => o.UnsafeUseInsecureChannelCallCredentials = true); var app = builder.Build(); // ... app.UseAuthentication(); app.UseAuthorization(); // ...
?
有關(guān)配置調(diào)用憑據(jù)的詳細信息,請參gRPC身份驗證和授權(quán)
。
?
截止時間與取消
截止時間和取消功能是 gRPC 客戶端用來中止進行中調(diào)用的功能。 本文介紹截止時間和取消功能非常重要的原因,以及如何在 .NET gRPC 應(yīng)用中使用它們。
截止時間
截止時間功能讓 gRPC 客戶端可以指定等待調(diào)用完成的時間。 超過截止時間時,將取消調(diào)用。 設(shè)定一個截止時間非常重要,因為它將提供調(diào)用可運行的最長時間。 它能阻止異常運行的服務(wù)持續(xù)運行并耗盡服務(wù)器資源。 截止時間對于構(gòu)建可靠應(yīng)用非常有效,應(yīng)該進行配置。
截止時間配置:
-
在進行調(diào)用時,使用
CallOptions.Deadline
配置截止時間。 -
沒有截止時間默認值。 gRPC 調(diào)用沒有時間限制,除非指定了截止時間。
-
截止時間指的是超過截止時間的 UTC 時間。 例如,
DateTime.UtcNow.AddSeconds(5)
是從現(xiàn)在起 5 秒的截止時間。 -
如果使用的是過去或當前的時間,則調(diào)用將立即超過截止時間。
-
截止時間隨 gRPC 調(diào)用發(fā)送到服務(wù),并由客戶端和服務(wù)獨立跟蹤。 gRPC 調(diào)用可能在一臺計算機上完成,但當響應(yīng)返回給客戶端時,已超過了截止時間。
如果超過了截止時間,客戶端和服務(wù)將有不同的行為:
-
客戶端將立即中止基礎(chǔ)的 HTTP 請求并引發(fā)
DeadlineExceeded
錯誤。 客戶端應(yīng)用可以選擇捕獲錯誤并向用戶顯示超時消息。 -
在服務(wù)器上,將中止正在執(zhí)行的 HTTP 請求,并引發(fā)
配置 CallOptions.Deadline
以設(shè)置 gRPC 調(diào)用的截止時間:
var client = new Greet.GreeterClient(channel); try { var response = await client.SayHelloAsync( new HelloRequest { Name = "World" }, deadline: DateTime.UtcNow.AddSeconds(5)); // Greeting: Hello World Console.WriteLine("Greeting: " + response.Message); } catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) { Console.WriteLine("Greeting timeout."); }
?
在 gRPC 服務(wù)中使用 ServerCallContext.CancellationToken
:
public override async Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { var user = await _databaseContext.GetUserAsync(request.Name, context.CancellationToken); return new HelloReply { Message = "Hello " + user.DisplayName }; }
?
截止時間和重試
當 gRPC 調(diào)用配置了故障處理
和截止日期時,截止日期會跟蹤 gRPC 調(diào)用的所有重試時間。 如果超過了截止時間,gRPC 調(diào)用會立即中止底層 HTTP 請求,跳過任何剩余的重試,并引發(fā) DeadlineExceeded
錯誤。
?
傳播截止時間
需要安裝包:
Grpc.AspNetCore.Server.ClientFactory 2.50.0
?
?
從正在執(zhí)行的 gRPC 服務(wù)進行 gRPC 調(diào)用時,應(yīng)傳播截止時間。 例如:
-
客戶端應(yīng)用調(diào)用帶有截止時間的
FrontendService.GetUser
。 -
FrontendService
調(diào)用UserService.GetUser
。 客戶端指定的截止時間應(yīng)隨新的 gRPC 調(diào)用進行指定。 -
UserService.GetUser
接收截止時間。 如果超過了客戶端應(yīng)用的截止時間,將正確超時。
調(diào)用上下文將使用 ServerCallContext.Deadline
提供截止時間:
public override async Task<UserResponse> GetUser(UserRequest request, ServerCallContext context) { var client = new User.UserServiceClient(_channel); var response = await client.GetUserAsync( new UserRequest { Id = request.Id }, deadline: context.Deadline); return response; }
?
手動傳播截止時間可能會很繁瑣。 截止時間需要傳遞給每個調(diào)用,很容易不小心錯過。 gRPC 客戶端工廠提供自動解決方案。 指定 EnableCallContextPropagation
(需要安裝包:
-
自動將截止時間和取消令牌傳播到子調(diào)用。
-
如果子調(diào)用指定較早的截止時間,則不傳播截止時間。 例如,如果子調(diào)用使用
CallOptions.Deadline
指定新的 5 秒截止時間,則不使用傳播的 10 秒截止時間。 當多個截止時間可用時,使用最早的截止時間。 -
這是確保復雜的嵌套 gRPC 場景始終傳播截止時間和取消的一種極佳方式。
services .AddGrpcClient<User.UserServiceClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .EnableCallContextPropagation();
?
默認情況下,如果客戶端在 gRPC 調(diào)用的上下文之外使用,EnableCallContextPropagation
將引發(fā)錯誤。 此錯誤旨在提醒你沒有要傳播的調(diào)用上下文。 如果要在調(diào)用上下文之外使用客戶端,請使用 SuppressContextNotFoundErrors
在配置客戶端時禁止顯示該錯誤:
builder.Services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true);
?
?
故障處理與重試
gRPC 重試是一項功能,允許 gRPC 客戶端自動重試失敗的調(diào)用。 本文介紹如何配置重試策略,以便在 .NET 中創(chuàng)建可復原的容錯 gRPC 應(yīng)用。
gRPC 重試需要
?
暫時性故障處理
暫時性故障可能會中斷 gRPC 調(diào)用。 暫時性故障包括:
-
暫時失去網(wǎng)絡(luò)連接。
-
服務(wù)暫時不可用。
-
由于服務(wù)器負載導致超時。
gRPC 調(diào)用中斷時,客戶端會引發(fā) RpcException
并提供有關(guān)錯誤的詳細信息。 客戶端應(yīng)用必須捕獲異常并選擇如何處理錯誤。
var client = new Greeter.GreeterClient(channel); try { var response = await client.SayHelloAsync( new HelloRequest { Name = ".NET" }); Console.WriteLine("From server: " + response.Message); } catch (RpcException ex) { // Write logic to inspect the error and retry // if the error is from a transient fault. }
?
在整個應(yīng)用中復制重試邏輯是非常冗長的代碼,容易出錯。 幸運的是,.NET gRPC 客戶端擁有自動重試的內(nèi)置支持。
?
配置 gRPC 重試策略
重試策略在創(chuàng)建 gRPC 通道時配置一次:
var defaultMethodConfig = new MethodConfig { Names = { MethodName.Default }, RetryPolicy = new RetryPolicy { MaxAttempts = 5, InitialBackoff = TimeSpan.FromSeconds(1), MaxBackoff = TimeSpan.FromSeconds(5), BackoffMultiplier = 1.5, RetryableStatusCodes = { StatusCode.Unavailable } } }; var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } });
?
上述代碼:
-
創(chuàng)建一個
MethodConfig
。 重試策略可以按方法配置,而方法可以使用Names
屬性進行匹配。 此方法配置有MethodName.Default
,因此它將應(yīng)用于此通道調(diào)用的所有 gRPC 方法。 -
配置重試策略。 此策略將指示客戶端自動重試狀態(tài)代碼為
Unavailable
的失敗 gRPC 調(diào)用。 -
通過設(shè)置
GrpcChannelOptions.ServiceConfig
,將創(chuàng)建的通道配置為使用該重試策略。
隨著該通道一起創(chuàng)建的 gRPC 客戶端將自動重試失敗的調(diào)用:
var client = new Greeter.GreeterClient(channel); var response = await client.SayHelloAsync( new HelloRequest { Name = ".NET" }); Console.WriteLine("From server: " + response.Message);
?
gRPC 重試選項
下表描述了用于配置 gRPC 重試策略的選項:
選項 | 描述 |
---|---|
MaxAttempts |
最大調(diào)用嘗試次數(shù),包括原始嘗試。 此值受 GrpcChannelOptions.MaxRetryAttempts (默認值為 5)的限制。 必須為該選項提供值,且值必須大于 1。 |
InitialBackoff |
重試嘗試之間的初始退避延遲。 介于 0 與當前退避之間的隨機延遲確定何時進行下一次重試嘗試。 每次嘗試后,當前退避將乘以 BackoffMultiplier 。 必須為該選項提供值,且值必須大于 0。 |
MaxBackoff |
最大退避會限制指數(shù)退避增長的上限。 必須為該選項提供值,且值必須大于 0。 |
BackoffMultiplier |
每次重試嘗試后,退避將乘以該值,并將在乘數(shù)大于 1 的情況下以指數(shù)方式增加。 必須為該選項提供值,且值必須大于 0。 |
RetryableStatusCodes |
狀態(tài)代碼的集合。 具有匹配狀態(tài)的失敗 gRPC 調(diào)用將自動重試。 有關(guān)狀態(tài)代碼的更多信息,請參閱 |
重試何時有效
滿足以下條件時,將重試調(diào)用:
-
失敗狀態(tài)代碼與
RetryableStatusCodes
中的值匹配。 -
之前的嘗試次數(shù)小于
MaxAttempts
。 -
此調(diào)用未提交。
-
尚未超過截止時間。
在以下兩種情況下,將提交 gRPC 調(diào)用:
-
客戶端收到響應(yīng)頭。 調(diào)用
ServerCallContext.WriteResponseHeadersAsync
或?qū)⒌谝粋€消息寫入服務(wù)器響應(yīng)流時,服務(wù)器會發(fā)送響應(yīng)頭。 -
客戶端的傳出消息(如果是流式處理則為消息)已超出客戶端的最大緩沖區(qū)大小。
MaxRetryBufferSize
和MaxRetryBufferPerCallSize
在gPRC 配置
這一小節(jié)中進行配置。
無論狀態(tài)代碼是什么或以前的嘗試次數(shù)是多少,提交的調(diào)用都無法重試。
?
流式處理調(diào)用
流式處理調(diào)用可以與 gRPC 重試一起使用,但在將它們一起使用時,務(wù)必注意以下事項:
-
服務(wù)器流式處理、雙向流式處理: 在已收到第一個消息后,從服務(wù)器返回多個消息的流式處理 RPC 無法重試。 應(yīng)用必須添加額外的邏輯才能手動重新建立服務(wù)器和雙向流式傳輸調(diào)用。
-
客戶端流式處理、雙向流式處理: 傳出消息超出客戶端的最大緩沖區(qū)大小時,向服務(wù)器發(fā)送多個消息的流式處理 RPC 無法重試。 可通過配置增加最大緩沖區(qū)大小。
?
客戶端負載均衡
客戶端負載均衡功能允許 gRPC 客戶端以最佳方式在可用服務(wù)器之間分配負載。 本文介紹了如何配置客戶端負載均衡,以在 .NET 中創(chuàng)建可縮放的高性能 gRPC 應(yīng)用。
使用客戶端負載均衡需要具備以下組件:
-
.NET 5 或更高版本。
-
?
配置客戶端負載均衡
客戶端負載均衡是在創(chuàng)建通道時配置的。 使用負載均衡時需要考慮兩個組件:
-
解析程序,用于解析通道的地址。 解析程序支持從外部源獲取地址。 這也被稱為服務(wù)發(fā)現(xiàn)。
-
負載均衡器,用于創(chuàng)建連接,并選取 gRPC 調(diào)用將使用的地址。
解析程序和負載均衡器的內(nèi)置實現(xiàn)包含在
地址、連接和其他負載均衡狀態(tài)存儲在 GrpcChannel
實例中。 在進行 gRPC 調(diào)用時,必須重用通道,以使負載均衡正常工作。
?
配置解析程序
解析程序是使用創(chuàng)建通道時所用的地址配置的。
Scheme | 類型 | 說明 |
---|---|---|
dns |
DnsResolverFactory |
通過查詢 |
static |
StaticResolverFactory |
解析應(yīng)用已指定的地址。 如果應(yīng)用已經(jīng)知道它調(diào)用的地址,則建議使用。 |
通道不會直接調(diào)用與解析程序匹配的 URI。 而是創(chuàng)建一個匹配的解析程序,用它來解析地址。
例如,使用 GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })
:
-
dns
方案映射到DnsResolverFactory
。 為通道創(chuàng)建 DNS 解析程序的一個新實例。 -
解析程序?qū)?
my-example-host
進行 DNS 查詢,并獲得兩個結(jié)果:localhost:80
和localhost:81
。 -
負載均衡器使用
localhost:80
和localhost:81
創(chuàng)建連接并進行 gRPC 調(diào)用。
DnsResolverFactory
DnsResolverFactory
創(chuàng)建一個解析程序,旨在從外部源獲取地址。 DNS 解析通常用于對具有
var channel = GrpcChannel.ForAddress( "dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }); var client = new Greet.GreeterClient(channel); var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
?
前面的代碼:
-
使用地址
dns:///my-example-host
配置已創(chuàng)建的通道。dns
方案映射到DnsResolverFactory
。 -
不指定負載均衡器。 通道默認選取第一個負載均衡器。
-
啟動 gRPC 調(diào)用SayHello
-
DNS 解析程序為主機名
my-example-host
獲取地址。 -
選取第一個負載均衡器以嘗試連接到一個已解析的地址。
-
調(diào)用被發(fā)送到通道成功連接到的第一個地址。
-
?
DNS 地址緩存
在使用負載均衡時,性能非常重要。 通過緩存地址,從 gRPC 調(diào)用中消除了解析地址的延遲。 進行第一次 gRPC 調(diào)用時,將調(diào)用解析程序,后續(xù)調(diào)用將使用緩存。
如果連接中斷,則會自動刷新地址。 如果是在運行時更改地址,那么刷新非常重要。 例如,在 Kubernetes 中,
默認情況下,如果連接中斷,則會刷新 DNS 解析程序。 DNS 解析程序還可以根據(jù)需要定期刷新。 這對于快速檢測新的 pod 實例很有用。
services.AddSingleton<ResolverFactory>(
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
上面的代碼創(chuàng)建具有刷新間隔的 DnsResolverFactory
,并將其注冊到依賴關(guān)系注入。
?
StaticResolverFactory
靜態(tài)解析程序由 StaticResolverFactory
提供。 此解析程序:
-
不調(diào)用外部源。 相反,客戶端應(yīng)用會配置地址。
-
適用于應(yīng)用已經(jīng)知道它調(diào)用的地址的情況。
var factory = new StaticResolverFactory(addr => new[] { new BalancerAddress("localhost", 80), new BalancerAddress("localhost", 81) }); var services = new ServiceCollection(); services.AddSingleton<ResolverFactory>(factory); var channel = GrpcChannel.ForAddress( "static:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure, ServiceProvider = services.BuildServiceProvider() }); var client = new Greet.GreeterClient(channel);
?
上述代碼:
-
創(chuàng)建一個
StaticResolverFactory
。 此工廠知道兩個地址:localhost:80
和localhost:81
。 -
使用依賴項注入 (DI) 來注冊工廠。
-
使用以下內(nèi)容配置已創(chuàng)建的通道:
-
地址
static:///my-example-host
。static
方案映射到靜態(tài)解析程序。 -
使用 DI 服務(wù)提供程序設(shè)置
GrpcChannelOptions.ServiceProvider
。
-
本示例為 DI 創(chuàng)建了一個新的
?
配置負載均衡器
負載均衡器是使用 ServiceConfig.LoadBalancingConfigs
集合在
名稱 | 類型 | 說明 |
---|---|---|
pick_first |
PickFirstLoadBalancerFactory |
嘗試連接到地址,直到成功建立連接。 gRPC 調(diào)用都是針對第一次成功連接進行的。 |
round_robin |
RoundRobinLoadBalancerFactory |
嘗試連接到所有地址。 gRPC 調(diào)用是使用 |
service config
是“service configuration”的縮寫形式,用 ServiceConfig
類型表示。 有幾種方法可以讓通道獲取配置了負載均衡器的 service config
:
-
當使用
GrpcChannelOptions.ServiceConfig
創(chuàng)建通道時,應(yīng)用可以指定service config
。 -
或者,解析程序可以為通道解析
service config
。 此功能允許外部源指定其調(diào)用方應(yīng)如何執(zhí)行負載均衡。 解析程序是否支持解析service config
取決于解析程序?qū)崿F(xiàn)。 使用GrpcChannelOptions.DisableResolverServiceConfig
禁用此功能。 -
如果未提供
service config
,或者service config
沒有配置負載均衡器,則通道默認為PickFirstLoadBalancerFactory
。
var channel = GrpcChannel.ForAddress( "dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure, ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } } }); var client = new Greet.GreeterClient(channel); var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
?
前面的代碼:
-
在
service config
中指定了RoundRobinLoadBalancerFactory
。 -
啟動 gRPC 調(diào)用SayHello
-
DnsResolverFactory
創(chuàng)建一個解析程序,用于為主機名my-example-host
獲取地址。 -
輪循機制負載均衡器嘗試連接到所有已解析的地址。
-
gRPC 調(diào)用使用輪循機制邏輯均勻分布。
-
?
配置通道憑據(jù)
通道必須知道是否使用
-
ChannelCredentials.SecureSsl
- gRPC 調(diào)用使用ChannelCredentials.SecureSsl
進行保護。 等同于https
地址。 -
ChannelCredentials.Insecure
- gRPC 調(diào)用不使用傳輸安全性。 等同于http
地址。
var channel = GrpcChannel.ForAddress( "dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }); var client = new Greet.GreeterClient(channel); var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
?
?
6. gRPC JSON 轉(zhuǎn)碼
gRPC 有一個限制,即不是所有平臺都可以使用它。 瀏覽器并不完全支持 HTTP/2,這使得
gRPC JSON 轉(zhuǎn)碼是為 gRPC 服務(wù)創(chuàng)建 RESTful JSON API 的 ASP.NET Core 的擴展。 配置轉(zhuǎn)碼后,應(yīng)用可以使用熟悉的 HTTP 調(diào)用 gRPC 服務(wù):
-
HTTP 謂詞
-
URL 參數(shù)綁定
-
JSON 請求/響應(yīng)
gRPC 仍然可以用來調(diào)用服務(wù)。
使用步驟
-
將包引用添加到
-
通過添加
AddJsonTranscoding
,在服務(wù)器啟動代碼中注冊轉(zhuǎn)碼:在Program.cs
文件中,將builder.Services.AddGrpc();
更改為builder.Services.AddGrpc().AddJsonTranscoding();
。 -
在包含
.csproj
文件的項目目錄中創(chuàng)建目錄結(jié)構(gòu)/google/api
。 -
將
-
用 HTTP 綁定和路由在
.proto
文件中注釋 gRPC 方法:
syntax = "proto3"; option csharp_namespace = "GrpcServiceTranscoding"; import "google/api/annotations.proto"; package greet; // 定義Greeter服務(wù) service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { get: "/v1/greeter/{name}" }; } } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; }
?
SayHello
gRPC 方法現(xiàn)在可以作為 gRPC 和 JSON Web API 調(diào)用:
-
請求:
GET /v1/greeter/world
-
響應(yīng):
{ "message": "Hello world" }
HTTP 協(xié)議
.NET SDK 中包含的 ASP.NET Core gRPC 服務(wù)模板創(chuàng)建僅針對 HTTP/2 配置的應(yīng)用。 當應(yīng)用僅支持傳統(tǒng)的 gRPC over HTTP/2 時,HTTP/2 是很好的默認設(shè)置。 但是,轉(zhuǎn)碼同時適用于 HTTP/1.1 和 HTTP/2。 某些平臺(如 UWP 或 Unity)無法使用 HTTP/2。 若要支持所有客戶端應(yīng)用,請將服務(wù)器配置為啟用 HTTP/1.1 和 HTTP/2。
更新 appsettings.json
中的默認協(xié)議:
{ "Kestrel": { "EndpointDefaults": { "Protocols": "Http1AndHttp2" } } } 或者,在啟動代碼中配置 Kestrel 終結(jié)點 builder.WebHost.UseKestrel(p => { p.ConfigureEndpointDefaults(opt => { opt.Protocols = HttpProtocols.Http1AndHttp2; }); });
?
HTTP 規(guī)則
gRPC JSON 轉(zhuǎn)碼從 gRPC 方法創(chuàng)建 RESTful JSON Web API。 它使用用于自定義如何將 RESTful API 映射到 gRPC 方法的注釋和選項。
gRPC 方法必須在支持轉(zhuǎn)碼之前使用 HTTP 規(guī)則進行注釋。 HTTP 規(guī)則包括有關(guān)將 gRPC 方法作為 RESTful API 調(diào)用的信息,例如 HTTP 方法和路由。
import "google/api/annotations.proto"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { get: "/v1/greeter/{name}" }; } }
?
HTTP 規(guī)則:
-
是 gRPC 方法的注釋。
-
由名稱
google.api.http
標識。 -
從
google/api/annotations.proto
文件導入。
HTTP 方法
通過將路由設(shè)置為匹配的 HTTP 方法字段名稱來指定 HTTP 方法:
-
get
-
put
-
post
-
delete
-
patch
custom
字段適用于其他 HTTP 方法。
在以下示例中,CreateAddress
方法使用指定的路由映射到 POST
:
service Address { rpc CreateAddress (CreateAddressRequest) returns (CreateAddressReply) { option (google.api.http) = { post: "/v1/address", body: "*" }; } }
?
生成Swagger
-
將 gRPC JSON 轉(zhuǎn)碼與
-
.NET 7 中的實驗性是允許我們探索提供 OpenAPI 支持的最佳方式。
若要使用 gRPC JSON 轉(zhuǎn)碼啟用 OpenAPI,請執(zhí)行以下操作:
-
將包引用添加到
-
在啟動時配置 Swashbuckle。
AddGrpcSwagger
方法將 Swashbuckle 配置為包含 gRPC 終結(jié)點。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc().AddJsonTranscoding(); // JSON 轉(zhuǎn)碼 builder.Services.AddGrpcSwagger(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "gRPC transcoding", Version = "v1" }); }); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); app.MapGrpcService<GreeterService>(); app.Run();
?
Swagger 文檔注釋
根據(jù) .proto
協(xié)定中的注釋生成 OpenAPI 說明,如以下示例所示:
ProtoBuf復制
// My amazing greeter service. service Greeter { // Sends a greeting. rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { get: "/v1/greeter/{name}" }; } } message HelloRequest { // Name to say hello to. string name = 1; } message HelloReply { // Hello reply message. string message = 1; }
?
若要啟用 gRPC OpenAPI 注釋,請執(zhí)行以下操作:
-
使用
<GenerateDocumentationFile>true</GenerateDocumentationFile>
啟用服務(wù)器項目中的 XML 文檔文件。 -
配置
AddSwaggerGen
以讀取生成的 XML 文件。 將 XML 文件路徑傳遞到IncludeXmlComments
和IncludeGrpcXmlComments
,如以下示例所示:
builder.Services.AddSwaggerGen(c => { var filePath = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); c.IncludeXmlComments(filePath); c.IncludeGrpcXmlComments(filePath, includeControllerXmlComments: true); }).AddGrpcSwagger();
?
若要確認 Swashbuckle 為 RESTful gRPC 服務(wù)生成帶說明的 OpenAPI ,請啟動應(yīng)用并導航到 Swagger UI 頁面:
?
?
7. gRPC 配置
配置服務(wù)選項
gRPC 服務(wù)在 Startup.cs
中使用 AddGrpc
進行配置。 配置選項位于
下表描述了用于配置 gRPC 服務(wù)的選項:
選項 | 默認值 | 說明 |
---|---|---|
MaxSendMessageSize |
null |
可以從服務(wù)器發(fā)送的最大消息大?。ㄒ宰止?jié)為單位)。 嘗試發(fā)送超過配置的最大消息大小的消息會導致異常。 設(shè)置為 null 時,消息的大小不受限制。 |
MaxReceiveMessageSize |
4 MB | 可以由服務(wù)器接收的最大消息大小(以字節(jié)為單位)。 如果服務(wù)器收到的消息超過此限制,則會引發(fā)異常。 增大此值可使服務(wù)器接收更大的消息,但可能會對內(nèi)存消耗產(chǎn)生負面影響。 設(shè)置為 null 時,消息的大小不受限制。 |
EnableDetailedErrors |
false |
如果為 true ,則當服務(wù)方法中引發(fā)異常時,會將詳細異常消息返回到客戶端。 默認值為 false 。 將 EnableDetailedErrors 設(shè)置為 true 可能會泄漏敏感信息。 |
CompressionProviders |
gzip | 用于壓縮和解壓縮消息的壓縮提供程序的集合。 可以創(chuàng)建自定義壓縮提供程序并將其添加到集合中。 默認已配置提供程序支持 gzip 壓縮。 |
ResponseCompressionAlgorithm |
null |
壓縮算法用于壓縮從服務(wù)器發(fā)送的消息。 該算法必須與 CompressionProviders 中的壓縮提供程序匹配。 若要使算法可壓縮響應(yīng),客戶端必須通過在 grpc-accept-encoding 標頭中進行發(fā)送來指示它支持算法。 |
ResponseCompressionLevel |
null |
用于壓縮從服務(wù)器發(fā)送的消息的壓縮級別。 |
Interceptors |
None | 隨每個 gRPC 調(diào)用一起運行的偵聽器的集合。 偵聽器按注冊順序運行。 全局配置的偵聽器在為單個服務(wù)配置的偵聽器之前運行。 偵聽器默認為每個請求設(shè)置生存期。 將調(diào)用偵聽器構(gòu)造函數(shù),并從 |
IgnoreUnknownServices |
false |
如果為 true ,則對未知服務(wù)和方法的調(diào)用不會返回 UNIMPLEMENTED 狀態(tài),并且請求會傳遞到 ASP.NET Core 中的下一個注冊中間件。 |
可以通過在 Startup.ConfigureServices
中向 AddGrpc
調(diào)用提供選項委托,為所有服務(wù)配置選項:
public void ConfigureServices(IServiceCollection services) { services.AddGrpc(options => { options.EnableDetailedErrors = true; options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB }); }
?
用于單個服務(wù)的選項會替代 AddGrpc
中提供的全局選項,可以使用 AddServiceOptions<TService>
進行配置:
public void ConfigureServices(IServiceCollection services) { services.AddGrpc().AddServiceOptions<MyService>(options => { options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB }); }
?
服務(wù)偵聽器默認為每個請求設(shè)置生存期。 使用 DI 注冊偵聽器類型會覆蓋創(chuàng)建偵聽器的方式及其生存期。
public void ConfigureServices(IServiceCollection services) { services.AddGrpc(options => { options.Interceptors.Add<LoggingInterceptor>(); }); services.AddSingleton<LoggingInterceptor>(); }
?
配置客戶端選項
gRPC 客戶端配置在 GrpcChannelOptions
中進行設(shè)置。 配置選項位于
下表描述了用于配置 gRPC 通道的選項:
選項 | 默認值 | 說明 |
---|---|---|
HttpHandler |
新實例 | 用于進行 gRPC 調(diào)用的 HttpMessageHandler 。 可以將客戶端設(shè)置為配置自定義 HttpClientHandler ,或?qū)⒏郊犹幚沓绦蛱砑拥?gRPC 調(diào)用的 HTTP 管道。 如果未指定 HttpMessageHandler ,則會通過自動處置為通道創(chuàng)建新 HttpClientHandler 實例。 |
HttpClient |
null |
用于進行 gRPC 調(diào)用的 HttpClient 。 此設(shè)置是 HttpHandler 的替代項。 |
DisposeHttpClient |
false |
如果設(shè)置為 true 且指定了 HttpMessageHandler 或 HttpClient ,則在處置 GrpcChannel 時,將分別處置 HttpHandler 或 HttpClient 。 |
LoggerFactory |
null |
客戶端用于記錄有關(guān) gRPC 調(diào)用的信息的 LoggerFactory 。 可以通過依賴項注入來解析或使用 LoggerFactory.Create 來創(chuàng)建 LoggerFactory 實例。 |
MaxSendMessageSize |
null |
可以從客戶端發(fā)送的最大消息大小(以字節(jié)為單位)。 嘗試發(fā)送超過配置的最大消息大小的消息會導致異常。 設(shè)置為 null 時,消息的大小不受限制。 |
MaxReceiveMessageSize |
4 MB | 可以由客戶端接收的最大消息大小(以字節(jié)為單位)。 如果客戶端收到的消息超過此限制,則會引發(fā)異常。 增大此值可使客戶端接收更大的消息,但可能會對內(nèi)存消耗產(chǎn)生負面影響。 設(shè)置為 null 時,消息的大小不受限制。 |
Credentials |
null |
一個 ChannelCredentials 實例。 憑據(jù)用于將身份驗證元數(shù)據(jù)添加到 gRPC 調(diào)用。 |
CompressionProviders |
gzip | 用于壓縮和解壓縮消息的壓縮提供程序的集合。 可以創(chuàng)建自定義壓縮提供程序并將其添加到集合中。 默認已配置提供程序支持 gzip 壓縮。 |
ThrowOperationCanceledOnCancellation |
false |
如果設(shè)置為 true ,則在取消調(diào)用或超過其截止時間時,客戶端將引發(fā) |
UnsafeUseInsecureChannelCallCredentials |
false |
如果設(shè)置為 true ,則 CallCredentials 應(yīng)用于不安全通道發(fā)出的 gRPC 調(diào)用。 通過不安全的連接發(fā)送身份驗證標頭具有安全隱患,不應(yīng)在生產(chǎn)環(huán)境中執(zhí)行。 |
MaxRetryAttempts |
5 | 最大重試次數(shù)。 該值限制服務(wù)配置中指定的任何重試和 hedging 嘗試值。單獨設(shè)置該值不會啟用重試。 重試在服務(wù)配置中啟用,可以使用 ServiceConfig 來啟用。 null 值會刪除最大重試次數(shù)限制。 有關(guān)重試的詳細信息,請參閱故障處理與重試 |
MaxRetryBufferSize |
16 MB | 在重試或 hedging 調(diào)用時,可用于存儲發(fā)送的消息的最大緩沖區(qū)大小(以字節(jié)為單位)。 如果超出了緩沖區(qū)限制,則不會再進行重試,并且僅保留一個 hedging 調(diào)用,其他 hedging 調(diào)用將會取消。 此限制將應(yīng)用于通過通道進行的所有調(diào)用。 值 null 移除最大重試緩沖區(qū)大小限制。 |
MaxRetryBufferPerCallSize |
1 MB | 在重試或 hedging 調(diào)用時,可用于存儲發(fā)送的消息的最大緩沖區(qū)大小(以字節(jié)為單位)。 如果超出了緩沖區(qū)限制,則不會再進行重試,并且僅保留一個 hedging 調(diào)用,其他 hedging 調(diào)用將會取消。 此限制將應(yīng)用于一個調(diào)用。 值 null 移除每個調(diào)用的最大重試緩沖區(qū)大小限制。 |
ServiceConfig |
null |
gRPC 通道的服務(wù)配置。 服務(wù)配置可以用于配置 gRPC 重試。 |
下面的代碼:
-
設(shè)置通道上發(fā)送和接收的最大消息大小。
-
創(chuàng)建客戶端。
static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB MaxSendMessageSize = 2 * 1024 * 1024 // 2 MB }); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine("Greeting: " + reply.Message); }
?
請注意,未使用 GrpcChannelOptions
配置客戶端偵聽器。 相反,客戶端偵聽器是使用帶有通道的 Intercept
擴展方法配置的。 此擴展方法位于 Grpc.Core.Interceptors
命名空間中。
static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001"); var callInvoker = channel.Intercept(new LoggingInterceptor()); var client = new Greeter.GreeterClient(callInvoker); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine("Greeting: " + reply.Message); }
?
?
8. 身份驗證和授權(quán)
gRPC 可與
以下是使用 gRPC 和 ASP.NET Core 身份驗證的 Program.cs
的示例:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapGrpcService<GreeterService>();
?
備注
注冊 ASP.NET Core 身份驗證中間件的順序很重要。 始終在
UseRouting
之后和UseEndpoints
之前調(diào)用UseAuthentication
和UseAuthorization
。
應(yīng)用在調(diào)用期間使用的身份驗證機制需要進行配置。 身份驗證配置已添加到 Program.cs
中,并因應(yīng)用使用的身份驗證機制而異。
設(shè)置身份驗證后,可通過 ServerCallContext
使用 gRPC 服務(wù)方法訪問用戶。
?
public override Task<BuyTicketsResponse> BuyTickets( BuyTicketsRequest request, ServerCallContext context) { var user = context.GetHttpContext().User; // ... access data from ClaimsPrincipal ... }
?
?
持有者令牌身份驗證
客戶端可提供用于身份驗證的訪問令牌。 服務(wù)器驗證令牌并使用它來標識用戶。
在服務(wù)器上,使用
在 .NET gRPC 客戶端中,令牌可通過 Metadata
集合與調(diào)用一起發(fā)送。 Metadata
集合中的條目以 HTTP 標頭的形式與 gRPC 調(diào)用一起發(fā)送:
public bool DoAuthenticatedCall( Ticketer.TicketerClient client, string token) { var headers = new Metadata(); headers.Add("Authorization", $"Bearer {token}"); var request = new BuyTicketsRequest { Count = 1 }; var response = await client.BuyTicketsAsync(request, headers); return response.Success; }
?
在通道上配置 ChannelCredentials
是通過 gRPC 調(diào)用將令牌發(fā)送到服務(wù)的備用方法。 ChannelCredentials
可包含 CallCredentials
,這使得能夠自動設(shè)置 Metadata
。 CallCredentials
在每次進行 gRPC 調(diào)用時運行,因而無需在多個位置編寫代碼用于自行傳遞令牌。
備注
僅當通道通過 TLS 進行保護時,才應(yīng)用
CallCredentials
。 通過不安全的連接發(fā)送身份驗證標頭具有安全隱患,不應(yīng)在生產(chǎn)環(huán)境中執(zhí)行。 應(yīng)用可以配置通道以忽略此行為,并通過在通道上設(shè)置CallCredentials
始終使用UnsafeUseInsecureChannelCallCredentials
。
以下示例中的憑據(jù)將通道配置為隨每個 gRPC 調(diào)用發(fā)送令牌:
private static GrpcChannel CreateAuthenticatedChannel(string address) { var credentials = CallCredentials.FromInterceptor((context, metadata) => { if (!string.IsNullOrEmpty(_token)) { metadata.Add("Authorization", $"Bearer {_token}"); } return Task.CompletedTask; }); var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions { Credentials = ChannelCredentials.Create(new SslCredentials(), credentials) }); return channel; }
?
?
gRPC 客戶端工廠的持有者令牌
gRPC 客戶端工廠可以創(chuàng)建使用 AddCallCredentials
發(fā)送持有者令牌的客戶端。 此方法在
傳遞給 AddCallCredentials
的委托針對每個 gRPC 調(diào)用執(zhí)行:
builder.Services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("http://localhost:5001"); }) .AddCallCredentials(async (context, metadata) => { var serviceProvider = builder.Services.BuildServiceProvider(); var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>(); // 獲取客戶端的token var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token"); if (!string.IsNullOrEmpty(accessToken)) { metadata.Add("Authorization", $"Bearer {accessToken}"); } }).ConfigureChannel(o => o.UnsafeUseInsecureChannelCallCredentials = true);
?
依賴項注入 (DI) 可以與 AddCallCredentials
結(jié)合使用。 重載將 IServiceProvider
傳遞給委托,該委托可用于獲取
請考慮具有以下特征的應(yīng)用:
-
用于獲取持有者令牌的用戶定義的
ITokenProvider
。 在具有作用域生存期的 DI 中注冊ITokenProvider
。 -
gRPC 客戶端工廠配置為創(chuàng)建注入到 gRPC 服務(wù)和 Web API 控制器中的客戶端。
-
gRPC 調(diào)用應(yīng)使用
ITokenProvider
獲取持有者令牌。
public interface ITokenProvider { Task<string> GetTokenAsync(); } public class AppTokenProvider : ITokenProvider { private string _token; public async Task<string> GetTokenAsync() { if (_token == null) { // App code to resolve the token here. } return _token; } } builder.Services.AddScoped<ITokenProvider, AppTokenProvider>(); builder.Services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .AddCallCredentials(async (context, metadata, serviceProvider) => { var provider = serviceProvider.GetRequiredService<ITokenProvider>(); var token = await provider.GetTokenAsync(); metadata.Add("Authorization", $"Bearer {token}"); }));
?
前面的代碼:
-
定義
ITokenProvider
和AppTokenProvider
。 這些類型處理解析 gRPC 調(diào)用的身份驗證令牌。 -
在范圍內(nèi)的生存期內(nèi)向 DI 注冊
AppTokenProvider
類型。AppTokenProvider
緩存令牌,以便只需要范圍內(nèi)的第一個調(diào)用來計算它。 -
向客戶端工廠注冊
GreeterClient
類型。 -
為此客戶端配置
AddCallCredentials
。 每次發(fā)出調(diào)用并將ITokenProvider
返回的令牌添加到元數(shù)據(jù)時,都會執(zhí)行委托。
?
客戶端證書身份驗證
客戶端還可以提供用于身份驗證的客戶端證書。
備注
將服務(wù)器配置為接受客戶端證書。 有關(guān)在 Kestrel、IIS 和 Azure 中接受客戶端證書的信息,請參閱
在 .NET gRPC 客戶端中,客戶端證書已添加到 HttpClientHandler
中,后者之后用于創(chuàng)建 gRPC 客戶端:
public Ticketer.TicketerClient CreateClientWithCert( string baseAddress, X509Certificate2 certificate) { // Add client cert to the handler var handler = new HttpClientHandler(); handler.ClientCertificates.Add(certificate); // Create the gRPC channel var channel = GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions { HttpHandler = handler }); return new Ticketer.TicketerClient(channel); }
?
授權(quán)用戶訪問服務(wù)
默認情況下,未經(jīng)身份驗證的用戶可以調(diào)用服務(wù)中的所有方法。 若要要求進行身份驗證,請將 [Authorize]
特性應(yīng)用于服務(wù):
[Authorize] public class TicketerService : Ticketer.TicketerBase { }
?
可使用 [Authorize]
特性的構(gòu)造函數(shù)參數(shù)和屬性將訪問權(quán)限僅限于匹配特定
[Authorize("MyAuthorizationPolicy")] public class TicketerService : Ticketer.TicketerBase { }
?
各個服務(wù)方法也可以應(yīng)用 [Authorize]
特性。 如果當前用戶與同時應(yīng)用于方法和類的策略不匹配,則會向調(diào)用方返回錯誤:
[Authorize] public class TicketerService : Ticketer.TicketerBase { public override Task<AvailableTicketsResponse> GetAvailableTickets( Empty request, ServerCallContext context) { // ... buy tickets for the current user ... } [Authorize("Administrators")] public override Task<BuyTicketsResponse> RefundTickets( BuyTicketsRequest request, ServerCallContext context) { // ... refund tickets (something only Administrators can do) .. } }
?
2. Feign 組件
github 文檔:
需要安裝包:
SummerBoot 2.0.2
SummerBoot 是將SpringBoot的先進理念與C#的簡潔優(yōu)雅合二為一,聲明式編程,專注于”做什么”而不是”如何去做”。在更高層面寫代碼,更關(guān)心的是目標,而不是底層算法實現(xiàn)的過程,SummerBoot,致力于打造一個人性化框架,讓.net開發(fā)變得更簡單優(yōu)雅。
SummerBoot 框架中內(nèi)容較多,我們重點使用框架中提供的Feign組件。
個人覺得比gRPC 使用更簡單一點,個人還是比較推薦
?
1. Feign 簡介
Feign 是一種聲明式服務(wù)調(diào)用組件 , 我們只需要聲明一個接口并通過注解進行簡單的配置(類似于 Dao 接口上面的 Mapper 注解一樣)即可實現(xiàn)對 HTTP 接口的綁定。 通過 Feign,我們可以像調(diào)用本地方法一樣來調(diào)用遠程服務(wù),而完全感覺不到這是在進行遠程調(diào)用。
它(Feign)能做什么?
-
自定義攔截器(AOP)
-
封裝了Http遠程調(diào)用過程
-
微服務(wù)接入Nacos
-
可結(jié)合Polly做降級處理
-
結(jié)合JWT做授權(quán)與鑒權(quán)
?
請求方式
方式 | 特性(注解) |
---|---|
HttpGet | [GetMapping] |
HttpPut | [PutMapping] |
HttpDelete | [DeleteMapping] |
HttpPost | [PostMapping] |
?
2. 如何使用
封裝Http調(diào)用
feign底層基于httpClient。
1.注冊服務(wù)
builder.Services.AddSummerBoot();
builder.Services.AddSummerBootFeign();
?
2.定義接口
[FeignClient(Url = "http://localhost:5061")] public interface UserClient { [GetMapping("/User/GetUserList")] Task<List<UserInfo>> GetUserList(); }
?
提示
Feign 組件會由
FeignProxyBuilder
類自動為接口生成實現(xiàn)代理類,注意,此版本中接口必須定義為Task<> 異步方法。
定義一個接口,并且在接口上添加FeignClient注解,F(xiàn)eignClient注解里可以自定義http接口url的公共部分-url(整個接口請求的url由FeignClient里的url加上方法里的path組成),是否忽略遠程接口的https證書校驗-IsIgnoreHttpsCertificateValidate,接口超時時間-Timeout(單位s),自定義攔截器-InterceptorType。
[FeignClient( Url = "http://localhost:5001/home" , IsIgnoreHttpsCertificateValidate = true , InterceptorType = typeof(MyRequestInterceptor) ,Timeout = 100) ] public interface ITestFeign { [GetMapping("/query")] Task<Test> TestQuery([Query] Test tt); [GetMapping("/addTest")] Task<Test> AddTest([Body] TestBo bo); }
?
[Body]
請求體中支持Json與Form兩種格式,默認為Json, 如果需要設(shè)置為Form提交,則修改為:
[PostMapping("/User/AddUser")] Task<List<UserInfo>> AddUser([Body(BodySerializationKind.Form)] UserInfo bo);
?
3.服務(wù)調(diào)用
[Route("[controller]/[action]")] [ApiController] public class RpcController:ControllerBase { private readonly IUserClient _userClient; public RpcController( IUserClient userClient) { _userClient = userClient; } [HttpGet] public async Task<List<UserInfo>> GetUserList() { return await _userClient.GetUserList(); } }
?
?
讀取配置
同時,url和path可以通過讀取配置獲取,配置項通過${}包裹,配置的json如下:
{ "configurationTest": { "url": "http://localhost:5001/home", "path": "/query" } }
?
接口如下:
[FeignClient(Url = "${configurationTest:url}")] public interface ITestFeignWithConfiguration { [GetMapping("${configurationTest:path}")] Task<Test> TestQuery([Query] Test tt); }
?
3. 設(shè)置請求頭(header)
接口上可以選擇添加Headers注解,代表這個接口下所有http請求都帶上注解里的請求頭。Headers的參數(shù)為變長的string類型的參數(shù),同時Headers也可以添加在方法上,代表該方法調(diào)用的時候,會加該請求頭,接口上的Headers參數(shù)可與方法上的Headers參數(shù)互相疊加,同時headers里可以使用變量,變量的占位符為{{}},如
[FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(MyRequestInterceptor),Timeout = 100)] [Headers("a:a","b:b")] public interface ITestFeign { [GetMapping("/testGet")] Task<Test> TestAsync(); [GetMapping("/testGetWithHeaders")] [Headers("c:c")] Task<Test> TestWithHeadersAsync(); //header替換 [Headers("a:{{methodName}}")] [PostMapping("/abc")] Task<Test> TestHeaderAsync(string methodName); } await TestFeign.TestAsync() >>> get, http://localhost:5001/home/testGet,header為 "a:a" 和 "b:b" await TestFeign.TestWithHeadersAsync() >>> get, http://localhost:5001/home/testGetWithHeaders,header為 "a:a" ,"b:b"和 "c:c" await TestFeign.TestHeaderAsync("abc"); >>> post, http://localhost:5001/home/abc,同時請求頭為 "a:abc" [Headers("a:a","b:b")] 冒號左邊是鍵名稱,右邊是鍵值
?
?
4. 自定義攔截器
自定義攔截器對接口下的所有方法均生效,攔截器的應(yīng)用場景主要是在請求前做一些操作,比如請求第三方業(yè)務(wù)接口前,需要先登錄第三方系統(tǒng),那么就可以在攔截器里先請求第三方登錄接口,獲取到憑證以后,放到header里,攔截器需要實現(xiàn)IRequestInterceptor接口,例子如下
//先定義一個用來登錄的loginFeign客戶端 [FeignClient(Url = "http://localhost:5001/login", IsIgnoreHttpsCertificateValidate = true,Timeout = 100)] public interface ILoginFeign { [PostMapping("/login")] Task<LoginResultDto> LoginAsync([Body()] LoginDto loginDto ); } //接著自定義登錄攔截器 public class LoginInterceptor : IRequestInterceptor { private readonly ILoginFeign loginFeign; private readonly IConfiguration configuration; public LoginInterceptor(ILoginFeign loginFeign, IConfiguration configuration) { this.loginFeign = loginFeign; this.configuration = configuration; } public async Task ApplyAsync(RequestTemplate requestTemplate) { var username = configuration.GetSection("username").Value; var password = configuration.GetSection("password").Value; var loginResultDto = await this.loginFeign.LoginAsync(new LoginDto(){Name = username,Password = password}); if (loginResultDto != null) { requestTemplate.Headers.Add("Authorization", new List<string>() { "Bearer "+loginResultDto.Token }); } await Task.CompletedTask; } } //定義訪問業(yè)務(wù)接口的testFegn客戶端,在客戶端上定義攔截器為loginInterceptor [FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(LoginInterceptor),Timeout = 100)] public interface ITestFeign { [GetMapping("/testGet")] Task<Test> TestAsync(); } await TestFeign.TestAsync(); >>> get to http://localhost:5001/home/testGet,header為 "Authorization:Bearer abc"
?
忽略攔截器
有時候我們接口中的某些方法,是不需要攔截器的,那么就可以在方法上添加注解IgnoreInterceptor,那么該方法發(fā)起的請求,就會忽略攔截器,如
//定義訪問業(yè)務(wù)接口的testFegn客戶端,在客戶端上定義攔截器為loginInterceptor [FeignClient(Url = "http://localhost:5001/home", IsIgnoreHttpsCertificateValidate = true, InterceptorType = typeof(LoginInterceptor),Timeout = 100)] public interface ITestFeign { [GetMapping("/testGet")] [IgnoreInterceptor] Task<Test> TestAsync(); } await TestFeign.TestAsync(); >>> get to http://localhost:5001/home/testGet,沒有header
?
?
5. 微服務(wù)-接入nacos
目前Feign組件只支持Nacos。
1.配置文件里添加nacos配置
注意:這里的nacos 配置與原生的nacos配置不太一樣,SummberBoot會集成Nacos服務(wù)注冊,所以若考慮接入SummberBoot中的Nacos,則原生的Nacos服務(wù)注入則應(yīng)去掉。
"nacos": { //--------使用nacos則serviceAddress和namespaceId必填------ //nacos服務(wù)地址,如http://172.16.189.242:8848 "serviceAddress": "http://172.16.189.242:8848/", //命名空間id,如832e754e-e845-47db-8acc-46ae3819b638或者public "namespaceId": "dfd8de72-e5ec-4595-91d4-49382f500edf", //--------如果只是訪問nacos中的微服務(wù),則僅配置lbStrategy即可,defaultNacosGroupName和defaultNacosNamespaceId選填------ //客戶端負載均衡算法,一個服務(wù)下有多個實例,lbStrategy用來挑選服務(wù)下的實例,默認為Random(隨機),也可以選擇WeightRandom(根據(jù)服務(wù)權(quán)重加權(quán)后再隨機) "lbStrategy": "Random", //defaultNacosGroupName,選填,為FeignClient注解中NacosGroupName的默認值,為空則默認為DEFAULT_GROUP "defaultNacosGroupName": "", //defaultNacosNamespaceId,選填,為FeignClient注解中NacosNamespaceId的默認值,為空則默認為public "defaultNacosNamespaceId": "", //--------如果需要使用nacos配置中心,則ConfigurationOption必填,允許監(jiān)聽多個配置------ "configurationOption": [ { "namespaceId": "f3dfa56a-a72c-4035-9612-1f9a8ca6f1d2", //配置的分組 "groupName": "DEFAULT_GROUP", //配置的dataId, "dataId": "def" }, { "namespaceId": "public", //配置的分組 "groupName": "DEFAULT_GROUP", //配置的dataId, "dataId": "abc" } ], //-------如果是要將本應(yīng)用注冊為服務(wù)實例,則全部參數(shù)均需配置-------------- //是否要把應(yīng)用注冊為服務(wù)實例 "registerInstance": true, //要注冊的服務(wù)名 "serviceName": "test", //服務(wù)的分組名 "groupName": "DEFAULT_GROUP", //權(quán)重,一個服務(wù)下有多個實例,權(quán)重越高,訪問到該實例的概率越大,比如有些實例所在的服務(wù)器配置高,那么權(quán)重就可以大一些,多引流到該實例,與上面的參數(shù)lbStrategy設(shè)置為WeightRandom搭配使用 "weight": 1, //本應(yīng)用對外的網(wǎng)絡(luò)協(xié)議,http或https "protocol": "http", //本應(yīng)用對外的端口號,比如5000 "port": 5000 }
?
2.接入nacos服務(wù)中心
// 拋棄原生Nacos注冊 // builder.Services.AddNacosAspNet(builder.Configuration); builder.Services.AddSummerBoot(); builder.Services.AddSummerBootFeign(p => { p.AddNacos(builder.Configuration); // Feign組件接入Nacos });
?
3.定義接口
namespace NetCloud.RpcClient { [FeignClient( ServiceName = "NetCloud.Nacos.UserService" , NacosNamespaceId = "NetCloud" , NacosGroupName = "DEFAULT_GROUP" , MicroServiceMode = true ) ] public interface IUserServiceClient { [GetMapping("/User/GetUserList")] Task<List<UserInfo>> GetUserList(); } }
?
同時ServiceName,NacosGroupName,NacosNamespaceId也支持從配置文件中讀取,如
{ "ServiceName": "NetCloud.Nacos.UserService", "NacosGroupName": "DEFAULT_GROUP", "NacosNamespaceId": "NetCloud" } [FeignClient( ServiceName = "${ServiceName}", MicroServiceMode = true,NacosGroupName = "${NacosGroupName}", NacosNamespaceId = "${NacosNamespaceId}")] public interface IFeignService { [GetMapping("/home/index")] Task<string> TestGet(); }
?
視頻配套鏈接:課程簡介 (cctalk.com)
?
?
到了這里,關(guān)于第四章 RPC 調(diào)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!