Tutoriel sur gRPC
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 et GraphQL. Ce tutoriel, contrairement aux précédents, ne servira pas pour le TP sur les API mixtes car movie
restera une API GraphQL dans le TP.
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-TUTOGRPC
Le contenu de ce repository sera votre espace de travail pour ce tutoriel uniquement. Il contient un répertoire pour le service Movie
et un répertoire Client
permettant de tester notre service Movie
et surtout de vous montrer le fonctionnement d’un client dont vous aurez besoin pour le TP sur les API mixtes.
Postman
permet depuis peu de tester des API gRPC sans implémenter de client, mais il faut tout de même savoir coder un client pour le TP ! Nous verrons aussi comment utiliser Postman
.
2. Protocol buffers et génération des stubs
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 fichiersproto
-
--python_out=
and--grpc_python_out=
précisent le répertoire de destination des fichiersPython
générés -
enfin, le fichier
proto
à compiler est indiqué
Note
|
Sous Windows les années passées et en fonction des versions de gRPC il fallait parfois une commande légèrement différente. Regardez bien le message d’erreur qui indique souvent l’élément manquant ou de trop. |
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 auxstub
du client et du serveur -
movie_pb2.py
: ce fichier contient le code relatif aux types de messages et lemarshalling
etunmarshalling
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 utiliser 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 unary_unary
, unary_stream
,
stream_unary
et stream_stream
).
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. 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():
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
# used in circumstances in which the with statement does not fit the needs
# of the code.
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)
channel.close()
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
pymon 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
7. Utilisation de Postman
Au lieu d’écrire un client gRPC et pour pouvoir débugger son serveur sans coder de client, il est bon d’utiliser Postman (ou équivalent). Voici comment procéder.
Commencez par créer une nouvelle API avec le bouton "new"
Choisissez le type d’API gRPC
Dans URL si le serveur tourne il vous sera directement proposé (ici localhost:3001
)
Dans "Select a method" vous pourrez importer le fichier proto de votre serveur gRPC et alors vous seront proposées les méthodes associées. Sélectionnez ensuite une méthode.
Appuyez sur "Use Example Message" pour que Postman vous génère un message type d’un client gRPC pour la méthode sélectionnée et remplacez les valeurs
Voici un exemple de réponse obtenue