使用 .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 服务的长期连接。 创建通道后,进行配置,使其具有与调用服务相关的选项。 例如,可在 GrpcChannelOptions 上指定用于调用的 HttpClient、发收和接收消息的最大大小以及记录日志,并将其与 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 通道配置客户端证书的信息,请参阅 ASP.NET Core 的 gRPC 中的身份验证和授权

若要调用不安全的 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 当前不支持通过 HTTP/2 使用 Grpc.Net.Client 调用 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() 进行访问,它会返回元数据的集合。 在响应完成后,会返回尾部。 因此,在访问尾部之前,必须等待所有响应消息。

一元和客户端流式调用必须等待出现 ResponseAsync 后才能调用 GetTrailers()

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

其他资源