使用 .NET 客户端调用 gRPC 服务
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
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.Core 和 gRPC-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
}
从启动服务器流式处理调用返回的类型会实现 IDisposable
。 始终释放流式处理调用,以确保它已停止,且已清理所有资源。
客户端流式处理调用
客户端无需发送消息即可开始客户端流式处理调用 。 客户端可选择使用 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
从启动客户端流式处理调用返回的类型会实现 IDisposable
。 始终释放流式处理调用,以确保它已停止,且已清理所有资源。
双向流式处理调用
客户端无需发送消息即可开始双向流式处理调用 。 客户端可选择使用 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;
为获得最佳性能并避免客户端和服务中出现不必要的错误,请尝试正常完成双向流式调用。 当服务器已读取请求流且客户端已读取响应流时,双向调用正常完成。 前面的示例调用就是一个正常结束的双向调用。 在调用中,客户端:
- 通过调用
EchoClient.Echo
启动新的双向流式调用。 - 使用
ResponseStream.ReadAllAsync()
创建用于从服务中读取消息的后台任务。 - 使用
RequestStream.WriteAsync
将消息发送到服务器。 - 使用
RequestStream.CompleteAsync()
通知服务器它已发送消息。 - 等待直到后台任务已读取所有传入消息。
双向流式处理调用期间,客户端和服务可在任何时间互相发送消息。 与双向调用交互的最佳客户端逻辑因服务逻辑而异。
从启动双向流式处理调用返回的类型会实现 IDisposable
。 始终释放流式处理调用,以确保它已停止,且已清理所有资源。
访问 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 服务。