Julho de 2019

Volume 34, Número 7

[Moderno]

Serviços de gRPC do ASP.NET Core

Por Dino Esposito | Julho de 2019

Dino EspositoDesenvolvida originalmente pela Google, a gRPC atualmente é uma estrutura de RPC (chamada de procedimento remoto) que surgiu como alternativa às interfaces com base em HTTP e RESTful para conectar componentes remotos e, especificamente, microsserviços. A nova estrutura de RPC foi criada em parte para funcionar com tecnologias modernas, como HTTP/2 e Protobuf.

A estrutura gRPC oferece associações nativas para várias linguagens de programação, incluindo C#, e seu uso interno em microsserviços ASP.NET e ASP.NET Core nunca foi um problema. No entanto, vale mencionar que algumas implementações anteriores de gRPC encapsulavam DLLs nativas e mantinham o serviço hospedado em seu próprio servidor. Já no .NET Core 3.0, o serviço gRPC é uma implementação .NET completa hospedada em Kestrel, assim como outros aplicativos Web ASP .NET. Este artigo explora o modelo de projeto do Visual Studio 2019 e a camada dedicada do ASP.NET Core.

Criando um serviço gRPC no Visual Studio 2019

Quando você escolhe criar um novo aplicativo Web ASP.NET Core, o Visual Studio 2019 dá a oportunidade de criar um novo tipo de componente: um serviço gRPC. Se você concluir o assistente, acabará tendo um projeto mínimo de ASP.NET Core com a dupla de arquivos de sempre, startup.cs e program.cs, além de algumas novas pastas atípicas chamadas protos e services. O arquivo program.cs não tem nada de especial, mas vale a pena dar uma olhada no arquivo startup.cs.

O método Configure da classe Startup contém a seguinte linha:

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}

Como esperado, AddGrpc é um método de extensão da classe IServiceCollection. Se você é uma pessoa curiosa e quer analisar os bastidores do método, veja um resumo do que você vai encontrar (provavelmente não é informação que você vá usar alguma vez na vida, mas é sempre bom ver como as engrenagens funcionam!):

services.AddRouting();
services.AddOptions();
services.TryAddSingleton<GrpcMarkerService>();
services.TryAddSingleton<ServiceMethodsRegistry>();
services.TryAddSingleton(typeof(ServerCallHandlerFactory<>));

A chamada de AddRouting é um pouco mais funcional, já que serve para habilitar o uso de roteamento global (um novo recurso introduzido com o ASP.NET Core 2.2) para comunicação entre clientes gRPC e pontos de extremidade gRPC no serviço ASP.NET Core que está sendo criado. Os três singletons adicionados ao ambiente de tempo de execução do serviço cuidam do gerenciamento geral do ciclo de vida do serviço: fábrica, descoberta e invocação.

No método Configure da classe startup do aplicativo, você verá o uso do sistema de roteamento global e os pontos de extremidade necessários declarados da seguinte forma:

app.UseRouting();
app.UseEndpoints(endpoints =>
{
  endpoints.MapGrpcService<GreeterService>();
});

Um serviço gRPC não se baseia em controladores; ele usa uma classe de serviço que é construída para que cada solicitação processe a chamada do cliente. Essa classe de serviço é chamada GreeterService no código de exemplo que o modelo gera. O método MapGrpcService<T> cria uma associação da URL da chamada da gRPC com um manipulador de chamada que é invocado durante o processamento da solicitação. A fábrica do manipulador é recuperada e usada para criar uma nova instância da classe T para que a ação solicitada possa ocorrer. Essa é uma grande diferença entre a implementação do gRPC do ASP.NET Core 3.0 e a implementação de C# existente. Observe, no entanto, que ainda é possível que uma instância da classe de serviço seja resolvida como um singleton no contêiner de DI.

O protótipo da classe de serviço

No projeto de exemplo baseado em modelo, a classe de serviço é definida da seguinte forma:

public class GreeterService : Greeter.GreeterBase
{
  ...
}

Primeiro, você deve achar que implementar um serviço de gRPC é, assim como nas classes de controlador simples, codificar uma classe com vários métodos de ação públicos. Bem, não exatamente. É claro que o projeto do Visual Studio contém um arquivo com uma classe definida, como mostrado no código anterior. No entanto, a classe base da classe de serviço no trecho de código, a classe Greeter.GreeterBase, não está definida em nenhum lugar do mesmo projeto. Como isso é possível? A resposta está no código-fonte de outro arquivo, um pequeno arquivo de texto, encontrado na pasta protos (consulte a Figura 1).

Pasta da solução de um projeto de gRPC
Figura 1 Pasta da solução de um projeto gRPC

A pasta protos contém um ou mais arquivos de texto com a extensão .proto, conhecidos como arquivos de definição do buffer do protocolo, ou a forma abreviada arquivos Protobuf. Normalmente, existe um para cada serviço encontrado no serviço ASP.NET Core, mas não existem realmente restrições. Na verdade, você pode definir um serviço em um arquivo e as mensagens que ele usa em outro arquivo, ou definir vários serviços no mesmo arquivo. O arquivo de texto .proto fornece a definição da interface do serviço. Veja o conteúdo do arquivo .proto para o serviço greeter de exemplo:

syntax = "proto3";
package Greet;service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}

Se a primeira parte do conteúdo não for um elemento de sintaxe, o compilador de proto assumirá uma sintaxe proto2 mais antiga. A sintaxe mais recente .proto é proto3. Em seguida, você deve encontrar o nome do pacote que será criado.

A seção de serviço indica a interface de programação de aplicativo, ou seja, a lista de pontos de extremidade que podem ser chamados. Observe que SayHello é um método unário, ou seja, um método que funciona como uma chamada de função normal: o cliente envia uma solicitação única ao servidor e recebe uma resposta única. Além disso, a gRPC dá suporte a métodos de streaming de cliente, de servidor e bidirecionais. Eles permitem que o cliente crie mensagens para um fluxo e recebam uma resposta, ou enviem uma mensagem e recebam um fluxo de mensagens de retorno; ou, ainda, compartilhem um fluxo com um servidor para leitura e gravação. Descreva os métodos de streaming em um arquivo Protobuf usando o fluxo de palavras-chave, desta forma:

rpc SayHello1 (HelloRequest) returns (stream HelloReply) {}
rpc SayHello2 (stream HelloRequest) returns (HelloReply) {}
rpc SayHello3 (stream HelloRequest) returns (stream HelloReply) {}

As seções de mensagem no arquivo proto se referem a tipos de mensagens usados pelos métodos. Melhor dizendo, a seção de mensagem define todos os tipos de objeto de transferência de dados personalizados usados pelos métodos públicos. No trecho de código de exemplo, o serviço chamado Greeter é composto de um método de RPC chamado SayHello. O método usa um parâmetro de entrada do tipo HelloRequest e retorna um valor de saída no formato de uma instância do tipo HelloReply. Os dois tipos são compostos de uma única propriedade de cadeia de caracteres. O valor inteiro (nesse caso, 1) atribuído às propriedades das duas mensagens indica o número do campo e determina a posição do conteúdo em questão em formato binário de mensagem que está sendo transferido eletronicamente. Assim, esses valores não devem ser alterados depois que o serviço é implantado.

Detalhes dos elementos de mensagem

Na definição dos tipos de mensagem no arquivo proto, você pode usar vários tipos primitivos, incluindo bool, int32, sint32, double, float e uma longa lista de outras variações numéricas (int64, fixed32, uint32 e muito mais). Você também pode usar o tipo bytes para qualquer sequência aleatória de bytes. Mais especificamente, o tipo sint32 é ideal para inteiros com sinal, já que esse formato resulta na codificação mais eficiente de valores negativos. Observe que os tipos mencionados acima são os definidos na sintaxe proto3. Todas as linguagens compatíveis (como C#) os transformarão em tipos específicos a uma linguagem. Em C#, você pode ter long, bool, int, float e double. As propriedades recebem um valor padrão que coincide (pelo menos em C#) com o valor padrão do tipo de linguagem.

O esquema de uma mensagem pode variar em algum grau, já que as propriedades declaradas são opcionais. Mas se ela estiver marcada com a palavra-chave repetida, a mesma propriedade ainda será opcional, mas poderá ser repetida mais de uma vez. Isso explica porque o número de campo é relevante, já que é usado como espaço reservado do conteúdo real, como mostrado aqui:

message ListOfCitiesResponse {
  repeated string city = 1;
}

Além dos tipos escalares primitivos, a sintaxe de proto também dá suporte a enumerações. Os tipos enumerados podem ser definidos no arquivo proto ou até em linha no corpo do tipo de mensagem, assim:

enum Department {
  Unknown = 0;
  ICT = 1;
  HR = 2,
  Accounting = 3;
}

Observe que o primeiro elemento precisa usar o valor 0. Você pode ter membros com o mesmo valor, desde que os declare por meio da opção allow_alias, como mostrado aqui:

enum Department {
  option allow_alias = true;
  Unknown = 0;
  ICT = 1;
  HR = 2,
  Accounting = 3;
  Finance = 3;
}

Sem a opção allow_alias, você receberá um erro de compilação em caso de valores de enumeração repetidos. Se a enumeração estiver definida globalmente no arquivo proto, você poderá simplesmente usá-la por nome nos vários tipos de mensagem. Se ela estiver definida no corpo de um tipo de mensagem, você ainda poderá prefixar seu nome com o nome do tipo de mensagem. A Figura 2 mostra isso.

Figura 2 Definição inserida de tipos enumerados

message EmployeeResponse {
  string firstName = 1;
  string lastName = 2;
  enum Department {
    Unknown = 0;
    ICT = 1;
    HR = 2;
  }
  Department currentDepartment = 3;
}
message ContactResponse {
  ...  EmployeeResponse.Department department = 3;
}

Você pode referenciar livremente todos os tipos de mensagem no mesmo arquivo proto, além daqueles definidos nos arquivos proto externos, desde que sejam importados primeiro. Eis o código:

import "protos/another.proto";

As propriedades de um tipo de mensagem não são limitadas a tipos escalares e enumerações. Um elemento de mensagem pode fazer referência a outro tipo de mensagem, independentemente de sua definição estar inserida na mensagem, ser global no arquivo proto ou ter sido importada de outro arquivo proto.

Se durante o tempo de vida do serviço você precisar atualizar um dos tipos de mensagem, basta tomar cuidado para não reutilizar o número de campo. Você pode sempre adicionar novos elementos, desde que o código saiba como lidar com clientes que possam enviar pacotes sem esse elemento extra. Também é possível remover campos. Nesse caso, no entanto, é essencial que o número do campo removido não seja reutilizado. Na verdade, isso pode causar confusão e conflitos. Para se proteger disso, é possível declarar o número de campo crítico como reservado, da seguinte forma:

message PersoneResponse {
  reserved 1, 2, 5 to 8;
  reserved "gender", "address";
  ...
}

Você pode reservar números de campo (também usando a sintaxe estendida N a M), assim como nomes de campo. O mesmo se aplica a entradas em um tipo enumerado. No entanto, na maioria das vezes é melhor apenas renomear o campo com algum prefixo como NOTUSED_.

Essa explicação não aborda todas as possíveis variações da sintaxe proto3. Para obter mais informações, consulte bit.ly/2Hz5NJW.

A classe de serviço real

O código-fonte do arquivo .proto é processado silenciosamente para gerar uma classe base, a classe Greeter.GreeterBase ausente, que fornece o canal para que a comunicação cliente/servidor da gRPC possa ocorrer. Você encontrará o código-fonte real da base na pasta \Debug/netcoreapp3.0 do projeto. A Figura 3 mostra um trecho.

Figura 3 Classe de serviço de rRPC gerada automaticamente

public static partial class Greeter
{
  public abstract partial class GreeterBase
  {
    public virtual Task<Greet.HelloReply> SayHello(
      Greet.HelloRequest request,
      ServerCallContext context)
    {
      throw new RpcException();
    }
    ...
  }
}

Esse arquivo é gerado depois que você executa o build e não existirá antes disso. Observe também que seu projeto precisa ter uma referência ao pacote Grpc.Tools.

Tirando o arquivo de texto .proto e o trabalho de bastidores para compilá-lo em uma classe base, a classe de serviço resultante não é muito diferente de um controlador MVC simples. Como você pode ver, a classe é composta de alguns métodos públicos substituídos e suas implementações reais:

public override Task<HelloReply> SayHello(
  HelloRequest request, ServerCallContext context)
{
  return Task.FromResult(new HelloReply
  {
    Message = "Hello " + request.Name
  });
}

No corpo do método, você pode fazer o que for melhor para a tarefa: chamar um banco de dados ou serviço externo ou realizar algum cálculo devido. Para que a classe de serviço receba chamadas, você precisa iniciar o servidor gRPC para escutar em uma porta configurada. Vamos ver agora o cliente de gRPC.

Codificando um cliente de gRPC

O aplicativo de servidor do ASP.NET Core tem dependências no pacote de gRPC do ASP.NET Core e também no protocolo Google.ProtoBuf. Ele também tem uma dependência no pacote Grpc.Tools, mas não para ação no tempo de execução. O pacote é responsável por processar o conteúdo do arquivo de texto proto. O aplicativo cliente pode ser um aplicativo de console com dependências apenas no pacote Grpc.Core, o pacote Google.ProtoBuf e Grpc.Tools.

Para adicionar uma dependência ao serviço real, você tem duas opções. Uma é adicionar uma referência ao arquivo proto e deixar as ferramentas fazerem o trabalho. Outra é criar um terceiro projeto (biblioteca de classes) que contém apenas o arquivo proto. Em seguida, você vincula o assembly resultante tanto ao projeto de cliente quanto ao projeto de servidor. Para fazer referência ao arquivo proto, copie a pasta protos do projeto de servidor para o projeto de cliente e edite o arquivo CSPROJ, adicionando o seguinte código:

<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

Em seguida, crie o código para abrir um canal e fazer chamadas. Convém notar que a estrutura gRPC usa o protocolo binário ProtoBuf para chamadas remotas, que, por sua vez, usa HTTP/2 como transporte. (Observe que o ProtoBuf é a configuração padrão, mas, em tese, você pode usar outras pilhas de serialização/desserialização.) Veja a configuração necessária para chamar um serviço gRPC. Observe que, quando o ASP.NET Core 3.0 for lançado, existirá um cliente gRPC gerenciado que mudará o código anterior para criar um cliente:

var channel = new Channel(serviceUrl, ChannelCredentials.Insecure);
var client = new Greeter.GreeterClient(channel);

Por padrão, a URL do serviço é localhost e a porta (conforme configurada no projeto de servidor) é 50051. Na referência do cliente, chame os métodos prototipados como se fossem uma chamada local, como mostra o código abaixo:

var request = new HelloRequest { Name = name };
var response = client.SayHelloAsync(request);
Console.WriteLine(response.Message);

Você pode ver a saída resultante na Figura 4.

Aplicativos cliente e de servidor na prática
Figura 4 Aplicativos cliente e de servidor na prática

No fim das contas, a gRPC tem uma analogia com o antigo Distributed COM da década de 1990. Assim como o DCOM, ela permite que você chame objetos remotos como se fossem locais e faz isso por meio de um protocolo binário muito rápido que usa HTTP/2.

A gRPC não é REST e não é perfeita, mas é mais uma opção e é totalmente open-source. Não dá ainda para saber se a gRPC substituirá REST no coração dos desenvolvedores. É fato que alguns cenários de microsserviço realistas e concretos já existem mostrando que a gRPC é realmente uma vantagem. Você pode encontrar uma comparação útil entre gRPC e REST aqui: bit.ly/30VB7do.


Dino Esposito é autor de mais de 20 livros e de mais de mil artigos em seus 25 anos de carreira. Autor de “The Sabbatical Break”, um show de estilo cênico, Esposito se ocupa escrevendo software para um mundo mais ecológico, como estrategista digital da BaxEnergy. Siga-o no Twitter: @despos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: John Luo, James Newton-King


Discuta esse artigo no fórum do MSDN Magazine