gRPC no lado do Cliente

A maioria dos componentes de comunicação entre sistemas que utilizam REST serializam seus payloads em JSON. Atualmente, o JSON não possui um padrão de validação de esquema amplamente utilizado: JSON Schema não é difundido. A validação de esquema padrão permite delegar a validação a uma biblioteca de terceiros e pronto. Sem um padrão, temos que recorrer à validação manual no código. Pior ainda, devemos manter o código de validação sincronizado com o esquema.

O XML possui validação de esquema nativamente: um documento XML pode declarar uma gramática à qual deve se conformar. O SOAP, sendo baseado em XML, também se beneficia disso.

Outras alternativas de serialização possuem a opção de validação de esquema: por exemplo, Avro, Kryo e Protocol Buffers. Curiosamente, o gRPC usa o Protobuf para oferecer RPC entre componentes distribuídos:

O gRPC é uma moderna estrutura open source de alto desempenho para Remote Procedure Call (RPC) que pode ser executada em qualquer ambiente. Pode conectar eficientemente serviços em e entre datacenters com suporte pluggable para balanceamento de carga, rastreamento, verificação de saúde e autenticação. Também é aplicável na última etapa da computação distribuída para conectar dispositivos, aplicativos móveis e navegadores a serviços de backend.

Por que gRPC?

Além disso, o Protocolo é um mecanismo de serialização binária, economizando muito banda. Portanto, o gRPC é uma excelente opção para comunicação entre sistemas. Mas se todos os seus componentes falam gRPC, como clientes simples podem chamá-los? Neste post, construiremos um serviço gRPC e mostraremos como chamá-lo a partir de cURL.

A Simple gRPC Service

A documentação do gRPC é exaustiva, então aqui está um resumo:

  • gRPC é uma estrutura de Chamada de Procedimento Remoto.
  • Funciona em uma ampla variedade de linguagens.
  • Baseia-se em Protocol Buffers:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

  • Faz parte do portfólio CNCF e está atualmente na fase de incubação.

Vamos configurar nosso serviço gRPC. Usaremos Java, Kotlin, Spring Boot e um projeto de integração específico do gRPC com Spring Boot. A estrutura do projeto contém dois projetos: um para o modelo e outro para o código. Comecemos com o projeto do modelo.

I didn’t want something complicated. Reusing a simple example is enough: the request sends a string, and the response prefixes it with Hello. We design this model in a dedicated Protobuf schema file:

ProtoBuf

 

syntax = "proto3";                                        //1

package ch.frankel.blog.grpc.model;                       //2

option java_multiple_files = true;                        //3
option java_package = "ch.frankel.blog.grpc.model";       //3
option java_outer_classname = "HelloProtos";              //3

service HelloService {                                    //4
    rpc SayHello (HelloRequest) returns (HelloResponse) {
    }
}

message HelloRequest {                                    //5
    string name = 1;                                      //6
}

message HelloResponse {                                   //7
    string message = 1;                                   //6
}

  1. Versão da definição Protobuf
  2. Pacote
  3. Configuração específica do Java
  4. Definição do serviço
  5. Definição da requisição
  6. Definição do campo: Primeiro vem o tipo, depois o nome e finalmente a ordem.
  7. Definição da resposta

Vamos usar o Maven para gerar o código Java de modelo:

XML

 

<project>
  <dependencies>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-stub</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-protobuf</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>jakarta.annotation</groupId>              <!--1-->
      <artifactId>jakarta.annotation-api</artifactId>
      <version>1.3.5</version>
      <optional>true</optional>
    </dependency>
  </dependencies>
  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>                 <!--2-->
        <artifactId>os-maven-plugin</artifactId>
        <version>1.7.1</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>    <!--3-->
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>${protobuf-plugin.version}</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

  1. Dependências em tempo de compilação
  2. Informações sobre o Sistema Operacional; usado no próximo plugin
  3. Gere o código Java a partir do arquivo proto.

Após a compilação, a estrutura deve se parecer com o seguinte:

Podemos empacotar as classes em um JAR e usá-lo em um projeto de aplicativo web. O último está em Kotlin, mas apenas porque é minha linguagem JVM favorita.

Só precisamos de uma dependência específica do Spring Boot starter para integrar endpoints gRPC com o Spring Boot:

XML

 

<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-server-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

Aqui está a parte importante:

Kotlin

 

@GrpcService                                                        //1
class HelloService : HelloServiceImplBase() {                       //2
  override fun sayHello(
      request: HelloRequest,                                        //2
      observer: StreamObserver<HelloResponse>                       //3
  ) {
    with(observer) {
      val reply = HelloResponse.newBuilder()                        //2
                               .setMessage("Hello ${request.name}") //4
                               .build()
      onNext(reply)                                                 //5
      onCompleted()                                                 //5
    }
  }
}

  1. O grpc-server-spring-boot-starter detecta a anotação e faz sua magia.
  2. Classes de referência geradas no projeto acima
  3. A assinatura do método permite um parâmetro StreamObserver. A classe vem de grpc-stub.jar
  4. Obtenha a solicitação e prefixe-a para construir a mensagem de resposta.
  5. Jogue os eventos. 

Agora podemos iniciar o aplicativo web com ./mvnw spring-boot:run.

Testando o Serviço gRPC

A ideia por trás desta postagem é que acessar o serviço gRPC com ferramentas regulares é impossível. Para testar, precisamos de uma ferramenta dedicada, no entanto. Encontrei grpcurl. Vamos instalá-lo e usá-lo para listar os serviços disponíveis:

Shell

 

grpcurl --plaintext localhost:9090 list   #1-2

  1. Liste todos os serviços gRPC disponíveis sem verificação de TLS. 
  2. Para evitar conflitos entre gRPC e outros canais, como REST, o Spring Boot usa outro portão.
Plain Text

 

ch.frankel.blog.grpc.model.HelloService   #1
grpc.health.v1.Health                     #2
grpc.reflection.v1alpha.ServerReflection  #2

  1. O serviço gRPC que definimos
  2. Dois serviços adicionais fornecidos pelo starter personalizado

Também podemos mergulhar na estrutura do serviço:

Shell

 

grpcurl --plaintext localhost:9090 describe ch.frankel.blog.grpc.model.HelloService
Java

 

service HelloService {
  rpc SayHello ( .ch.frankel.blog.grpc.model.HelloRequest ) returns ( .ch.frankel.blog.grpc.model.HelloResponse );
}

Finalmente, podemos chamar o serviço com dados:

Shell

 

grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
JSON

 

{
  "message": "Hello John"
}

Acessando o Serviço gRPC com Ferramentas Regulares

Imagine que temos uma aplicação cliente JavaScript comum que precisa acessar o serviço gRPC. Quais seriam as alternativas?

A abordagem geral é através do grpc-web:

A JavaScript implementation of gRPC for browser clients. For more information, including a quick start, see the gRPC-web documentation.

Os clientes gRPC-web se conectam aos serviços gRPC por meio de um proxy especial; por padrão, o gRPC-web usa o Envoy.

No futuro, esperamos que o gRPC-web seja suportado em frameworks web específicos de linguagem para linguagens como Python, Java e Node. Para mais detalhes, consulte a roadmap.

grpc-web

A descrição menciona uma limitação única: funciona apenas para JavaScript (até o momento). No entanto, há outra. É bastante intrusivo. Você precisa obter o arquivo proto, gerar código repetitivo, e fazer seu código chamá-lo. Você deve fazer isso para cada tipo de cliente. Pior ainda, se o arquivo proto mudar, você precisa regenerar o código do cliente em cada um deles.

Existe uma alternativa, porém, se você estiver usando um Gateway de API. Descreverei como fazê-lo com Apache APISIX, mas talvez outros gateways possam fazer o mesmo. grpc-transcode é um plugin que permite transcodificar chamadas REST para gRPC e vice-versa.

O primeiro passo é registrar o arquivo proto no Apache APISIX:

Shell

 

curl http://localhost:9180/apisix/admin/protos/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d "{ \"content\": \"$(sed 's/"/\\"/g' ../model/src/main/proto/model.proto)\" }"

O segundo passo é criar uma rota com o plugin mencionado:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",                           #1
  "plugins": {
    "grpc-transcode": {
      "proto_id": "1",                                       #2
      "service": "ch.frankel.blog.grpc.model.HelloService",  #3
      "method": "SayHello"                                   #4
    }
  },
  "upstream": {
    "scheme": "grpc",
    "nodes": {
      "server:9090": 1
    }
  }
}'

  1. Defina uma rota granular. 
  2. Referencie o arquivo proto definido na instrução anterior. 
  3. Serviço gRPC
  4. Método gRPC

Neste ponto, qualquer cliente pode fazer uma requisição HTTP ao ponto final definido. O Apache APISIX vai transcodificar a chamada para gRPC, encaminhá-la ao serviço definido, obter a resposta e transcodificá-la novamente.

Shell

 

curl localhost:9080/helloservice/sayhello?name=John
JSON

 

{"message":"Hello John"}

Comparado ao grpc-web, a abordagem da API Gateway permite compartilhar o arquivo proto com um único componente: a própria Gateway.

Benefícios da Transcodificação

Neste ponto, podemos aproveitar as capacidades da API Gateway. Imagine que queremos um valor padrão se nenhum name for passado, por exemplo, World. Os desenvolvedores gostariam de configurá-lo no código, mas qualquer mudança no valor exigiria uma compilação e implantação completas. As alterações podem ser quase instantâneas se colocarmos o valor padrão na cadeia de processamentos de rotas da Gateway. Vamos alterar nossa rota conforme:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",
  "plugins": {
    "grpc-transcode": {
      ...
    },
    "serverless-pre-function": {                    #1
      "phase": "rewrite",                           #2
      "functions" : [
        "return function(conf, ctx)                 #3
          local core = require(\"apisix.core\")
          if not ngx.var.arg_name then
            local uri_args = core.request.get_uri_args(ctx)
            uri_args.name = \"World\"
            ngx.req.set_uri_args(uri_args)
          end
        end"
      ]
    }
  },
  "upstream": {
      ...
  }
}'

  1. Plugin genérico quando nenhum outro se encaixa
  2. Reverta a requisição.
  3. Código Lua mágico que faz o truque

Agora, podemos executar a requisição com um argumento vazio e obter o resultado esperado:

Shell

 

curl localhost:9080/helloservice/sayhello?name
JSON

 

{"message":"Hello World"}

Conclusão

Neste post, descrevemos brevemente o gRPC e como ele beneficia a comunicação entre serviços. Desenvolvemos um simples serviço gRPC usando Spring Boot e grpc-server-spring-boot-starter. No entanto, isso tem um custo: clientes regulares não podem acessar o serviço. Tivemos que recorrer ao grpcurl para testá-lo. O mesmo acontece com clientes baseados em JavaScript – ou o navegador.

Para contornar essa limitação, podemos aproveitar um API Gateway. Demonstrei como configurar o Apache APISIX com o plugin grpc-transcode para alcançar o resultado desejado.

O código-fonte completo deste post pode ser encontrado no GitHub.

Para ir mais longe

Source:
https://dzone.com/articles/grpc-on-the-client-side