Premiers pas avec gRPC-Java

1. Introduction

Dans cet atelier de programmation, vous allez utiliser gRPC-Java pour créer un client et un serveur qui constituent la base d'une application de cartographie d'itinéraires écrite en Java.

À la fin de ce tutoriel, vous disposerez d'un client qui se connecte à un serveur distant à l'aide de gRPC pour obtenir le nom ou l'adresse postale d'un lieu situé à des coordonnées spécifiques sur une carte. Une application complète peut utiliser cette conception client-serveur pour énumérer ou résumer les points d'intérêt le long d'un itinéraire.

L'API du serveur est définie dans un fichier Protocol Buffers, qui sera utilisé pour générer du code passe-partout pour le client et le serveur afin qu'ils puissent communiquer entre eux. Vous gagnerez ainsi du temps et des efforts pour implémenter cette fonctionnalité.

Ce code généré gère non seulement les complexités de la communication entre le serveur et le client, mais aussi la sérialisation et la désérialisation des données.

Points abordés

  • Utiliser Protocol Buffers pour définir une API de service.
  • Comment créer un client et un serveur basés sur gRPC à partir d'une définition Protocol Buffers à l'aide de la génération de code automatisée.
  • Comprendre la communication client-serveur avec gRPC.

Cet atelier de programmation s'adresse aux développeurs Java qui découvrent gRPC ou qui souhaitent se rafraîchir la mémoire sur gRPC, ou à toute personne intéressée par la création de systèmes distribués. Aucune expérience préalable avec gRPC n'est requise.

2. Avant de commencer

Prérequis

  • JDK version 8 ou ultérieure

Obtenir le code

Pour que vous n'ayez pas à partir de zéro, cet atelier de programmation fournit un échafaudage du code source de l'application que vous devez compléter. Les étapes suivantes vous montreront comment finaliser l'application, y compris en utilisant les plug-ins du compilateur de tampon de protocole pour générer le code gRPC standard.

Commencez par créer le répertoire de travail de l'atelier de programmation et accédez-y :

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Téléchargez et extrayez l'atelier de programmation :

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

Vous pouvez également télécharger le fichier .zip contenant uniquement le répertoire de l'atelier de programmation et le décompresser manuellement.

Le code source complet est disponible sur GitHub si vous ne souhaitez pas saisir d'implémentation.

3. Définir le service

La première étape consiste à définir le service gRPC de l'application, sa méthode RPC, ainsi que ses types de messages de requête et de réponse à l'aide de Protocol Buffers. Votre service fournira :

  • Méthode RPC appelée GetFeature que le serveur implémente et que le client appelle.
  • Les types de messages Point et Feature sont des structures de données échangées entre le client et le serveur lors de l'utilisation de la méthode GetFeature. Le client fournit des coordonnées cartographiques sous la forme d'un Point dans sa requête GetFeature au serveur, et le serveur répond avec un Feature correspondant qui décrit ce qui se trouve à ces coordonnées.

Cette méthode RPC et ses types de messages seront tous définis dans le fichier src/main/proto/routeguide/route_guide.proto du code source fourni.

Les tampons de protocole sont communément appelés "protobufs". Pour en savoir plus sur la terminologie gRPC, consultez Concepts fondamentaux, architecture et cycle de vie de gRPC.

Comme nous générons du code Java dans cet exemple, nous avons spécifié une option de fichier java_package et un nom pour la classe Java dans notre .proto :

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Types de messages

Dans le fichier routeguide/route_guide.proto du code source, définissez d'abord le type de message Point. Un Point représente une paire de coordonnées (latitude et longitude) sur une carte. Pour cet atelier de programmation, utilisez des nombres entiers pour les coordonnées :

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Les nombres 1 et 2 sont des ID uniques pour chacun des champs de la structure message.

Ensuite, définissez le type de message Feature. Un Feature utilise un champ string pour le nom ou l'adresse postale d'un élément à un emplacement spécifié par un Point :

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Méthode de service

Le fichier route_guide.proto possède une structure service nommée RouteGuide qui définit une ou plusieurs méthodes fournies par le service de l'application.

Ajoutez la méthode rpc GetFeature dans la définition RouteGuide. Comme expliqué précédemment, cette méthode recherche le nom ou l'adresse d'un lieu à partir d'un ensemble de coordonnées donné. Par conséquent, faites en sorte que GetFeature renvoie un Feature pour un Point donné :

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Il s'agit d'une méthode RPC unaire : un RPC simple où le client envoie une requête au serveur et attend une réponse, comme pour un appel de fonction local.

4. Générer le code client et serveur

Ensuite, nous devons générer les interfaces client et serveur gRPC à partir de notre définition de service .proto. Pour ce faire, nous utilisons le compilateur de tampon de protocole protoc avec un plug-in gRPC Java spécial. Vous devez utiliser le compilateur proto3 (qui est compatible avec les syntaxes proto2 et proto3) pour générer des services gRPC.

Lorsque vous utilisez Gradle ou Maven, le plug-in de compilation protoc peut générer le code nécessaire lors de la compilation. Pour savoir comment générer du code à partir de vos propres fichiers .proto, consultez le fichier README de grpc-java.

Nous avons fourni un environnement et une configuration Gradle dans le code source de l'atelier de programmation pour compiler ce projet.

Exécutez la commande suivante dans le répertoire grpc-java-getting-started :

$ chmod +x gradlew
$ ./gradlew generateProto

Les classes suivantes sont générées à partir de notre définition de service :

  • Feature.java, Point.java et d'autres qui contiennent tout le code du tampon de protocole pour remplir, sérialiser et récupérer nos types de messages de requête et de réponse.
  • RouteGuideGrpc.java qui contient (avec d'autres codes utiles) une classe de base pour l'implémentation des serveurs RouteGuide, RouteGuideGrpc.RouteGuideImplBase, avec toutes les méthodes définies dans les classes de service et de bouchon RouteGuide que les clients peuvent utiliser.

5. Implémenter le serveur

Commençons par examiner comment créer un serveur RouteGuide. Pour que notre service RouteGuide fonctionne correctement, deux éléments sont nécessaires :

  • Implémenter l'interface de service générée à partir de notre définition de service, qui effectue le "travail" réel de notre service.
  • Exécuter un serveur gRPC pour écouter les requêtes des clients et les distribuer à l'implémentation de service appropriée.

Implémenter RouteGuide

Comme vous pouvez le voir, notre serveur possède une classe RouteGuideService qui étend la classe abstraite RouteGuideGrpc.RouteGuideImplBase générée :

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

Nous vous fournissons les deux fichiers suivants pour initialiser votre serveur avec des fonctionnalités :

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Examinons en détail une implémentation RPC simple.

RPC unaire

RouteGuideService implémente toutes nos méthodes de service. Dans ce cas, il s'agit simplement de GetFeature(), qui prend un message Point du client et renvoie dans un message Feature les informations de localisation correspondantes à partir d'une liste de lieux connus.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

La méthode getFeature() comporte deux paramètres :

  • Point : la requête.
  • StreamObserver<Feature> : observateur de réponse, qui est une interface spéciale permettant au serveur d'appeler avec sa réponse.

Pour renvoyer notre réponse au client et mettre fin à l'appel :

  1. Nous construisons et remplissons un objet de réponse Feature à renvoyer au client, comme spécifié dans notre définition de service. Dans cet exemple, nous effectuons cette opération dans une méthode privée checkFeature() distincte.
  2. Nous utilisons la méthode onNext() de l'observateur de réponse pour renvoyer le Feature.
  3. Nous utilisons la méthode onCompleted() de l'observateur de réponse pour indiquer que nous avons terminé de traiter le RPC.

Démarrer le serveur

Une fois que nous avons implémenté toutes nos méthodes de service, nous devons démarrer un serveur gRPC pour que les clients puissent réellement utiliser notre service. Nous incluons dans notre code récurrent la création de l'objet ServerBuilder :

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Nous créons le service dans le constructeur :

  1. Spécifiez le port que nous souhaitons utiliser pour écouter les requêtes client à l'aide de la méthode forPort() du générateur (il utilisera l'adresse générique).
  2. Créez une instance de notre classe d'implémentation de service RouteGuideService et transmettez-la à la méthode addService() du générateur.
  3. Appelez build() sur le générateur pour créer un serveur RPC pour notre service.

L'extrait de code suivant montre comment créer un objet ServerBuilder.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

L'extrait de code suivant montre comment créer un objet serveur pour notre service RouteGuide.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Implémentez une méthode de démarrage qui appelle start sur le serveur que nous avons créé ci-dessus.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Implémentez une méthode pour attendre la fin de l'exécution du serveur afin qu'il ne quitte pas immédiatement.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

Comme vous pouvez le voir, nous créons et démarrons notre serveur à l'aide d'un ServerBuilder.

Dans la méthode principale, nous :

  1. Créez une instance RouteGuideServer.
  2. Appelez start() pour activer un serveur RPC pour notre service.
  3. Attendez que le service soit arrêté en appelant blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Créer le client

Dans cette section, nous allons créer un client pour notre service RouteGuide.

Instancier un stub

Pour appeler des méthodes de service, nous devons d'abord créer un stub. Il existe deux types de stubs, mais nous n'avons besoin que du stub bloquant pour cet atelier de programmation. Il existe deux types de comptes :

  • un stub bloquant/synchrone qui effectue un appel RPC et attend la réponse du serveur, puis renvoie une réponse ou génère une exception.
  • un stub non bloquant/asynchrone qui effectue des appels non bloquants au serveur, où la réponse est renvoyée de manière asynchrone. Vous ne pouvez passer certains types d'appels de streaming qu'en utilisant le stub asynchrone.

Nous devons d'abord créer un canal gRPC, puis l'utiliser pour créer notre stub.

Nous aurions pu utiliser un ManagedChannelBuilder directement pour créer le canal.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Utilisons plutôt une méthode utilitaire qui accepte une chaîne avec hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Nous pouvons maintenant utiliser le canal pour créer notre stub de blocage. Pour cet atelier de programmation, nous n'avons que des RPC bloquants. Nous utilisons donc la méthode newBlockingStub fournie dans la classe RouteGuideGrpc que nous avons générée à partir de notre .proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Appeler les méthodes de service

Voyons maintenant comment appeler les méthodes de service.

RPC simple

L'appel du RPC simple GetFeature est presque aussi simple que l'appel d'une méthode locale.

Nous créons et remplissons un objet de tampon de protocole de requête (Point dans notre cas), le transmettons à la méthode getFeature() sur notre stub de blocage et récupérons un Feature.

Si une erreur se produit, elle est encodée en tant que Status, que nous pouvons obtenir à partir de StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

Le boilerplate enregistre un message contenant le contenu selon qu'une fonctionnalité était présente ou non au point spécifié.

7. Essayez !

  1. Exécutez la commande suivante dans le répertoire start_here :
$ ./gradlew installDist

Cela compilera votre code, l'empaquettera dans un fichier JAR et créera les scripts qui exécutent l'exemple. Ils seront créés dans le répertoire build/install/start_here/bin/. Les scripts sont les suivants : route-guide-server et route-guide-client.

Le serveur doit être en cours d'exécution avant le démarrage du client.

  1. Exécutez le serveur :
$ ./build/install/start_here/bin/route-guide-server
  1. Exécutez le client :
$ ./build/install/start_here/bin/route-guide-client

Un résultat semblable à celui-ci s'affiche (les codes temporels sont omis pour plus de clarté) :

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. Étape suivante