Chamar os serviços gRPC com o cliente .NET

Uma biblioteca de clientes gRPC do .NET está disponível no pacote NuGet Grpc.Net.Client. Este documento explica como:

  • Configurar um cliente gRPC para chamar os serviços gRPC.
  • Fazer chamadas gRPC para métodos unários, de streaming de servidor, de streaming de cliente e de streaming bidirecional.

Configurar o cliente gRPC

Os clientes gRPC são tipos de cliente concretos gerados a partir de arquivos .proto. O cliente gRPC concreto tem métodos que se traduzem para o serviço gRPC no arquivo .proto. Por exemplo, um serviço chamado Greeter gera um tipo GreeterClient com métodos para chamar o serviço.

Um cliente gRPC é criado a partir de um canal. Comece usando GrpcChannel.ForAddress para criar um canal e, em seguida, use o canal para criar um cliente gRPC:

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

Um canal representa uma conexão de longa duração com um serviço gRPC. Quando um canal é criado, ele é configurado com opções relacionadas à chamada de um serviço. Por exemplo, o HttpClient usado para fazer chamadas, o tamanho máximo da mensagem de envio e recebimento e o registro em log podem ser especificados em GrpcChannelOptions e usados com GrpcChannel.ForAddress. Para obter uma lista completa de opções, consulte opções de configuração do cliente.

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

Configurar TLS

Um cliente gRPC deve usar a mesma segurança em nível de conexão que o serviço chamado. O TLS do cliente gRPC é configurado quando o canal gRPC é criado. Um cliente gRPC lança um erro quando chama um serviço e a segurança em nível de conexão do canal e do serviço não correspondem.

Para configurar um canal gRPC para usar o TLS, verifique se o endereço do servidor começa com https. Por exemplo, GrpcChannel.ForAddress("https://localhost:5001") usa o protocolo HTTPS. O canal gRPC negocia automaticamente uma conexão protegida pelo TLS e usa uma conexão segura para fazer chamadas gRPC.

Dica

O gRPC dá suporte à autenticação de certificado do cliente por TLS. Para obter informações sobre como configurar certificados de cliente com um canal gRPC, consulte Autenticação e autorização no gRPC para ASP.NET Core.

Para chamar serviços gRPC não seguros, verifique se o endereço do servidor começa com http. Por exemplo, GrpcChannel.ForAddress("http://localhost:5000") usa o protocolo HTTPS. No .NET Core 3.1, é necessária uma configuração adicional para chamar serviços gRPC não seguros com o cliente .NET.

Desempenho de cliente

Desempenho e uso do canal e do cliente:

  • A criação de um canal pode ser uma operação cara. A reutilização de um canal para chamadas gRPC oferece benefícios de desempenho.
  • Um canal gerencia conexões com o servidor. Se a conexão for fechada ou perdida, o canal se reconectará automaticamente na próxima vez em que uma chamada gRPC for feita.
  • Os clientes gRPC são criados com canais. Os clientes gRPC são objetos leves e não precisam ser armazenados em cache ou reutilizados.
  • Vários clientes gRPC podem ser criados a partir de um canal, incluindo diferentes tipos de clientes.
  • Um canal e clientes criados a partir do canal podem ser usados com segurança por vários threads.
  • Os clientes criados a partir do canal podem fazer várias chamadas simultâneas.

GrpcChannel.ForAddress não é a única opção para criar um cliente gRPC. Se chamar serviços gRPC de um aplicativo ASP.NET Core, considere a integração de fábrica do cliente gRPC. A integração do gRPC com HttpClientFactory oferece uma alternativa centralizada para a criação de clientes gRPC.

Observação

No momento, não há suporte para chamar gRPC por HTTP/2 com Grpc.Net.Client no Xamarin. Estamos trabalhando para melhorar o suporte a HTTP/2 em uma versão futura do Xamarin. Grpc.Core e gRPC-Web são alternativas viáveis que funcionam hoje.

Fazer chamadas gRPC

Uma chamada gRPC é iniciada chamando um método no cliente. O cliente gRPC manipulará a serialização de mensagens e abordará a chamada gRPC para o serviço correto.

O gRPC tem diferentes tipos de métodos. Como o cliente é usado para fazer uma chamada gRPC depende do tipo de método chamado. Os tipos de método gRPC são:

  • Unário
  • Streaming de servidor
  • Streaming de cliente
  • Streaming bidirecional

Chamada unária

Uma chamada unária começa com o cliente enviando uma mensagem de solicitação. Uma mensagem de resposta é retornada quando o serviço é concluído.

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

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

Cada método de serviço unário no arquivo .proto resultará em dois métodos .NET no tipo de cliente gRPC concreto para chamar o método: um método assíncrono e um método de bloqueio. Por exemplo, em GreeterClient, há duas maneiras de chamar SayHello:

  • GreeterClient.SayHelloAsync – chama o serviço Greeter.SayHello de forma assíncrona. Pode ser aguardado.
  • GreeterClient.SayHello – chama o serviço Greeter.SayHello e bloqueia até a conclusão. Não use em código assíncrono.

Chamada de streaming de servidor

Uma chamada de streaming de servidor começa com o cliente enviando uma mensagem de solicitação. ResponseStream.MoveNext() lê mensagens transmitidas do serviço. A chamada de streaming de servidor é concluída quando ResponseStream.MoveNext() retorna 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
}

Ao usar o C# 8 ou posterior, a sintaxe await foreach pode ser usada para ler mensagens. O método de extensão IAsyncStreamReader<T>.ReadAllAsync() lê todas as mensagens do fluxo de resposta:

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
}

Chamada de streaming de cliente

Uma chamada de streaming de cliente é iniciada sem que o cliente envie uma mensagem. O cliente pode optar por enviar mensagens com RequestStream.WriteAsync. Quando o cliente termina de enviar mensagens, RequestStream.CompleteAsync() deverá ser chamado para notificar o serviço. A chamada é concluída quando o serviço retorna uma mensagem de resposta.

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

Chamada de streaming bidirecional

Uma chamada de streaming bidirecional é iniciada sem que o cliente envie uma mensagem. O cliente pode optar por enviar mensagens com RequestStream.WriteAsync. As mensagens transmitidas do serviço são acessíveis com ResponseStream.MoveNext() ou ResponseStream.ReadAllAsync(). A chamada de streaming bidirecional é concluída quando o ResponseStream não tem mais mensagens.

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;

Para obter o melhor desempenho e evitar erros desnecessários no cliente e no serviço, tente concluir chamadas de streaming bidirecionais normalmente. Uma chamada bidirecional é concluída normalmente quando o servidor termina de ler o fluxo de solicitação e o cliente termina de ler o fluxo de resposta. A chamada de exemplo anterior é um exemplo de uma chamada bidirecional que termina normalmente. Na chamada, o cliente:

  1. Inicia uma nova chamada de streaming bidirecional chamando EchoClient.Echo.
  2. Cria uma tarefa em segundo plano para ler mensagens do serviço usando ResponseStream.ReadAllAsync().
  3. Envia mensagens para o servidor com RequestStream.WriteAsync.
  4. Notifica o servidor de que terminou de enviar mensagens com RequestStream.CompleteAsync().
  5. Aguarda até que a tarefa em segundo plano tenha lido todas as mensagens de entrada.

Durante uma chamada de streaming bidirecional, o cliente e o serviço podem enviar mensagens entre si a qualquer momento. A melhor lógica de cliente para interagir com uma chamada bidirecional varia dependendo da lógica de serviço.

Acessar cabeçalhos gRPC

As chamadas gRPC retornam cabeçalhos de resposta. Os cabeçalhos de resposta HTTP passam metadados de nome/valor sobre uma chamada que não está relacionada à mensagem retornada.

Os cabeçalhos são acessíveis usando ResponseHeadersAsync, que retorna uma coleção de metadados. Os cabeçalhos normalmente são retornados com a mensagem de resposta; portanto, você deve aguardá-los.

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;

Uso de ResponseHeadersAsync:

  • Deve aguardar o resultado de ResponseHeadersAsync para obter a coleção de cabeçalhos.
  • Não precisa ser acessado antes de ResponseAsync (ou o fluxo de resposta ao transmitir por streaming). Se uma resposta tiver sido retornada, ResponseHeadersAsync retornará cabeçalhos instantaneamente.
  • Gerará uma exceção se houver um erro de conexão ou servidor e os cabeçalhos não forem retornados para a chamada gRPC.

Acessar trailers gRPC

As chamadas gRPC podem retornar trailers de resposta. Os trailers são usados para fornecer metadados de nome/valor sobre uma chamada. Os trailers fornecem funcionalidade semelhante aos cabeçalhos HTTP, mas são recebidos no final da chamada.

Os cabeçalhos são acessíveis usando GetTrailers(), que retorna uma coleção de metadados. Os trailers são retornados após a conclusão da resposta. Portanto, você deve aguardar todas as mensagens de resposta antes de acessar os trailers.

As chamadas de streaming unárias e de cliente devem aguardar ResponseAsync antes de chamar 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");

As chamadas de streaming de servidor e bidirecional devem terminar de aguardar o fluxo de resposta antes de chamar 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");

Os trailers também podem ser acessados por meio de RpcException. Um serviço pode retornar trailers junto com uma status gRPC diferente de OK. Nessa situação, os trailers são recuperados da exceção gerada pelo cliente 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");
}

Configurar prazo

É recomendável configurar um prazo de chamada gRPC porque ele fornece o tempo máximo de execução de uma chamada. Ela impede que serviços funcionando de forma inadequada sejam executados para sempre e o esgotamento dos recursos do servidor. Os prazos são uma ferramenta útil para compilar aplicativos confiáveis.

Configure CallOptions.Deadline para definir um prazo para uma chamada 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.");
}

Para obter mais informações, consulte Serviços gRPC confiáveis com prazos finais e cancelamento.

Recursos adicionais