Olá!
Este é um post da sessão Playground, uma iniciativa para demonstrar, com pequenos tutoriais, tecnologias e ferramentas que entendo ter potencial para trazer ganhos aos seus projetos.
Apresentando o gRPC Web for .NET
Neste artigo quero fazer uma pequena apresentação sobre como funciona a biblioteca gRPC-Web for .NET, lançada pela Microsoft para suportar o padrão gRPC-Web em aplicações .NET Core e, com ela, superar algumas limitações encontradas no uso do gRPC.
Importante! Este artigo presume que você já tenha algum conhecimento sobre o padrão gRPC e sua implementação para .NET.
Caso não tenha, não se preocupe! O artigo cobre algumas noções básicas, e é possível ter uma introdução um pouco mais detalhada com este artigo da Microsoft: Introdução ao gRPC.
Como dito acima, há certas limitações no uso do gRPC. As que considero principais são:
1) Não poder hospedar um serviço no IIS ou no Azure App Service;
2) Não poder chamar métodos gRPC via navegador.
A primeira limitação nos obriga a criar serviços auto-hospedados, como Windows Services ou Linux Daemons por exemplo, e nos impede de usar uma implementação de servidor web tão familiar a nós desenvolvedores .NET, bem como um serviço de hospedagem que muitos já utilizamos para nossas aplicações, devido a certas features do protocolo HTTP/2 que não são suportadas por ambos.
A segunda é um tanto pior porque interfere na arquitetura dos nossos serviços. Isso porque serviços concebidos para falar Protobuf via gRPC dentro da rede vão precisar fornecer seus dados para o cliente via Web API, que vai serializá-los em formato JSON.
Essa necessidade adiciona complexidade (na forma de uma nova camada de aplicação), um ponto de falha (na forma da Web API), e um desempenho inferior na entrega dos dados, já que JSON é um formato de serialização em texto (e verboso!) enquanto Protobuf é um formato de serialização binário.
Entendendo essas limitações do gRPC como justificativas para o uso do gRPC Web, vamos ver como fazê-lo!
Você vai precisar de:
- Um editor ou IDE (ex.: VSCode);
- Protoc: uma aplicação CLI para gerar o proxy JS e os modelos de mensagem definidos em seu arquivo Protobuf;
-
Protoc-gen-gRPC-web: um plugin para o
protoc
que define as configurações de exportação do JS gerado; - Webpack (npm): para criar o JS final para distribuição, com todas as dependências necessárias ao gRPC-Web.
Começando a aplicação
A aplicação de exemplo será bem simples, e simulará um jogo de loteria com 6 números, selecionáveis de um intervalo de 1 a 30.
O primeiro passo para a criação de nossa aplicação é sua infraestrutura. Por praticidade, vamos criar a aplicação como uma Web API padrão do .NET Core, remover a pasta Controllers
e o arquivo WeatherForecast.cs
da raiz do projeto:
dotnet new webapi -o Grpc.Web.Lottery
Em seguida, precisamos definir os contratos do serviço gRPC via arquivo .proto
. Para isso, vamos criar, na raiz do projeto, a pasta Protos
, e incluir o arquivoLottery.proto
com o seguinte conteúdo:
syntax="proto3";
option csharp_namespace="gRPC.Web.Lottery.Rpc";
package Lottery;
service LotteryService
{
rpc Play(PlayRequest) returns (PlayReply);
}
message PlayRequest
{
repeated int32 Numbers=1;
}
message PlayReply
{
string Message=1;
}
Como você pode ver, a definição dos contratos é exatamente a mesma que atende ao gRPC. Não há qualquer mudança para suportar o gRPC-Web!
Com os contratos definidos, é hora de tornar possível a geração do proxy C# do serviço gRPC e suas mensagens a partir do Protobuf. Para isso são necessários dois pacotes, e a indicação do arquivo .proto
que será usado como fonte:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspnetCore" Version="2.29.0" />
<PackageReference Include="Grpc.AspnetCore.Web" Version="2.29.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos/Lottery.proto" GrpcServices="Server" />
</ItemGroup>
</Project>
O pacote Grpc.AspnetCore
é responsável pela geração do código C# com os contratos definidos no arquivo .proto
e oferecer suporte ao gRPC. Já o pacote Grpc.AspnetCore.Web
oferece o suporte ao padrão gRPC-Web. Após a instalação dos pacotes, vamos gerar o código C#. Para isso, basta invocar um build via CLI:
dotnet build
Importante! O código gerado pela lib Grpc.AspnetCore não é incluído diretamente no projeto. Em vez disso, é gerado na pasta
obj
, podendo ser importado normalmente pelo namespace definido no arquivo.proto
na instruçãooption csharp_namesapce
, neste casoGrpc.Web.Lottery.Rpc
.
A lógica e o serviço
Uma vez criada a infraestrutura do projeto, e o código C# com o proxy gRPC e suas mensagens, vamos criar a lógica para a nossa aplicação. Primeiro vamos criar uma pasta chamada Models
na raiz do projeto e, em seguida, o arquivo LotteryDrawer.cs
com o seguinte conteúdo:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Grpc.Web.Lottery.Models
{
public class LotteryDrawer
{
private const int LotteryRange = 30;
private const int NumbersToDraw = 6;
private static readonly Random _random = new Random();
public static IEnumerable<int> Draw()
{
int[] numbers = Enumerable.Range(1, LotteryRange).ToArray();
for(int oldIndex = 0; oldIndex < LotteryRange -2; oldIndex++)
{
int newIndex = _random.Next(oldIndex, LotteryRange);
(numbers[oldIndex], numbers[newIndex]) = (numbers[newIndex], numbers[oldIndex]);
}
return numbers.Take(NumbersToDraw);
}
}
}
O código acima gera uma sequência com 30 números, os embaralha com um algoritmo chamado Embaralhamento de Fisher-Yates (texto em inglês) e retorna os 6 primeiros, que serão comparados adiante com os números informados pelo jogador via cliente JS.
Agora que temos a lógica para escolher os números, vamos à implementação do serviço gRPC propriamente dito. Para isso, criaremos a pasta Rpc
na raiz do projeto, e adicionaremos o arquivo LotteryServiceHandler.cs
com o seguinte conteúdo:
using System;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Web.Lottery.Models;
namespace Grpc.Web.Lottery.Rpc
{
public class LotteryServiceHandler : LotteryService.LotteryServiceBase
{
override public Task<PlayReply> Play (PlayRequest request, Core.ServerCallContext context)
{
var result = LotteryDrawer.Draw();
bool won = result.OrderBy(i => i)
.SequenceEqual(request.Numbers
.AsEnumerable()
.OrderBy(i => i));
return Task.FromResult(new PlayReply { Message = $"Números sorteados: {string.Join('-', result)}. Você {(won ? "ganhou" : "perdeu")}!" });
}
}
}
Acima nós temos o código que vai manipular as requisições gRPC-Web. Note que a classe LotteryServiceHandler
herda de LotteryService.LotteryServiceBase
, o proxy que foi gerado no build feito a partir do arquivo .proto
. Além disso, o método Play
recebe como argumento o tipo PlayRequest
e retorna o tipo PlayReply
, ambos declarados como mensagens no mesmo arquivo.
O que o serviço faz é bastante simples: sorteia 6 números de um intervalo entre 1 e 30 e, após ordená-los, os compara com os números escolhidos pelo jogador, também ordenados. Se a sequência for igual, o jogador ganhou!
O Front-end
Agora vamos nos dedicar à interface de usuário pela qual o jogador escolherá seus números. Por praticidade, vamos usar uma Razor Page e, para criá-la, vamos adicionar a pasta Pages
à raiz do projeto e, dentro dela, criar o arquivo Index.cshtml
com o seguinte conteúdo:
@page
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8"/>
<title>gRpc Web Lotery</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<div style="margin:0 0 10px 3px"><span>Escolha 6 números de 1 a 30:</span></div>
<table>
<tbody>
<tr>
<td><input type="number" name="chosen1" min="1" max="30"></td>
<td><input type="number" name="chosen2" min="1" max="30"></td>
<td><input type="number" name="chosen3" min="1" max="30"></td>
</tr>
<tr>
<td><input type="number" name="chosen4" min="1" max="30"></td>
<td><input type="number" name="chosen5" min="1" max="30"></td>
<td><input type="number" name="chosen6" min="1" max="30"></td>
</tr>
</tbody>
</table>
<div style="margin: 20px 0 0 3px"><button id="buttonPlay">Jogar!</button></div>
<div style="margin: 20px 0 0 3px"><span id="resultSpan"></span></div>
<script src="~/js/dist/main.js"></script>
</body>
E, agora, assim como criamos o proxy gRPC e suas mensagens em C# a partir do arquivo .proto
, vamos gerar seus equivalentes gRPC-Web em JS. Para hospedá-los, vamos aproveitar o recurso de arquivos estáticos do Asp.Net Core, criando as pastas wwwroot\js
na raíz do projeto. Em seguida, na CLI, vamos à pasta Protos
e chamar o protoc
em conjunto com o plugin protoc-gen-grpc-web
.
PS X:\code\Grpc.Web.Lottery\Protos> protoc -I='.' Lottery.proto --js_out=import_style=commonjs:..\wwwroot\js --grpc-web_out=import_style=commonjs,mode=grpcweb:..\wwwroot\js
O comando acima vai exportar para a pasta wwwroot\js
um arquivo JS com os contratos Protobuf a partir do arquivo Lottery.proto
e, em seguida, um segundo arquivo JS com o proxy gRPC-Web.
Um detalhe interessante: no trecho
mode=grpcweb
é definido o modo de serialização das mensagens. Omode=grpcweb
utiliza serialização binária no payload das chamadas, enquanto omode=grpcwebtext
utiliza serialização em texto, como uma sequência de bytes codificados como uma string em base 64.
Agora que temos criados nosso cliente e contratos gRPC-Web, vamos implementar a chamada ao servidor. Na pasta wwwroot\js
vamos criar o arquivo lottery-client.js
com o conteúdo a seguir:
const {PlayRequest, PlayReply} = require('./Lottery_pb.js');
const {LotteryServiceClient} = require('./Lottery_grpc_web_pb.js');
const client = new LotteryServiceClient('https://localhost:5001');
(function() {
document.querySelector('#buttonPlay').addEventListener("click", function(event) {
var request = new PlayRequest();
var chosenNumbers = [];
for(var i = 1; i<= 6; i++)
chosenNumbers[i-1] = document.querySelector('input[name="chosen' + i + '"]').value;
request.setNumbersList(chosenNumbers);
client.play(request, {}, (err, response) => {
document.querySelector("#resultSpan").innerHTML = response.getMessage();
});
});
})();
Repare que no código acima importamos os arquivos gerados pelo protoc
e pelo protoc-gen-grpc-web
para termos acesso ao proxy gRPC-Web e às mensagens que serão trocadas com o servidor. Em seguida, quando o documento é carregado, adicionamos um manipulador de evento de clique ao botão definido em nossa Razor Page para enviar os números escolhidos pelo jogador para o servidor.
Agora que temos nossa lógica pronta, precisamos adicionar aos nossos scripts o arquivo de pacote npm com as dependências do nosso cliente JS. Na pasta wwwroot\js
vamos adicionar o arquivo package.json
com o seguinte conteúdo:
{
"name": "grpc-web-lottery",
"version": "0.1.0",
"description": "gRPC-Web Lottery",
"main": "lottery-client.js",
"devDependencies": {
"@grpc/grpc-js": "~1.0.5",
"@grpc/proto-loader": "~0.5.4",
"async": "~1.5.2",
"google-protobuf": "~3.12.0",
"grpc-web": "~1.1.0",
"lodash": "~4.17.0",
"webpack": "~4.43.0",
"webpack-cli": "~3.3.11"
}
}
E, por fim, vamos criar nosso JS final com o webpack:
PS X:\code\Grpc.Web.Lottery\wwwroot\js> npm install
PS X:\code\Grpc.Web.Lottery\wwwroot\js> npx webpack lottery-client.js
Toques finais!
Estamos quase lá! Precisamos agora voltar à infraestrutura do projeto e adicionar algumas configurações. No arquivo Startup.cs
na raiz do projeto, vamos adicionar as seguinte instruções aos métodos de configuração:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
app.UseRouting();
app.UseGrpcWeb();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<LotteryServiceHandler>()
.EnableGrpcWeb();
endpoints.MapRazorPages();
});
}
Importante! A declaração de uso do gRPC-Web,
app.UseGrpcWeb()
, deve estar entreapp.UseRouting()
eapp.UseEndpoints(...)
para ter efeito!
E voi la!
Agora podemos testar nossa aplicação. Estando tudo certo, o resultado será o seguinte:
É! Infelizmente eu perdi! :(
Mas, apesar disso, temos nossa primeira aplicação utilizando gRPC-Web, que poderá ser hospedada em um IIS, Azure App Service, e que dispensa a necessidade de falar JSON com o navegador, aproveitando o formato binário do Protobuf! :)
Para ver um exemplo funcional, segue uma versão hospedada no Azure App Service: gRPC-Web Lottery.
Para acessar o código-fonte completo, clique aqui!
Gostou? Me deixe saber com uma curtida. Tem dúvidas? Mande um comentário que responderei assim que possível.
Até a próxima!
Referências: