La mayoría de los componentes de comunicación entre sistemas que utilizan REST serializan su carga útil en JSON. Hasta ahora, JSON carece de un estándar de validación de esquema ampliamente utilizado: JSON Schema no está muy extendido. La validación de esquemas estándar permite delegar la validación a una biblioteca de terceros y dejarlo ahí. Sin uno, debemos recurrir a la validación manual en el código. Peor aún, debemos mantener el código de validación sincronizado con el esquema.
XML tiene validación de esquemas incorporada: un documento XML puede declarar una gramática a la que debe ajustarse. SOAP, al estar basado en XML, también se beneficia de ello.
Otras alternativas de serialización tienen una opción de validación de esquemas: por ejemplo, Avro, Kryo, y Protocol Buffers. Curiosamente, gRPC utiliza Protobuf para ofrecer RPC entre componentes distribuidos:
gRPC es un moderno marco de código abierto de alto rendimiento para llamadas a procedimientos remotos (RPC) que puede funcionar en cualquier entorno. Puede conectar de manera eficiente servicios en y entre centros de datos con soporte enchufable para equilibrio de carga, seguimiento, verificación de estado y autenticación. También es aplicable en el último tramo de la computación distribuida para conectar dispositivos, aplicaciones móviles y navegadores a servicios de backend.
Además, el Protocolo es un mecanismo de serialización binario, ahorrando mucho ancho de banda. Por lo tanto, gRPC es una excelente opción para la comunicación entre sistemas. Pero si todos tus componentes hablan gRPC, ¿cómo pueden llamar a ellos clientes simples? En esta publicación, construiremos un servicio gRPC y mostraremos cómo llamarlo desde cURL.
A Simple gRPC Service
La documentación de gRPC es exhaustiva, así que aquí hay un resumen:
- gRPC es un marco de llamadas a procedimientos remotos.
- Funciona en una amplia gama de lenguajes.
- Se basa en 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.
- Forma parte de la cartera CNCF y actualmente se encuentra en etapa de incubación.
Configuremos nuestro servicio gRPC. Usaremos Java, Kotlin, Spring Boot y un proyecto de integración específico de gRPC con Spring Boot. La estructura del proyecto contiene dos proyectos: uno para el modelo y otro para el código. Comencemos con el proyecto del 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
}
- Versión de definición de Protobuf
- Paquete
- Configuración específica de Java
- Definición del servicio
- Definición de la solicitud
- Definición del campo: Primero viene el tipo, luego el nombre y finalmente, el orden.
- Definición de la respuesta
Utilizaremos Maven para generar el código de Java de plantilla:
<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>
- Dependencias de compilación
- Obtener información sobre el Sistema Operativo; utilizado en el siguiente complemento
- Generar código Java a partir del archivo
proto
.
Después de la compilación, la estructura debería lucir algo así:
Podemos empaquetar las clases en un JAR y utilizarlo en un proyecto de aplicación web. Este último está en Kotlin, pero solo porque es mi lenguaje JVM favorito.
Solo necesitamos una dependencia específica de Spring Boot starter para integrar los endpoints gRPC con Spring Boot:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
Aquí está la 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
}
}
}
- El
grpc-server-spring-boot-starter
detecta la anotación y hace su magia. - Clases de referencia generadas en el proyecto anterior
- La firma del método permite un parámetro
StreamObserver
. La clase proviene delgrpc-stub.jar
. - Obtener la solicitud y anteponerla para construir el mensaje de respuesta.
- Reproducir los eventos.
Ahora podemos iniciar la aplicación web con ./mvnw spring-boot:run
.
Probar el Servicio gRPC
La idea detrás de esta publicación es que acceder al servicio gRPC con herramientas regulares es imposible. Para probarlo, necesitamos una herramienta dedicada, después de todo. Encontré grpcurl. Instalémoslo y utilicémoslo para listar los servicios disponibles:
grpcurl --plaintext localhost:9090 list #1-2
- Listar todos los servicios gRPC disponibles sin verificación de TLS.
- Para evitar conflictos entre gRPC y otros canales, como REST, Spring Boot utiliza otro puerto.
ch.frankel.blog.grpc.model.HelloService #1
grpc.health.v1.Health #2
grpc.reflection.v1alpha.ServerReflection #2
- El servicio gRPC que definimos
- Dos servicios adicionales proporcionados por el starter personalizado
También podemos profundizar en la estructura del servicio:
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 llamar al servicio con datos:
grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
{
"message": "Hello John"
}
Accediendo al Servicio gRPC con Herramientas Regulares
Imagina que tenemos una aplicación cliente regular en JavaScript que necesita acceder al servicio gRPC. ¿Cuáles serían las alternativas?
El enfoque general es a través de grpc-web
:
A JavaScript implementation of gRPC for browser clients. For more information, including a quick start, see the gRPC-web documentation.
Los clientes gRPC-web se conectan a los servicios gRPC a través de un proxy especial; por defecto, gRPC-web utiliza Envoy.
En el futuro, esperamos que gRPC-web sea compatible con frameworks web específicos de lenguaje para lenguajes como Python, Java y Node. Para más detalles, consulta la hoja de ruta.
– grpc-web
La descripción menciona una limitación única: solo funciona para JavaScript (hasta ahora). Sin embargo, hay otra. Es bastante intrusivo. Tienes que obtener el archivo proto
, generar código de plantilla y hacer que tu código lo llame. Debes hacerlo para cada tipo de cliente. Peor aún, si el archivo proto cambia, debes regenerar el código del cliente en cada uno de ellos.
Existe una alternativa, aunque, si estás utilizando una API Gateway. Describiré cómo hacerlo con Apache APISIX, pero quizás otras puertas de enlace puedan hacer lo mismo. grpc-transcode es un plugin que permite transcodificar llamadas REST a gRPC y viceversa.
El primer paso es registrar el archivo proto en 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)\" }"
El segundo paso es crear una ruta con el 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
}
}
}'
- Definir una ruta granular.
- Referenciar el archivo proto definido en el comando anterior.
- Servicio gRPC
- Método gRPC
En este punto, cualquier cliente puede hacer una solicitud HTTP al punto final definido. Apache APISIX transcribirá la llamada a gRPC, la reenviará al servicio definido, obtendrá la respuesta y la transcribirá nuevamente.
curl localhost:9080/helloservice/sayhello?name=John
{"message":"Hello John"}
En comparación con grpc-web
, el enfoque de API Gateway permite compartir el archivo proto
con un solo componente: el Gateway en sí.
Beneficios de la Transcodificación
En este punto, podemos aprovechar las capacidades del API Gateway. Imagina que queremos un valor predeterminado si no se pasa name
, por ejemplo, World
. Los desarrolladores estarían encantados de configurarlo en el código, pero cualquier cambio en el valor requeriría una compilación y despliegue completos. Los cambios pueden ser casi instantáneos si ponemos el valor predeterminado en la cadena de procesamiento de rutas del Gateway. Cambiemos nuestra ruta en consecuencia:
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 para todos los casos cuando ninguno encaja
- Reescribir la solicitud.
- Código Lua mágico que hace el truco
Ahora, podemos ejecutar la solicitud con un argumento vacío y obtener el resultado esperado:
curl localhost:9080/helloservice/sayhello?name
{"message":"Hello World"}
Conclusión
En este post, hemos descrito brevemente gRPC y cómo beneficia la comunicación entre servicios. Desarrollamos un simple servicio gRPC utilizando Spring Boot y grpc-server-spring-boot-starter
. Sin embargo, esto tiene un costo: los clientes regulares no pueden acceder al servicio. Tuvimos que recurrir a grpcurl
para probarlo. Lo mismo ocurre con los clientes basados en JavaScript o el navegador.
Para sortear esta limitación, podemos aprovechar un API Gateway. Demostré cómo configurar Apache APISIX con el plugin grpc-transcode
para lograr el resultado deseado.
El código fuente completo de este post se puede encontrar en GitHub.