国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

第四章 RPC 調(diào)用

這篇具有很好參考價值的文章主要介紹了第四章 RPC 調(diào)用。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

通過以上案例我們發(fā)現(xiàn),Http請求調(diào)用服務(wù)實例屬實過于麻煩。其實對于請求同一個服務(wù),很多步驟都是相同的,例如:服務(wù)名,地址,httpClient 創(chuàng)建步驟等。

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)用棧,可以幫你傳輸任何能量,使你想唱就唱。

    主要功能點包括:

    1. 分布式RPC,目前支持Tcp和Http(類REST風格)雙通道(見Demo:TcpRpcTest和HttpRpcTest): 可以多個RpcServer端和多個RpcClient端,其中client通過HashingAlgorithm根據(jù)Key計算出server。

    2. 分布式緩存系統(tǒng)(Memcached),包括MemcachedServer和MemcachedClient(見Demo:MemcachedTest): 可以多個MemcachedServer端和多個MemcachedClient端,其中client通過HashingAlgorithm根據(jù)Key計算出server。

    3. 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 是一種與語言無關(guān)的高性能遠程過程調(diào)用 (RPC) 框架。

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

?

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 類型是由生成進程自動生成的。 工具包 Grpc.Tools 基于 greet.proto 文件生成以下文件:

  • 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ā)證書不受信任。 要解決此問題,請參閱使用不受信任/無效的證書調(diào)用 gRPC 服務(wù)。

?

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 工具支持

需要工具包 Grpc.Tools 才能從 .proto 文件生成 C# 資產(chǎn)。 生成的資產(chǎn)(文件):

  • 在每次生成項目時按需生成。

  • 不會添加到項目中或是簽入到源代碼管理中。

  • 是包含在 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。 此約束包括 stringByteString,它們都屬于 C# 類。 string 默認為空字符串值,ByteString 默認為空字節(jié)值。 嘗試將它們設(shè)置為 null 會引發(fā)錯誤。

日期和時間

本機標量類型不提供與 .NET 的 DateTimeOffset、DateTimeTimeSpan 等效的日期和時間值。 可使用 Protobuf 的一些“已知類型”擴展來指定這些類型。 這些擴展為受支持平臺中的復雜字段類型提供代碼生成和運行時支持。

下表顯示日期和時間類型:

.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 命名空間中的 TimestampDuration 類。 這些類提供在 DateTimeOffset、DateTimeTimeSpan 之間進行轉(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? 屬性。 引用類型屬性(如 stringByteString )保持不變,但可以向它們分配 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.SpanByteString.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)了 IList。 因此你可使用 LINQ 查詢,或者將其轉(zhuǎn)換為數(shù)組或列表。 RepeatedField<T> 屬性沒有公共 setter。 項應(yīng)添加到現(xiàn)有集合中。

var person = new Person();

person.Roles.Add("user");

var roles = new [] { "admin", "manager" };
person.Roles.Add(roles);

var list = roles.ToList();

?

?

字典

.NET IDictionary 類型在 Protobuf 中使用 map<key_type, value_type> 表示。

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ā) ServerCallContext.CancellationToken。 應(yīng)在服務(wù)器上通過異步方法使用 CancellationToken 標記,以實現(xiàn)以下目的:

  • 所有異步工作都與流式處理調(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)交互的一種安全方法是將生成方-使用者模式與 System.Threading.Channels 配合使用。

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.Net.Client NuGet 包提供了 .NET gRPC 客戶端庫。 本文檔介紹如何執(zhí)行以下操作:

  • 配置 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)用中,客戶端:

  1. 通過調(diào)用 EchoClient.Echo 啟動新的雙向流式調(diào)用。

  2. 使用 ResponseStream.ReadAllAsync() 創(chuàng)建用于從服務(wù)中讀取消息的后臺任務(wù)。

  3. 使用 RequestStream.WriteAsync 將消息發(fā)送到服務(wù)器。

  4. 使用 RequestStream.CompleteAsync() 通知服務(wù)器它已發(fā)送消息。

  5. 等待直到后臺任務(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)容)的替代方法。 Grpc.Net.ClientFactory NuGet 包中提供了工廠集成。

工廠具有以下優(yōu)勢:

  • 提供了用于配置邏輯 gRPC 客戶端實例的中心位置。

  • 可管理基礎(chǔ) HttpClientMessageHandler 的生存期。

  • 在 ASP.NET Core gRPC 服務(wù)中自動傳播截止時間和取消。

?

注冊 gRPC 客戶端

若要注冊 gRPC 客戶端,可在 Program.cs 中的應(yīng)用入口點處的 WebApplicationBuilder 的實例中使用通用的 AddGrpcClient 擴展方法,并指定 gRPC 類型化客戶端類和服務(wù)地址:

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)用, 此方法在 Grpc.Net.ClientFactory 版本 2.46.0 以上版本中可用。

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ā) ServerCallContext.CancellationToken。 盡管中止了 HTTP 請求,gRPC 調(diào)用仍將繼續(xù)在服務(wù)器上運行,直到方法完成。 將取消令牌傳遞給異步方法,使其隨調(diào)用一同被取消,這非常重要。 例如,向異步數(shù)據(jù)庫查詢和 HTTP 請求傳遞取消令牌。 傳遞取消令牌讓取消的調(diào)用可以在服務(wù)器上快速完成,并為其他調(diào)用釋放資源。

配置 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)傳播截止時間。 例如:

  1. 客戶端應(yīng)用調(diào)用帶有截止時間的 FrontendService.GetUser。

  2. FrontendService 調(diào)用 UserService.GetUser。 客戶端指定的截止時間應(yīng)隨新的 gRPC 調(diào)用進行指定。

  3. 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(需要安裝包: Grpc.AspNetCore.Server.ClientFactory ):

  • 自動將截止時間和取消令牌傳播到子調(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.Net.Client 2.36.0 或更高版本。

?

暫時性故障處理

暫時性故障可能會中斷 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)代碼的更多信息,請參閱狀態(tài)代碼及其在 gRPC 中的用法。 至少需要提供一個可重試的狀態(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ū)大小。 MaxRetryBufferSizeMaxRetryBufferPerCallSizegPRC 配置這一小節(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 或更高版本。

  • Grpc.Net.Client 版本 2.45.0 或更高版本。

?

配置客戶端負載均衡

客戶端負載均衡是在創(chuàng)建通道時配置的。 使用負載均衡時需要考慮兩個組件:

  • 解析程序,用于解析通道的地址。 解析程序支持從外部源獲取地址。 這也被稱為服務(wù)發(fā)現(xiàn)。

  • 負載均衡器,用于創(chuàng)建連接,并選取 gRPC 調(diào)用將使用的地址。

解析程序和負載均衡器的內(nèi)置實現(xiàn)包含在 Grpc.Net.Client 中。 也可以通過編寫自定義解析程序和負載均衡器來擴展負載均衡。

地址、連接和其他負載均衡狀態(tài)存儲在 GrpcChannel 實例中。 在進行 gRPC 調(diào)用時,必須重用通道,以使負載均衡正常工作。

?

配置解析程序

解析程序是使用創(chuàng)建通道時所用的地址配置的。

Scheme 類型 說明
dns DnsResolverFactory 通過查詢 DNS 地址記錄的主機名來解析地址。
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:80localhost:81。

  • 負載均衡器使用 localhost:80localhost:81 創(chuàng)建連接并進行 gRPC 調(diào)用。

DnsResolverFactory

DnsResolverFactory 創(chuàng)建一個解析程序,旨在從外部源獲取地址。 DNS 解析通常用于對具有 Kubernetes 無外設(shè)服務(wù)的 Pod 實例進行負載均衡。

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 中,重啟的 Pod 會觸發(fā) DNS 解析程序刷新并獲取 Pod 的新地址。

默認情況下,如果連接中斷,則會刷新 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:80localhost:81。

  • 使用依賴項注入 (DI) 來注冊工廠。

  • 使用以下內(nèi)容配置已創(chuàng)建的通道:

    • 地址 static:///my-example-host。 static 方案映射到靜態(tài)解析程序。

    • 使用 DI 服務(wù)提供程序設(shè)置 GrpcChannelOptions.ServiceProvider。

本示例為 DI 創(chuàng)建了一個新的 ServiceCollection。 假設(shè)一個應(yīng)用已設(shè)置 DI,比如一個 ASP.NET Core 網(wǎng)站。 在這種情況下,類型應(yīng)被注冊到現(xiàn)有的 DI 實例。 GrpcChannelOptions.ServiceProvider 是通過從 DI 獲取 IServiceProvider 來配置的。

?

配置負載均衡器

負載均衡器是使用 ServiceConfig.LoadBalancingConfigs 集合在 service config 中指定的。 兩個負載均衡器都是內(nèi)置的,映射到負載均衡器配置名稱:

名稱 類型 說明
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ù)

通道必須知道是否使用傳輸安全性發(fā)送了 gRPC 調(diào)用。 httphttps 不再是地址的一部分,方案現(xiàn)在指定一個解析程序,使得在使用負載均衡時必須對通道選項配置 Credentials

  • 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 是一種高性能遠程過程調(diào)用 (RPC) 框架。 gRPC 使用 HTTP/2、流式傳輸、Protobuf 和消息協(xié)定來創(chuàng)建高性能的實時服務(wù)。

gRPC 有一個限制,即不是所有平臺都可以使用它。 瀏覽器并不完全支持 HTTP/2,這使得 REST API 和 JSON 成為將數(shù)據(jù)引入瀏覽器應(yīng)用的主要方式。 盡管 gRPC 帶來了很多好處,REST API 和 JSON 在新式應(yīng)用中仍發(fā)揮著重要作用。 構(gòu)建 gRPC 和 JSON Web API 給應(yīng)用開發(fā)增加了不必要的開銷。

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ù)。

使用步驟

  1. 將包引用添加到 Microsoft.AspNetCore.Grpc.JsonTranscoding。

  2. 通過添加 AddJsonTranscoding,在服務(wù)器啟動代碼中注冊轉(zhuǎn)碼:在 Program.cs 文件中,將 builder.Services.AddGrpc(); 更改為 builder.Services.AddGrpc().AddJsonTranscoding();。

  3. 在包含 .csproj 文件的項目目錄中創(chuàng)建目錄結(jié)構(gòu) /google/api。

  4. google/api/http.protogoogle/api/annotations.proto 文件添加到 /google/api 目錄中。

  5. 用 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 文件導入。 google/api/http.protogoogle/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

OpenAPI (Swagger) 是一個與語言無關(guān)的規(guī)范,用于描述 REST API。 gRPC JSON 轉(zhuǎn)碼支持從轉(zhuǎn)碼的 RESTful API 生成 OpenAPI。 Microsoft.AspNetCore.Grpc.Swagger 包:

  • 將 gRPC JSON 轉(zhuǎn)碼與 Swashbuckle 集成。

  • .NET 7 中的實驗性是允許我們探索提供 OpenAPI 支持的最佳方式。

若要使用 gRPC JSON 轉(zhuǎn)碼啟用 OpenAPI,請執(zhí)行以下操作:

  1. 將包引用添加到 Microsoft.AspNetCore.Grpc.Swagger。 版本必須為 0.3.0-xxx 或更高版本。

  2. 在啟動時配置 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í)行以下操作:

  1. 使用 <GenerateDocumentationFile>true</GenerateDocumentationFile> 啟用服務(wù)器項目中的 XML 文檔文件。

  2. 配置 AddSwaggerGen 以讀取生成的 XML 文件。 將 XML 文件路徑傳遞到 IncludeXmlCommentsIncludeGrpcXmlComments,如以下示例所示:

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 頁面:

第四章 RPC 調(diào)用

?

?

7. gRPC 配置

配置服務(wù)選項

gRPC 服務(wù)在 Startup.cs 中使用 AddGrpc 進行配置。 配置選項位于 Grpc.AspNetCore.Server 包中。

下表描述了用于配置 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ù),并從依賴關(guān)系注入 (DI) 解析參數(shù)。 還可以向 DI 注冊偵聽器類型,以重寫其創(chuàng)建方式及其生存期。 與 ASP.NET Core 中間件相比,偵聽器會提供類似的功能。 有關(guān)詳細信息,請參閱 gRPC 偵聽器與中間件。
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.Net.Client 包中。

下表描述了用于配置 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 且指定了 HttpMessageHandlerHttpClient,則在處置 GrpcChannel 時,將分別處置 HttpHandlerHttpClient
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ā) OperationCanceledException。
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 可與 ASP.NET Core 身份驗證配合使用,將用戶與每個調(diào)用關(guān)聯(lián)。

以下是使用 gRPC 和 ASP.NET Core 身份驗證的 Program.cs 的示例:

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapGrpcService<GreeterService>();

?

備注

注冊 ASP.NET Core 身份驗證中間件的順序很重要。 始終在 UseRouting 之后和 UseEndpoints 之前調(diào)用 UseAuthenticationUseAuthorization

應(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ù)器上,使用 JWT 持有者中間件配置持有者令牌身份驗證。

在 .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è)置 MetadataCallCredentials 在每次進行 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ā)送持有者令牌的客戶端。 此方法在 Grpc.Net.ClientFactory 版本 2.46.0 或更高版本中可用。

傳遞給 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 傳遞給委托,該委托可用于獲取使用范圍內(nèi)服務(wù)和暫時性服務(wù)從 DI 構(gòu)建的服務(wù)

請考慮具有以下特征的應(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}");
    }));

?

前面的代碼:

  • 定義 ITokenProviderAppTokenProvider。 這些類型處理解析 gRPC 調(diào)用的身份驗證令牌。

  • 在范圍內(nèi)的生存期內(nèi)向 DI 注冊 AppTokenProvider 類型。 AppTokenProvider 緩存令牌,以便只需要范圍內(nèi)的第一個調(diào)用來計算它。

  • 向客戶端工廠注冊 GreeterClient 類型。

  • 為此客戶端配置 AddCallCredentials。 每次發(fā)出調(diào)用并將 ITokenProvider 返回的令牌添加到元數(shù)據(jù)時,都會執(zhí)行委托。

?

客戶端證書身份驗證

客戶端還可以提供用于身份驗證的客戶端證書。 證書身份驗證在 TLS 級別發(fā)生,遠在到達 ASP.NET Core 之前。 當請求進入 ASP.NET Core 時,可借助客戶端證書身份驗證包將證書解析為 ClaimsPrincipal。

備注

將服務(wù)器配置為接受客戶端證書。 有關(guān)在 Kestrel、IIS 和 Azure 中接受客戶端證書的信息,請參閱在 ASP.NET Core 中配置證書身份驗證。

在 .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)限僅限于匹配特定授權(quán)策略的用戶。 例如,如果有一個名為 MyAuthorizationPolicy 的自定義授權(quán)策略,請使用以下代碼確保僅匹配該策略的用戶才能訪問服務(wù):

[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/README.zh-cn.md at master · TripleView/SummerBoot · 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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 第四章 路由基礎(chǔ)

    第四章 路由基礎(chǔ)

    目錄 4.1 路由器概述 4.1.1 路由器定義 4.1.2 路由器工作原理 4.1.3 路由表的生成方式 (1)直連路由 (2)靜態(tài)路由 (3)動態(tài)路由 4.1.4 路由器的接口 (1)配置接口 (2)局域網(wǎng)接口 (3)廣域網(wǎng)接口 4.1.5 路由器的硬件連接 (1)局域網(wǎng)線纜:雙絞線 (2)廣域網(wǎng)接口 (3)配置專

    2024年02月08日
    瀏覽(29)
  • 第四章 磁盤設(shè)備

    第四章 磁盤設(shè)備 一、 關(guān)于掛裝的基本常識 ?與 DOS/Windows 采用驅(qū)動器標識符(A:、B:、C:)使用磁 盤設(shè)備的方法不同。Linux 采用單根目錄樹管理全部文件系 統(tǒng)。磁盤設(shè)備必須掛載到系統(tǒng)目錄樹上才能使用。 (Linux 啟動過程已完成對/、/ boot 和/swap 三個分區(qū)的掛裝) ?所謂掛

    2024年02月03日
    瀏覽(28)
  • 第四章-邊界安全

    第四章-邊界安全

    1)什么是防火墻 墻,始于防,忠于守。從古至今,墻予人以安全之意。 防御外網(wǎng)對內(nèi)網(wǎng)的入侵 防火墻是一種 網(wǎng)絡(luò)安全設(shè)備或系統(tǒng) ,用于監(jiān)控和控制網(wǎng)絡(luò)流量,防止未經(jīng)授權(quán)的訪問和攻擊。防火墻可以根據(jù)預定的規(guī)則和策略,過濾入站和出站數(shù)據(jù)包,保護網(wǎng)絡(luò)的安全性和完

    2024年01月19日
    瀏覽(40)
  • 第四章 數(shù)組

    第四章 數(shù)組

    可以多看幾遍視頻 把上課的代碼,自己加加注釋,在自己寫之前,可以畫一個流程圖 照著流程圖把代碼自己實現(xiàn)一遍 不要懷疑自己,不要遇到困難就覺得自己不行,遇到困難就解決困難,編程初學者都是這樣一步一步走過來的。 在指針階段會演示整型、浮點型、字符型傳遞

    2024年02月12日
    瀏覽(33)
  • Linux第四章

    Linux第四章

    ctrl+c :強制停止(Linux某些程序的運行;命令輸入錯誤) ctrl+d :退出或登出(退出賬戶的登錄;或者退出某些特定程序的專屬頁面) history :查看歷史輸入過的命令(!命令前綴,自動執(zhí)行上一次匹配前綴的命令,不能搜索太久的) ctrl+r :輸入內(nèi)容去匹配歷史命令(如果搜索到的內(nèi)

    2024年02月01日
    瀏覽(26)
  • 第四章,登錄注冊

    目錄 4.1 添加注冊頁面 4.2 注冊表單驗證 4.3提交注冊信息 4.4 完善注冊功能

    2024年02月12日
    瀏覽(27)
  • 第四章 搜索功能

    指定返回的字段 在ES中,通過_source子句可以設(shè)定返回結(jié)果的字段。_source指向一個JSON數(shù)組,數(shù)組中的元素是希望返回的字段名稱。 例如,通過source指定查詢字段 結(jié)果計數(shù) 給前端傳遞搜索匹配結(jié)果的文檔條數(shù),即需要對搜索結(jié)果進行計數(shù)。ES提供了_count API功能,在該API中,用

    2023年04月08日
    瀏覽(35)
  • 第四章——數(shù)學知識1

    第四章——數(shù)學知識1

    質(zhì)數(shù):在大于1的整數(shù)中,如果只包含1和本身這倆個約束,就被叫質(zhì)數(shù)或素數(shù)。 質(zhì)數(shù)的判定——試除法:如果d能整除n,則n/d再除n,結(jié)果是一個整數(shù)。 d≤n/d。 質(zhì)因數(shù):一個正整數(shù)的倆個因數(shù)都是質(zhì)數(shù) 分解質(zhì)因數(shù)——試除法: 從小到大枚舉所有的質(zhì)因數(shù),這里我們要的是質(zhì)

    2023年04月26日
    瀏覽(27)
  • 第四章 Text

    第四章 Text

    在本章中,您將學習如何在頁面上繪制文本。 繪圖文本是 PDF 圖形中最復雜的部分,但它也是幫助 PDF 擊敗競爭對手成為當今國際標準的原因。 當其他原始播放器將文本轉(zhuǎn)換為光柵圖像或矢量路徑(以保持視覺完整性)時,PDF 的發(fā)明者知道用戶需要可以搜索和復制的文本,而

    2024年02月06日
    瀏覽(33)
  • 四,Eureka 第四章

    四,Eureka 第四章

    ? ? ? ?2.3.4修改主啟動類 標注為Eureka客戶端 ? ? ? ? ? springcloud-eureka-sever-7001 springcloud-eureka-sever-7001? ?springcloud-eureka-sever003 ? ? ? ? ? 5.25編寫PaymentMapper接口? ?5. ? ?

    2024年02月15日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包