Setembro de 2019

Volume 34 – Número 9

[Moderno]

Métodos de Streaming em Serviços gRPC do ASP.NET Core

De Dino Esposito

Dino EspositoNa edição anterior do Cutting Edge, apresentei a criação de um novo tipo de serviço com base na estrutura gRPC que (embora esteja disponível para desenvolvedores C# há algum tempo) estreia no ASP.NET Core 3.0 como um serviço nativo hospedado diretamente pelo Kestrel. A estrutura gRPC é adequada para a comunicação binária ponto a ponto entre os pontos de extremidade conectados que, na maioria das vezes, mas não necessariamente, são microsserviços. Ele também é compatível com soluções técnicas atualizadas, como o Google Protobuf para serialização de conteúdo e HTTP/2 para transporte.

O Visual Studio 2019 vem com um modelo de projeto ASP.NET Core 3.0 para criar o esqueleto de um serviço gRPC com apenas alguns cliques. Para obter uma introdução sobre gRPC e o kit de início gerado pelo Visual Studio, você pode conferir minha coluna de julho em msdn.com/magazine/mt833481. Neste mês, avanço mais uma etapa em minha exploração do gRPC. Primeiro, abordarei as ferramentas subjacentes de modo um pouco mais detalhado. Na verdade, algumas ferramentas são necessárias para analisar o conteúdo do arquivo .proto para as classes C# a serem usadas como a base tanto da implementação de cliente quanto da de serviço. Além disso, falarei sobre métodos de streaming e classes de mensagens complexas. Por fim, me concentrarei em como integrar métodos de gRPC transmitidos por streaming na interface do usuário de um aplicativo cliente Web.

Como criar o serviço gRPC

O modelo de projeto interno do Visual Studio coloca o arquivo de definição de interface do serviço (o arquivo .proto) em uma subpasta chamada protos, localizada no mesmo projeto de serviço. Neste artigo, no entanto, usarei uma abordagem diferente e começarei adicionando uma biblioteca de classes do .NET Standard 2.0 totalmente nova à solução inicialmente vazia.

A biblioteca de classes proto não contém explicitamente nenhuma classe C#. Tudo o que ela contém é um ou mais arquivos .proto. Você pode organizar arquivos .proto em pastas e subpastas como desejar. No aplicativo de exemplo, tenho um único arquivo .proto para um serviço de exemplo localizado na pasta de projeto protos. Aqui está um trecho do bloco de serviço do arquivo .proto de exemplo:

service H2H {
  rpc Details (H2HRequest) returns (H2HReply) {}
}

Espera-se que o serviço de exemplo H2H recupere algumas informações relacionadas a esporte de alguma localização remota. O método Details passa uma solicitação de disputa e recebe a pontuação das partidas anteriores entre as equipes ou os jogadores especificados. As mensagens H2HRequest e H2HReply podem ter a seguinte aparência:

message H2HRequest {
  string Team1 = 1;
  string Team2 = 2;
}
message H2HReply {
  uint32 Won1 = 1;
  uint32 Won2 = 2;
  bool Success = 3;
}

O primeiro tipo de mensagem passa informações sobre as equipes a serem processadas, enquanto o último recebe o histórico de partidas anteriores e um sinalizador booliano que denota o êxito ou a falha da operação. Até aqui tudo bem. Tudo nas mensagens é definido conforme vimos no artigo anterior. Usando o jargão gRPC, o método Details é um método unário, o que significa que cada solicitação recebe uma única resposta (e apenas ela). No entanto, essa é a maneira mais comum de codificar um serviço gRPC. Vamos adicionar funcionalidades de streaming da seguinte forma:

rpc MultiDetails (H2HMultiRequest) returns (stream H2HMultiReply) {}

O novo método MultiDetails é um método de streaming do lado do servidor, o que significa que, para cada solicitação que ele obtém de algum cliente gRPC, ele pode retornar várias respostas. Neste exemplo, o cliente pode enviar uma matriz de solicitações de disputa e receber respostas de disputa individuais de modo assíncrono, pois elas são elaboradas na extremidade do serviço. Para que isso aconteça, o método de serviço gRPC deve ser rotulado com a palavra-chave “fluxo” na seção “retornos”. Um método de fluxo pode exigir tipos de mensagem ad hoc também, como mostrado aqui:

message H2HMultiRequest {
  string Team = 1;
  repeated string OpponentTeam = 2;
}

Como mencionado, o cliente pode solicitar um registro de disputa entre uma determinada equipe e uma matriz de outras equipes. A palavra-chave repetida no tipo de mensagem apenas denota que o membro OpponentTeam pode aparecer mais de uma vez. Em termos puramente de C#, o tipo de mensagem H2HMultiRequest é conceitualmente equivalente ao seguinte pseudocódigo:

class H2HMultiRequest
{  string Team {get; set;}  IEnumerable<string> OpponentTeam {get; set;}}

No entanto, observe que o código gerado pelas ferramentas gRPC é ligeiramente diferente, conforme mostrado aqui:

public RepeatedField<string> OpponentTeam {get; set;}

Observe, na verdade, que qualquer classe gerada com base em uma mensagem gRPC tipo T implementa os membros da interface Google.ProtoBuf.IMessage<T>. O tipo de mensagem de resposta deve ser atribuído para descrever os dados reais retornados em cada etapa da fase de streaming. Portanto, cada resposta deve se referir a uma resposta de disputa individual, entre a equipe principal e uma das equipes adversárias especificadas na matriz, desta forma:

message H2HMultiReply {
  H2HItem Team1 = 1;
  H2HItem Team2 = 2;
}
message H2HItem {
  string Name = 1;
  uint32 Won = 2;
}

O tipo de mensagem H2HItem indica quantas partidas a equipe especificada ganhou em relação à outra equipe especificada na solicitação.

Antes de prosseguir para examinar a implementação de um método de fluxo, daremos uma olhada nas dependências exigidas pela biblioteca de classes compartilhada que insere a definição do proto. O projeto do Visual Studio precisa referenciar os pacotes NuGet na Figura 1.

The NuGet Dependencies of the Proto Shared Class Library
Figura 1 As dependências do NuGet da biblioteca de classes compartilhada proto

O projeto que inclui o arquivo de origem .proto precisa referenciar o pacote Grpc.Tools, bem como o Grpc.Net.Client (adicionado no .NET Core 3.0 Versão Prévia 6) e os pacotes Google.Protobuf exigidos por qualquer projeto do gRPC (seja ele cliente, serviço ou biblioteca). O pacote de ferramentas é basicamente responsável por analisar o arquivo .proto e gerar todas as classes C# necessárias no tempo de compilação. Um bloco de grupo de itens no arquivo .csproj instrui o sistema de ferramentas sobre como proceder. Eis o código:

<ItemGroup>
  <Protobuf Include="Protos\h2h.proto"
            GrpcServices="Server, Client"
            Generator="MSBuild:Compile" />
  <Content Include="@(Protobuf)" />
  <None Remove="@(Protobuf)" />
</ItemGroup>

A parte mais relevante do bloco ItemGroup é o nó Protobuf e, em particular, o atributo GrpcServices. O token do servidor no respectivo valor de cadeia de caracteres atribuído indica que as ferramentas devem gerar a classe de serviço para o protótipo de interface. O token de cliente indica que a criação da classe de cliente base para invocar o serviço também é esperada. Com isso feito, a DLL resultante contém classes C# para os tipos de mensagem, a classe de serviço base e a classe de cliente. O projeto de serviço e o projeto do cliente (seja ele de console, da Web ou para área de trabalho) só precisam referenciar o protótipo de DLL para que possam lidar com o serviço gRPC.

Como implementar o serviço

O serviço gRPC é um projeto ASP.NET Core com algumas configurações especiais feitas na classe de inicialização. Além da plataforma de servidor ASP.NET Core e do protótipo de assembly, ele referencia também a estrutura gRPC do ASP.NET Core e o pacote Google.Protobuf. A classe de inicialização adiciona o serviço de runtime gRPC no método Configure e acrescenta pontos de extremidade gRPC no método ConfigureServices, conforme mostrado na Figura 2.

Figura 2 Como configurar o gRPC

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{  // Some other code here   ...
  app.UseRouting();
  app.UseEndpoints(endpoints =>
  {
    endpoints.MapGrpcService<H2HService>();
  });
}

A classe de serviço herda da classe de serviço base as ferramentas criadas com base no conteúdo do arquivo .proto, desta forma:

public class H2HService : Sample.H2H.H2HBase
{
  // Unary method Details
  public override Task<H2HReply> Details(
              H2HRequest request, ServerCallContext context)
  {
    ...
  }
  ...
}

Métodos unários como o método Details têm uma assinatura mais simples do que métodos de fluxo. Eles retornam um objeto Task<TReply> e aceitam um objeto TRequest mais uma instância de ServerCallContext para acessar os detalhes essenciais da solicitação de entrada. Um método de fluxo do lado do servidor tem um parâmetro de fluxo de resposta adicional usado pelo código de implementação para transmitir os pacotes de volta. A Figura 3 apresenta a implementação do método de fluxo MultiRequest.

Figura 3 Um método de serviço gRPC de fluxo do lado do servidor

public override async Task MultiDetails(      H2HMultiRequest request,
      IServerStreamWriter<H2HMultiReply> responseStream,
      ServerCallContext context)
{  // Loops through the batch of operations embedded   // in the current request
  foreach (var opponent in request.OpponentTeam)
  {
    // Grab H2H data to return
    var h2h = GetHeadToHead_Internal(request.Team, opponent);
    // Copy raw data into an official reply structure    // Raw data is captured in some way: an external REST service
    // or some local/remote database    var item1 = new H2HItem {
     Name = h2h.Id1, Won = (uint) h2h.Record1};
    var item2 = new H2HItem {
     Name = h2h.Id2, Won = (uint) h2h.Record2};
    var reply = new H2HMultiReply { Team1 = item1, Team2 = item2 };
    // Write back via the output response stream
    await responseStream.WriteAsync(reply);
  }
  return;
}

Como você pode ver, em comparação com os métodos unários clássicos, o método de fluxo usa um parâmetro adicional do tipo IServerStreamWriter<TReply>. Esse é o fluxo de saída que o método usará para transmitir resultados conforme eles estiverem prontos. No código da Figura 3, o método entra em um loop para cada uma das operações solicitadas (nesse caso, uma matriz de equipes para obter partidas anteriores). Em seguida, ele transmite de volta os resultados conforme a consulta a um banco de dados local/remoto ou a um serviço Web retorna. Depois de concluído, o método retorna e o ambiente de runtime subjacente encerra o fluxo.

Como escrever um cliente para um método de fluxo

No código de exemplo, o aplicativo cliente é um aplicativo ASP.NET Core 3.0 simples. Ele contém referências ao pacote Google.Protobuf e ao pacote Grpc.Net.Client, além da biblioteca de protótipos compartilhados. A interface do usuário apresenta um botão com algum JavaScript anexado que posta em um método controlador. (Observe que nada impede que você use um formulário HTML clássico, exceto que o uso do Ajax para postar pode facilitar a tarefa de receber notificações de respostas e atualizar a interface do usuário mais facilmente.) A Figura 4 apresenta o código.

Figura 4 Como chamar o serviço gRPC

[HttpPost]
public async Task<IActionResult> Multi()
{
  // Call the RPC service
  var serviceUrl = "http://localhost:50051";
    AppContext.SetSwitch(
                "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
                true);
    var httpClient = new HttpClient() {BaseAddress = new Uri(serviceUrl) };
  var client = GrpcClient.Create<H2H.H2HClient>(httpClient);
  var request = new H2HMultiRequest() { Team = "AF-324" };
  request.OpponentTeam.AddRange(new[] { "AW-367", "AD-683", "AF-510" });
  var model = new H2HMultiViewModel();
  using (var response = client.MultiDetails(request))
  {
    while (await response.ResponseStream.MoveNext())
    {
      var reply = response.ResponseStream.Current;      // Do something here ...
    }  }
  return View(model);
}

Vale a pena relembrar que a porta do serviço gRPC depende do projeto do Visual Studio, enquanto a classe do chamador do cliente é definida na biblioteca de protótipos. Para preparar a solicitação para um método de streaming do lado do servidor, você não precisa fazer nada além de apenas popular o tipo de mensagem de entrada. Como mencionado, a coleção OpponentTeam é um tipo .NET enumerável e pode ser populada com AddRange ou chamadas repetidas para Add. O tipo propriamente dito não é um dos tipos de coleção do .NET Core, mas ainda é um tipo de coleção, apesar de ser implementado no pacote Google Protobuf.

Já que um método do lado do servidor transmite pacotes de volta até o final do fluxo, a chamada para o método propriamente dita retorna um objeto de fluxo. Em seguida, o código do cliente enumera os pacotes aguardando o fim da resposta. Cada iteração do loop while na Figura 4 captura um único pacote de resposta do serviço gRPC. O que acontece em seguida depende do aplicativo cliente. Em geral, há três situações distintas.

Uma é quando o aplicativo cliente tem sua própria interface do usuário, mas pode aguardar para coletar a resposta inteira antes de mostrar algo novo para esse usuário. Nesse caso, você carrega os dados transportados pelo objeto de resposta atual no modelo de exibição retornado pelo método de controlador. O segundo cenário é quando não há nenhuma interface do usuário (por exemplo, se o cliente é um microsserviço em funcionamento). Nesse caso, os dados recebidos são processados assim que estão disponíveis. Por fim, no terceiro cenário, o aplicativo cliente tem sua própria interface do usuário responsiva e é capaz de apresentar dados aos usuários à medida que eles vêm do servidor. Nesse caso, você pode anexar um ponto de extremidade SignalR Core ao aplicativo cliente e notificar a interface do usuário em tempo real (confira a Figura 5).

The Sample Application in Action
Figura 5 O aplicativo de exemplo em ação

O snippet de código a seguir mostra como o código do cliente é alterado quando um hub do SignalR é usado sobre a chamada gRPC:

var reply = response.ResponseStream.Current;
await _h2hHubContext.Clients
                    .Client(connId)
                    .SendAsync("responseReceived",
        reply.Player1.Name,
        reply.Player1.Won,
        reply.Player2.Name,
        reply.Player2.Won);

Você pode verificar o código-fonte para obter detalhes completos da solução. Falando de SignalR, há alguns pontos que vale a pena explorar. Primeiro, o código do SignalR é usado somente pelo aplicativo cliente que se conecta ao serviço gRPC. O hub é injetado no controlador do aplicativo cliente, não no serviço gRPC. E segundo, no que diz respeito à transmissão, vale a pena observar que o SignalR Core também tem sua própria API de streaming.

Outros tipos de fluxos de gRPC

Neste artigo, me concentrei em métodos de streaming de gRPC do lado do servidor, mas essa não é a única opção. A estrutura gRPC também é compatível com métodos de streaming do lado do cliente (várias solicitações/uma resposta) e bidirecional (várias solicitações/várias respostas). Para streaming do lado do cliente, a única diferença é o uso de um IAsyncStreamReader como o fluxo de entrada no método de serviço, conforme mostrado neste código:

public override async Task<H2HReply> Multi(
         IAsyncStreamReader<H2HRequest> requestStream,
         ServerCallContext context){  while (await requestStream.MoveNext())
  {
    var requestPacket = requestStream.Current;   
      // Some other code here
      ...  } }

Um método bidirecional retornará nulo e não usará parâmetros, pois ele estará lendo e gravando dados de entrada e saída por meio de fluxos de entrada e saída.

Em resumo, o gRPC é uma estrutura completa para conectar dois pontos de extremidade (cliente e servidor) em um protocolo binário, flexível e de software livre. O suporte que você obtém para gRPC do ASP.NET Core 3.0 é incrível e melhorará com o tempo, tornando agora um ótimo momento para começar e experimentar o gRPC, especialmente na comunicação entre microsserviços.


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
James Newton-King é um engenheiro da equipe de ASP.NET Core e trabalha com gRPC para .NET Core

John Luo é um engenheiro da equipe de ASP.NET Core e trabalha com gRPC para .NET Core


Discuta esse artigo no fórum do MSDN Magazine