利用 .NET 用戶端呼叫 gRPC 服務

Grpc.Net.Client NuGet 套件中有提供 .NET gRPC 用戶端程式庫。 本文件說明如何:

  • 設定 gRPC 用戶端以呼叫 gRPC 服務。
  • 對一元、伺服器串流、用戶端串流和雙向串流方法進行 gRPC 呼叫。

設定 gRPC 用戶端

gRPC 用戶端是.proto 檔案產生的具體用戶端類型。 具體 gRPC 用戶端具有可轉譯為 .proto 檔案中 gRPC 服務的方法。 例如,稱為 Greeter 的服務會產生 GreeterClient 類型,其具有呼叫服務的方法。

從通道建立 gRPC 用戶端。 首先,使用 GrpcChannel.ForAddress 來建立通道,然後使用通道來建立 gRPC 用戶端:

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greet.GreeterClient(channel);

通道代表 gRPC 服務的長期連線。 建立通道時,會使用與呼叫服務相關的選項進行設定。 例如,用來進行呼叫的 HttpClient、傳送和接收訊息大小上限,以及在 GrpcChannelOptions 上指定記錄,並搭配 GrpcChannel.ForAddress 使用。 如需選項的完整清單,請參閱用戶端組態選項

var channel = GrpcChannel.ForAddress("https://localhost:5001");

var greeterClient = new Greet.GreeterClient(channel);
var counterClient = new Count.CounterClient(channel);

// Use clients to call gRPC services

設定 TLS

gRPC 用戶端必須使用與所呼叫服務相同的連線層級安全性。 gRPC 用戶端傳輸層安全性 (TLS) 會在建立 gRPC 通道時設定。 gRPC 用戶端在呼叫服務時擲回錯誤,而通道和服務的連線層級安全性不相符。

若要將 gRPC 通道設定為使用 TLS,請確定伺服器位址以 https 開頭。 例如,GrpcChannel.ForAddress("https://localhost:5001") 使用 HTTPS 通訊協定。 gRPC 通道會自動交涉受 TLS 保護的連線,並使用安全連線進行 gRPC 呼叫。

提示

gRPC 支援透過 TLS 進行用戶端憑證驗證。 如需使用 gRPC 通道設定用戶端憑證的資訊,請參閱 gRPC 中適用於 ASP.NET Core 的驗證和授權

若要呼叫不安全的 gRPC 服務,請確保伺服器位址以 http 開頭。 例如,GrpcChannel.ForAddress("http://localhost:5000") 使用 HTTP 通訊協定。 在 .NET Core 3.1 中,需要其他的設定,才能使用 .NET 用戶端呼叫不安全的 gRPC 服務

用戶端效能

通道和用戶端效能和使用方式:

  • 建立通道可能是昂貴的作業。 重複使用 gRPC 呼叫的通道可提供效能優勢。
  • 通道會管理伺服器的連線。 如果連線已關閉或遺失,通道會在下次進行 gRPC 呼叫時自動重新連線。
  • gRPC 用戶端是使用通道來建立。 gRPC 用戶端是輕量型物件,不需要快取或重複使用。
  • 您可以從通道建立多個 gRPC 用戶端,包括不同類型的用戶端。
  • 從通道建立的通道和用戶端可以安全地供多個執行緒使用。
  • 從通道建立的用戶端可以進行多個同時呼叫。

GrpcChannel.ForAddress 不是建立 gRPC 用戶端的唯一選項。 如果從 ASP.NET Core 應用程式呼叫 gRPC 服務,請考慮 gRPC 用戶端處理站整合。 gRPC 與 HttpClientFactory 整合提供建立 gRPC 用戶端的集中式替代方案。

注意

Xamarin 目前不支援使用 Grpc.Net.Client 透過 HTTP/2 呼叫 gRPC。 我們正努力在未來的 Xamarin 版本中改善 HTTP/2 支援。 Grpc.CoregRPC-Web 是現今可行的替代方案。

進行 gRPC 呼叫

gRPC 呼叫是藉由在用戶端上呼叫方法來起始。 gRPC 用戶端將處理訊息序列化並將 gRPC 呼叫定址到正確的服務。

gRPC 有不同類型的方法。 如何使用用戶端來進行 gRPC 呼叫,取決於呼叫的方法類型。 gRPC 方法類型如下:

  • 一元
  • 伺服器串流
  • 用戶端串流
  • 雙向串流

一元呼叫

一元呼叫會從傳送要求訊息的用戶端開始。 當服務完成時,會傳回回應訊息。

var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World

.proto 檔案中的每個一元服務方法都會在具體的 gRPC 用戶端類型上產生兩個 .NET 方法,以便呼叫方法:非同步方法和封鎖方法。 例如,在 GreeterClient 上有兩種呼叫 SayHello 的方式:

  • GreeterClient.SayHelloAsync - 以非同步方式呼叫 Greeter.SayHello 服務。 可以等候。
  • GreeterClient.SayHello - 呼叫 Greeter.SayHello 服務並封鎖直到完成為止。 請勿在非同步程式碼中使用。

伺服器串流呼叫

伺服器串流呼叫會從傳送要求訊息的用戶端開始。 ResponseStream.MoveNext() 會讀取從服務串流處理的訊息。 當 ResponseStream.MoveNext() 傳回 false 時,伺服器串流呼叫已完成。

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() 擴充方法會從回應串流讀取所有訊息:

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
}

用戶端串流呼叫

用戶端串流呼叫會在未使用傳送訊息的用戶端的情況下開始。 用戶端可以選擇使用 RequestStream.WriteAsync 來傳送訊息。 當用戶端完成傳送訊息時,應該呼叫 RequestStream.CompleteAsync() 以通知服務。 當服務傳回回應訊息時,呼叫就會完成。

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 });
}
await call.RequestStream.CompleteAsync();

var response = await call;
Console.WriteLine($"Count: {response.Count}");
// Count: 3

雙向串流呼叫

雙向串流呼叫會在未使用傳送訊息的用戶端的情況下開始。 用戶端可以選擇使用 RequestStream.WriteAsync 來傳送訊息。 從服務串流處理的訊息可透過 ResponseStream.MoveNext()ResponseStream.ReadAllAsync() 存取。 當 ResponseStream 沒有其他訊息時,雙向串流呼叫就會完成。

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;

為了獲得最佳效能,並避免用戶端和服務中不必要的錯誤,請嘗試正常完成雙向串流呼叫。 當伺服器完成讀取要求串流,且用戶端已完成讀取回應串流時,雙向呼叫就會正常完成。 上述範例呼叫是正常結束的雙向呼叫的一個範例。 在呼叫中,用戶端:

  1. 呼叫 EchoClient.Echo 來開始新的雙向串流呼叫。
  2. 建立背景工作,以使用 ResponseStream.ReadAllAsync() 從服務讀取訊息。
  3. 使用 RequestStream.WriteAsync 將訊息傳送至伺服器。
  4. 通知伺服器已使用 RequestStream.CompleteAsync() 完成訊息傳送。
  5. 等候背景工作讀取完所有傳入的訊息。

在雙向串流呼叫期間,用戶端和服務可以隨時傳送訊息給彼此。 與雙向呼叫互動的最佳用戶端邏輯會根據服務邏輯而有所不同。

存取 gRPC 標頭

gRPC 呼叫會傳回回應標頭。 HTTP 回應標頭會傳遞與傳回訊息無關之呼叫的名稱/值中繼資料。

標頭可以使用 ResponseHeadersAsync 來存取,其會傳回中繼資料的集合。 標頭通常會以回應訊息傳回;因此,您必須等候它們。

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 的結果,才能取得標頭集合。
  • 不必在 ResponseAsync (或串流時的回應串流) 之前存取。 如果已傳回回應,則 ResponseHeadersAsync 會立即傳回標頭。
  • 如果發生連線或伺服器錯誤,且未針對 gRPC 呼叫傳回標頭,則會擲回例外狀況。

存取 gRPC 結尾

gRPC 呼叫可能會傳回回應結尾。 結尾可用來提供關於呼叫的名稱/值中繼資料。 結尾提供與 HTTP 標頭類似的功能,但會在呼叫結束時收到。

結尾可使用 GetTrailers() 來存取,其會傳回中繼資料的集合。 回應完成之後會傳回結尾。 因此,您必須先等候所有回應訊息,才能存取結尾。

在呼叫 GetTrailers() 之前,一元和用戶端串流呼叫必須等候 ResponseAsync

var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
var response = await call.ResponseAsync;

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");

在呼叫 GetTrailers() 之前,伺服器和雙向串流呼叫必須完成等候回應串流:

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
}

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");

結尾也可以從 RpcException 存取。 服務可能會同時傳回結尾與不正常的 gRPC 狀態。 在此情況下,會從 gRPC 用戶端擲回的例外狀況擷取結尾:

var client = new Greet.GreeterClient(channel);
string myValue = null;

try
{
    using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
    var response = await call.ResponseAsync;

    Console.WriteLine("Greeting: " + response.Message);
    // Greeting: Hello World

    var trailers = call.GetTrailers();
    myValue = trailers.GetValue("my-trailer-name");
}
catch (RpcException ex)
{
    var trailers = ex.Trailers;
    myValue = trailers.GetValue("my-trailer-name");
}

設定期限

建議設定 gRPC 呼叫期限,因為它會提供呼叫執行時間上限。 它可以阻止行為不當的服務永遠運作並耗盡伺服器資源。 期限是建置可靠應用程式的有用工具。

設定 CallOptions.Deadline 以設定 gRPC 呼叫的期限:

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 服務

其他資源