Tutoriel sur gRPC

imt

1. Objectif

L’objectif de ce tutoriel est de vous introduire les concepts de base pour utiliser gRPC. Nous allons pour cela travailler sur le service Movie précédemment codé par une interface REST/OpenAPI.

Note
Pour tous les détails sur gRPC suivez ce lien https://grpc.io/
Important
Tout comme pour Flask merci de vous référer au document/page d’installation avant de continuer.

Téléchargez le contenu du repository git suivant : https://github.com/IMTA-FIL/UE-AD-A1-GRPC

Le contenu de ce repository sera votre espace de travail pour ce tutoriel et votre TP sur gRPC. Il contient un répertoire par service à implémenter dans le TP, dont un répertoire pour le service Movie qui nous intéresse ici. Chaque répertoire de service contient les données json qui lui sont associées.

Le repository, comme indiqué dans le guide d’installation, contient également un fichier requirements.txt racine utile pour installer les dépendances nécessaires (à savoir ici les bibliothèques Flask et requests). Il contient enfin les fichiers nécessaires à la construction de l’environnement Docker à savoir un fichier docker-compose.yaml et dans chaque répertoire un fichier requirements.txt et un Dockerfile.

2. Protocol buffers et génération des stubs

En plus des répertoires par service nous avons aussi un répertoire Client permettant de tester notre service Movie.

2.1. Création d’une première interface avec Protocol Buffers

Voici l’interface de base Protocol Buffers que nous allons utiliser. Elle se trouve à la fois côté client et côté serveur dans movie/protos/movie.proto et client/protos/movie.proto.

syntax = "proto3";

service Movie {
    rpc GetMovieByID(MovieID) returns (MovieData) {}
}

message MovieID {
    string id = 1;
}

message MovieData {
    string title = 1;
    float rating = 2;
    string director = 3;
    string id = 4;
}

Ce fichier représente le contrat entre le serveur et le client sur l’API à utiliser.

Nous y indiquons la version de Protocol Buffers utilisée, ici proto3.

Nous définissons ensuite notre service Movie qui va contenir une seule procédure distante, identifiée par le mot rpc : GetMovieByID. Cette procédure ou méthode prend en entrée un message de type MovieID et retourne un message de type MovieData.

Enfin nous définissons les types de messages utilisés par les procédures rpc. Ici MovieID est un message contenant simplement un identifiant de type string. Et MovieData est un message contenant les éléments qui constituent un film dans notre fichier JSON : un titre de type string, une note de type float, un directeur de typ string, et enfin l’identifiant du film de type string (comme pour MovieID). Les valeurs entières données à chaque élément du message servent au compilateur proto pour identifier et ordonner les éléments constituants le message.

2.2. Compilation du fichier movie.proto

Pour compiler ce fichier nous devons lancer la commande suivante (dans l’environnement virtuel Python si vous en avez créé un) :

python -m grpc_tools.protoc -I=./protos --python_out=. --grpc_python_out=. movie.proto
  • -I= précise le répertoire source qui contient les fichiers proto

  • --python_out= and --grpc_python_out= précisent le répertoire de destination des fichiers Python générés

  • enfin, le fichier proto à compiler est indiqué

Note
Pour plus de détail sur le langage Protocol Buffers voir https://developers.google.com/protocol-buffers/docs/pythontutorial

2.3. Analyse des fichiers générés par la compilation

Deux fichiers ont été générés suite à cette compilation du fichier movie.proto

  • movie_pb2_grpc.py : ce fichier contient les éléments relatifs aux stub du client et du serveur

  • movie_pb2.py : ce fichier contient le code relatif aux types de messages et le marshalling et unmarshalling des informations (nous ne regarderons pas plus en détail ce fichier)

Avec gRPC le stub du client est appelé le stub alors que le stub du serveur est appelé le servicer.

2.3.1. stub client

Une classe est créée pour le stub. Ici elle se nomme MovieStub et notre client devra hériter de cette classe. Elle contient un constructeur qui prend en entrée un channel gRPC pour créer ses connexions avec le serveur. Pour chaque méthode/procédure du service précisé dans le fichier proto initial, un attribut portant le même nom est ajouté à l’objet MovieStub. Ce sont ces attributs que notre client devra appeler pour effectuer des appels distants au service. L’attribut créé une connexion avec le serveur en utilisant l’input channel (4 types possibles unaryunary, unarystream, streamunary et streamstream).

2.3.2. servicer serveur

Pour chaque service, une classe servicer est créée (ici une seule nommée MovieServicer). Pour chaque rpc indiqué dans le fichier proto initial une méthode est ajoutée dans la classe MovieServicer. Notre service devra hériter de cette classe, et chaque méthode devra être surchargée dans l’implémentation du service.

2.3.3. fonction d’enregistrement

Pour chaque service (ici un seul) une fonction pour enregistrer un servicer qui implémente le serveur est générée. Ici cette fonction est add_MovieServicer_to_server.

3. Exemple d’écriture du service Movie

Nous éditons le fichier movie/movie.py.

3.1. Création et lecture des données JSON

Tout comme pour le TP sur REST nous allons utiliser le fichier movie.json disponible sur Moodle et le placer dans un répertoire data.

Le code ci-dessous permet la lecture d’un fichier JSON. L’objet movies récupéré est obtenu en lisant la clé "movies" et est donc un array.

import json

with open('{}/data/movies.json'.format("."), "r") as jsf:
   movies = json.load(jsf)["movies"]

3.2. Implémentation du servicer

Nous allons maintenant devoir fournir une implémentation de l’objet MovieServicer généré.

Tout d’abord nous devons importer les deux fichiers générés movie_pb2 et movie_pb2_grpc.

Nous créons ensuite une classe qui hérite de l’objet movie_pb2_grpc.MovieServicer généré. Dans cette classe nous surchargeons le constructeur où nous lisons le fichier json qui nous servira de base de données.

import json
import grpc
from concurrent import futures
import movie_pb2
import movie_pb2_grpc

class MovieServicer(movie_pb2_grpc.MovieServicer):

    def __init__(self):
        with open('{}/data/movies.json'.format("."), "r") as jsf:
            self.db = json.load(jsf)["movies"]

Nous implémentons ensuite la méthode GetMovieByID. Cette fonction parcourt les films de la base de données. Si l’identifiant fournit dans la requête est trouvé il est retourné par le serveur dans sa réponse à l’appel de procédure distant. Pour cela le descripteur de fichier movie_pb2.MovieData est utilisé pour construire le message à retourner.

class MovieServicer(movie_pb2_grpc.MovieServicer):

   ...

    def GetMovieByID(self, request, context):
        for movie in self.db:
            if movie['id'] == request.id:
                print("Movie found!")
                return movie_pb2.MovieData(title=movie['title'], rating=movie['rating'], director=movie['director'], id=movie['id'])
        return movie_pb2.MovieData(title="", rating="", director="", id="")
Note
une erreur s’est glissée dans le code ci-dessus à cause du type attendu pour rating, il faut corriger ça !

Enfin nous devons créer une fonction main qui va se charger de créer le serveur gRPC et enregistrer la classe MovieServicer à ce serveur. Nous ouvrons ici le port 3001.

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    movie_pb2_grpc.add_MovieServicer_to_server(MovieServicer(), server)
    server.add_insecure_port('[::]:3001')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

4. Exemple d’écriture du client

Pour implémenter le client il n’y a pas de classe à implémenter, il faut en revanche initier le stub en créant une connexion avec le serveur et faire appel au stub lors de l’appel de procédures distantes.

Nous ouvrons le fichier client/client.py.

Tout d’abord nous devons importer les éléments nécessaires pour coder le client, à savoir grpc, ainsi que les deux fichiers générés par la compilation.

import grpc
import movie_pb2
import movie_pb2_grpc

Ici nous commençons par une fonction main. Nous créons une connexion avec l’hôte localhost:3001 et utilisons ce channel pour créer le stub movie_pb2_grpc.MovieStub(channel). Puis nous appelons la fonction interne get_movie_by_id.

Note
Si vous utilisez docker-compose pour déployer votre code alors il faut utiliser le nom du service et non pas localhost puisqu’un réseau est créé entre les conteneurs et des noms sont donnés aux adresses des services par docker-compose`.

Cette fonction prend en paramètres le stub et le message d’entrée de l’appel distant que nous créons au moyen du descripteur movie_pb2.MovieID.

def run():
    with grpc.insecure_channel('localhost:3001') as channel:
        stub = movie_pb2_grpc.MovieStub(channel)

        print("-------------- GetMovieByID --------------")
        movieid = movie_pb2.MovieID(id = "a8034f44-aee4-44cf-b32c-74cf452aaaae")
        get_movie_by_id(stub, movieid)


if __name__ == '__main__':
    run()
Note
Pour en savoir plus sur les channels en gRPC voir la documentation https://grpc.github.io/grpc/python/grpc.html

Nous allons maintenant implémenter la fonction get_movie_by_id. Elle va tout simplement appeler la fonction du stub GetMovieByID en utilisant le message donné en entrée de type movie_pb2.MovieID. Le message de réponse est stocké dans la variable movie et est affichée.

def get_movie_by_id(stub,id):
    movie = stub.GetMovieByID(id)
    print(movie)

5. Exécution de nos services

Nous lançons le serveur tout d’abord.

cd path/movie
python movie.py

Puis le client.

cd path/client
python client.py

Nous devrions observer cette sortie côté client, c’est-à-dire les informations relatives au film ayant l’identifiant a8034f44-aee4-44cf-b32c-74cf452aaaae en entrée de l’appel de procédure distante :

-------------- GetMovieByID --------------
title: "The Martian"
rating: 8.199999809265137
director: "Ridley Scott"
id: "a8034f44-aee4-44cf-b32c-74cf452aaaae"

Et cette sortie côté serveur :

Movie found!

6. Modification de l’API

Nous allons maintenant ajouter une fonction à notre API RPC. Voici le nouveau fichier movie.proto qui est bien entendu à modifier côté serveur et côté client puisqu’il représente le contrat de l’API.

syntax = "proto3";

service Movie {
    rpc GetMovieByID(MovieID) returns (MovieData) {}
    rpc GetListMovies(Empty) returns (stream MovieData) {}
}

message MovieID {
    string id = 1;
}

message MovieData {
    string title = 1;
    float rating = 2;
    string director = 3;
    string id = 4;
}

message Empty {
}

Nous ajoutons donc une procédure distante GetListMovies qui prend un message d’entrée vide de type Empty et qui retourne un stream de messages de type MovieData.

Nous allons regénérer les fichiers movie_pb2 et movie_pb2_grpc permettant d’abstraire l’appel de procédure distant dans le code de notre serveur et notre client.

Tip
Vous pouvez soit exécuter la commande des 2 côtés, soit copier/coller les fichiers générés côté serveur et côté client.
python -m grpc_tools.protoc -I=./protos --python_out=. --grpc_python_out=. movie.proto

Jetez un coup d’œil aux nouveaux fichiers générés qui doivent inclure le nouvel appel RPC possible dans notre API !

Nous n’avons plus qu’à compléter notre serveur et notre client.

Voici le code de notre méthode GetListMovies dans la classe MovieServicer :

def GetListMovies(self, request, context):
    for movie in self.db:
        yield movie_pb2.MovieData(title=movie['title'], rating=movie['rating'], director=movie['director'], id=movie['id'])

Nous parcourons les films de la base de données, nous créons un message par film de type MovieData et au lieu de retourner notre message nous utilisons le mot clé yield qui va permettre à gRPC de mettre en place le stream de messages.

Voici maintenant le code de notre client auquel nous avons ajouté une fonction interne get_list_movies ne prenant comme entrée que le stub.

def get_list_movies(stub):
    allmovies = stub.GetListMovies(movie_pb2.Empty())
    for movie in allmovies:
        print("Movie called %s" % (movie.title))

Dans cette fonction nous faisons l’appel de procédure distante en utilisant le stub initialisé et en créant un message vide de type Empty (qui a été ajouté au fichier movie_pb2). Le stream de messages est agrégé dans la variable allmovies. Nous parcourons ces éléments et les affichons par titre.

Nous devons évidemment appeler cette fonction dans le main du client :

print("-------------- GetListMovies --------------")
get_list_movies(stub)

En redémarrant le serveur et le client vous devez observer cette nouvelle sortie côté client :

-------------- GetMovieByID --------------
title: "The Martian"
rating: 8.199999809265137
director: "Ridley Scott"
id: "a8034f44-aee4-44cf-b32c-74cf452aaaae"

-------------- GetListMovies --------------
Movie called The Good Dinosaur
Movie called The Night Before
Movie called Creed
Movie called Victor Frankenstein
Movie called The Danish Girl
Movie called Spectre