gRPC côté client

La plupart des composants de communication inter-systèmes qui utilisent REST sérialisent leur cargaison en JSON. À l’heure actuelle, JSON manque d’un standard de validation de schéma largement utilisé : JSON Schema n’est pas répandu. La validation de schéma standard permet de déléguer la validation à une bibliothèque tierce et de s’en tenir là. Sans cela, nous devons recourir à une validation manuelle dans le code. Pire encore, nous devons garder le code de validation synchronisé avec le schéma.

XML dispose de la validation de schéma prête à l’emploi : un document XML peut déclarer une grammaire à laquelle il doit se conformer. SOAP, étant basé sur XML, en bénéficie également.

D’autres alternatives de sérialisation offrent une option de validation de schéma : par exemple, Avro, Kryo, et Protocol Buffers. Assez intéressant, gRPC utilise Protobuf pour offrir des RPC entre composants distribués :

gRPC est un framework RPC moderne, open source et hautes performances qui peut fonctionner dans n’importe quel environnement. Il peut connecter efficacement les services dans et entre les centres de données avec un support extensible pour la mise en balance de charge, le suivi, les contrôles de santé et l’authentification. Il est également applicable dans le dernier kilomètre du calcul distribué pour connecter les appareils, les applications mobiles et les navigateurs aux services principaux.

Pourquoi gRPC?

De plus, le protocole est un mécanisme de sérialisation binaire, ce qui permet de réduire considérablement la bande passante utilisée. Par conséquent, gRPC est une excellente option pour la communication inter-systèmes. Mais si tous vos composants utilisent gRPC, comment des clients simples peuvent-ils les appeler? Dans cet article, nous allons créer un service gRPC et montrer comment l’appeler à partir de cURL.

A Simple gRPC Service

La documentation gRPC est très détaillée, voici donc un résumé:

  • gRPC est un cadre de Communication de Procédure Distante.
  • Il est compatible avec une grande variété de langages.
  • Il repose sur 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.

  • Il fait partie du portefeuille CNCF et est actuellement en phase d’incubation.

Mettons en place notre service gRPC. Nous utiliserons Java, Kotlin, Spring Boot et un projet d’intégration spécifique à gRPC pour Spring Boot. La structure du projet comprend deux projets : un pour le modèle et un pour le code. Commençons par le projet modèle.

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. Version de la définition Protobuf
  2. Package
  3. Configuration spécifique Java
  4. Définition du service
  5. Définition de la requête
  6. Définition du champ : d’abord le type, puis le nom, et enfin, l’ordre.
  7. Définition de la réponse

Nous utiliserons Maven pour générer le code Java de base:

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. Dépendances au moment de la compilation
  2. Récupérer des informations sur le Système d’Exploitation ; utilisé dans le plugin suivant
  3. Générer le code Java à partir du fichier proto.

Après la compilation, la structure devrait ressembler à peu près à ce qui suit :

Nous pouvons empaqueter les classes dans un JAR et l’utiliser dans un projet d’application web. Ce dernier est en Kotlin, mais uniquement parce que c’est ma langue JVM préférée.

Nous avons seulement besoin d’une dépendance spécifique de Spring Boot starter pour intégrer les points de terminaison gRPC avec Spring Boot :

XML

 

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

Voici l’élément significatif :

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. Le grpc-server-spring-boot-starter détecte l’annotation et fait son travail.
  2. Classes de référence générées dans le projet ci-dessus
  3. La signature de la méthode permet un paramètre StreamObserver. La classe provient de grpc-stub.jar
  4. Obtenir la requête et la préfixer pour construire le message de réponse.
  5. Jouer les événements. 

Nous pouvons maintenant démarrer l’application web avec ./mvnw spring-boot:run.

Tester le service gRPC

L’idée derrière cet article est que l’accès au service gRPC avec des outils réguliers est impossible. Pour tester, nous avons besoin d’un outil dédié, cependant. J’ai trouvé grpcurl. Installons-le et utilisons-le pour lister les services disponibles :

Shell

 

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

  1. Lister tous les services gRPC disponibles sans vérification TLS. 
  2. Pour éviter les conflits entre gRPC et d’autres canaux, par exemple REST, Spring Boot utilise un autre port.
Plain Text

 

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

  1. Le service gRPC que nous avons défini
  2. Deux services supplémentaires fournis par le starter personnalisé

Nous pouvons également plonger dans la structure du service :

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 );
}

Enfin, nous pouvons appeler le service avec des données:

Shell

 

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

 

{
  "message": "Hello John"
}

Accéder au service gRPC avec des outils réguliers

Imaginons que nous ayons une application client-side JavaScript classique qui doit accéder au service gRPC. Quelles seraient les alternatives?

L’approche générale se fait via grpc-web:

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

Les clients gRPC-web se connectent aux services gRPC via un proxy spécial; par défaut, gRPC-web utilise Envoy.

À l’avenir, nous nous attendons à ce que gRPC-web soit pris en charge dans les frameworks web spécifiques aux langages pour des langages tels que Python, Java et Node. Pour plus de détails, consultez le plan de route.

grpc-web

La description mentionne une limitation unique : elle fonctionne uniquement pour JavaScript (pour le moment). Cependant, il y en a une autre. Elle est assez intrusive. Vous devez obtenir le fichier proto, générer du code de base, et faire en sorte que votre code l’appelle. Vous devez le faire pour chaque type de client. Pire encore, si le fichier proto change, vous devez regénérer le code client dans chacun d’eux.

Une alternative existe cependant, si vous utilisez un API Gateway. Je vais décrire comment le faire avec Apache APISIX, mais peut-être que d’autres gateways peuvent faire de même. grpc-transcode est un plugin qui permet de transcoder des appels REST en gRPC et vice versa.

La première étape consiste à enregistrer le fichier proto dans 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)\" }"

Le deuxième étape consiste à créer une route avec le plugin mentionné ci-dessus :

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. Définir une route granulaire.
  2. Référencer le fichier proto défini dans la commande précédente.
  3. Service gRPC
  4. Méthode gRPC

À ce stade, n’importe quel client peut effectuer une requête HTTP vers le point de terminaison défini. Apache APISIX transcodera l’appel en gRPC, le transmettra au service défini, obtiendra la réponse et la transcodera à nouveau.

Shell

 

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

 

{"message":"Hello John"}

Comparé à grpc-web, l’approche par passerelle d’API permet de partager le fichier proto avec un seul composant : la passerelle elle-même.

Avantages du Transcodage

À ce stade, nous pouvons exploiter les capacités de la passerelle d’API. Imaginons que nous voulions une valeur par défaut si aucun nom n’est transmis, par exemple, World. Les développeurs seraient ravis de le définir dans le code, mais tout changement de valeur nécessiterait une compilation complète et un déploiement. Les changements peuvent être presque instantanés si nous plaçons la valeur par défaut dans la chaîne de traitement des routes de la passerelle. Modifions notre route en conséquence :

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 générique pour tout usage lorsqu’aucun autre ne convient
  2. Réécrire la requête.
  3. Code Lua magique qui fait l’affaire

Maintenant, nous pouvons exécuter la requête avec un argument vide et obtenir le résultat attendu :

Shell

 

curl localhost:9080/helloservice/sayhello?name
JSON

 

{"message":"Hello World"}

Conclusion

Dans ce post, nous avons brièvement décrit gRPC et comment il bénéficie à la communication inter-services. Nous avons développé un service gRPC simple à l’aide de Spring Boot et du grpc-server-spring-boot-starter. Cela a un coût, cependant : les clients réguliers ne peuvent pas accéder au service. Nous avons dû recourir au grpcurl pour le tester. Le même problème se pose pour les clients basés sur JavaScript – ou le navigateur.

Pour contourner cette limitation, nous pouvons exploiter un API Gateway. J’ai démontré comment configurer Apache APISIX avec le plugin grpc-transcode pour obtenir le résultat souhaité.

Le code source complet de ce post est disponible sur GitHub.

Pour aller plus loin

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