A maioria dos componentes de comunicação entre sistemas que utilizam REST serializam seus payloads em JSON. Atualmente, o JSON carece de 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, precisamos recorrer à validação manual no código. Pior ainda, devemos manter o código de validação em sincronia com o esquema.
O XML possui validação de esquema prontamente disponível: um documento XML pode declarar uma gramática à qual deve atender. O SOAP, sendo baseado em XML, também se beneficia disso.
Outras alternativas de serialização possuem 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 é um moderno e open source framework de alto desempenho para Remote Procedure Call (RPC) que pode ser executado em qualquer ambiente. Ele pode conectar eficientemente serviços em e entre datacenters com suporte plugável para balanceamento de carga, rastreamento, verificação de saúde e autenticação. Também é aplicável na última etapa de computação distribuída para conectar dispositivos, aplicativos móveis e navegadores a serviços de backend.
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 do cURL.
A Simple gRPC Service
A documentação 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 em 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. Vamos começar 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:
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
}
- Versão da definição Protobuf
- Pacote
- Configuração específica do Java
- Definição do serviço
- Definição da solicitação
- Definição do campo: Primeiro vem o tipo, depois o nome e finalmente a ordem.
- Definição da resposta
Vamos usar o Maven para gerar o código Java de modelo:
<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>
- Dependências de compilação
- Checar informações sobre o Sistema Operacional; usado no plugin seguinte
- Gerar 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 os endpoints gRPC com o Spring Boot:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
Aqui está a parte significativa:
@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
}
}
}
- O
grpc-server-spring-boot-starter
detecta a anotação e faz sua mágica. - Classes de referência geradas no projeto acima
- A assinatura do método permite um parâmetro
StreamObserver
. A classe vem degrpc-stub.jar
. - Obtenha a solicitação e prefixe-a para construir a mensagem de resposta.
- Execute 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:
grpcurl --plaintext localhost:9090 list #1-2
- Liste todos os serviços gRPC disponíveis sem verificação de TLS.
- Para evitar conflitos entre gRPC e outras canais, como REST, o Spring Boot usa outra porta.
ch.frankel.blog.grpc.model.HelloService #1
grpc.health.v1.Health #2
grpc.reflection.v1alpha.ServerReflection #2
- O serviço gRPC que definimos
- Dois serviços adicionais fornecidos pelo starter personalizado
Também podemos mergulhar na estrutura do serviço:
grpcurl --plaintext localhost:9090 describe ch.frankel.blog.grpc.model.HelloService
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:
grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
{
"message": "Hello John"
}
Acessando o Serviço gRPC com Ferramentas Regulares
Imagine que temos uma aplicação JavaScript do lado do cliente 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, no entanto, 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:
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:
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
}
}
}'
- Defina uma rota granular.
- Referencie o arquivo proto definido na instrução anterior.
- Serviço gRPC
- Método gRPC
Neste ponto, qualquer cliente pode fazer uma requisição HTTP ao endpoint definido. O Apache APISIX vai transcodificar a chamada para gRPC, encaminhá-la para o serviço definido, obter a resposta e transcodificá-la novamente.
curl localhost:9080/helloservice/sayhello?name=John
{"message":"Hello John"}
Em comparação com grpc-web
, a abordagem de Gateway de API permite compartilhar o arquivo proto
com um único componente: o próprio Gateway.
Benefícios da Transcodificação
Neste ponto, podemos aproveitar as capacidades do Gateway de API. Imagine que queremos um valor padrão se nenhum nome
for passado, por exemplo, Mundo
. Os desenvolvedores ficariam felizes em configurá-lo no código, mas qualquer alteração 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 do Gateway. Vamos alterar nossa rota de acordo:
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": {
...
}
}'
- Plugin genérico quando nenhum outro se encaixa
- Reescreva a requisição.
- Código Lua mágico que faz o truque
Agora, podemos executar a requisição com um argumento vazio e obter o resultado esperado:
curl localhost:9080/helloservice/sayhello?name
{"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 o Spring Boot e o grpc-server-spring-boot-starter
. No entanto, isso vem com um custo: clientes regulares não conseguem acessar o serviço. Tivemos que recorrer ao grpcurl
para testá-lo. O mesmo acontece com clientes baseados em JavaScript – ou no navegador.
Para contornar essa limitação, podemos aproveitar um Gateway de API. 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.