gRPC en el lado del cliente

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.

¿Por qué gRPC?

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:

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. Versión de definición de Protobuf
  2. Paquete
  3. Configuración específica de Java
  4. Definición del servicio
  5. Definición de la solicitud
  6. Definición del campo: Primero viene el tipo, luego el nombre y finalmente, el orden.
  7. Definición de la respuesta

Utilizaremos Maven para generar el código de Java de plantilla:

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. Dependencias de compilación
  2. Obtener información sobre el Sistema Operativo; utilizado en el siguiente complemento
  3. 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:

XML

 

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

Aquí está la parte significativa:

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. El grpc-server-spring-boot-starter detecta la anotación y hace su magia.
  2. Clases de referencia generadas en el proyecto anterior
  3. La firma del método permite un parámetro StreamObserver. La clase proviene del grpc-stub.jar
  4. Obtener la solicitud y anteponerla para construir el mensaje de respuesta.
  5. 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:

Shell

 

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

  1. Listar todos los servicios gRPC disponibles sin verificación de TLS. 
  2. Para evitar conflictos entre gRPC y otros canales, como REST, Spring Boot utiliza otro puerto.
Plain Text

 

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

  1. El servicio gRPC que definimos
  2. Dos servicios adicionales proporcionados por el starter personalizado

También podemos profundizar en la estructura del servicio:

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 llamar al servicio con datos:

Shell

 

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

 

{
  "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:

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)\" }"

El segundo paso es crear una ruta con el 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. Definir una ruta granular.
  2. Referenciar el archivo proto definido en el comando anterior.
  3. Servicio gRPC
  4. 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.

Shell

 

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

 

{"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:

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 para todos los casos cuando ninguno encaja
  2. Reescribir la solicitud.
  3. Código Lua mágico que hace el truco

Ahora, podemos ejecutar la solicitud con un argumento vacío y obtener el resultado esperado:

Shell

 

curl localhost:9080/helloservice/sayhello?name
JSON

 

{"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.

Para profundizar

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