gRPC pour les nuls

Ce titre n’a pas pour vocation d’être provocateur mais plutôt de reprendre l’humour de cette série de livres qui proposent de rendre accessibles à tous des sujets qui peuvent paraître complexes au premier abord. Pour illustrer le concept gRPC, nous allons construire une calculatrice gRPC.

Dans gRPC, une application cliente peut appeler directement une méthode sur une application serveur d’une machine différente comme s’il s’agissait d’un objet local, ce qui vous permet de créer plus facilement des applications et des services distribués. Comme dans de nombreux systèmes RPC, gRPC est basé sur l’idée de définir un service, en spécifiant les méthodes qui peuvent être appelées à distance avec leurs paramètres et leurs types de retour. Côté serveur, le serveur implémente cette interface et exécute un serveur gRPC pour gérer les appels clients. Du côté client, le client dispose d’un stub (appelé simplement client dans certaines langues) qui fournit les mêmes méthodes que le serveur.

gRPC

Les clients et serveurs gRPC peuvent s’exécuter et se parler dans divers environnements et peuvent être écrits dans l’une des langues prises en charge par gRPC. Ainsi, par exemple, vous pouvez facilement créer un serveur gRPC dans un langage avec des clients dans différents langages. Pour rester simple, l’exemple pris ici n’utilisera que le langage Python.

Par défaut, gRPC utilise « Protocol Buffers », le mécanisme open source mature de Google pour sérialiser des données structurées (bien qu’il puisse être utilisé avec d’autres formats de données tels que JSON). La première étape lorsque vous travaillez avec « Protocol Buffers » est de définir la structure des données que vous souhaitez sérialiser dans un fichier « proto ». Il s’agit d’un fichier texte ordinaire avec une extension .proto. Pour notre exemple, voici le fichier Calc.proto utilisé pour définir notre calculatrice.

syntax = "proto3";
 
package calc;
 
service Calculator {
rpc Add (AddRequest) returns (AddReply) {}
rpc Substract (SubstractRequest) returns (SubstractReply) {}
rpc Multiply (MultiplyRequest) returns (MultiplyReply) {}
rpc Divide (DivideRequest) returns (DivideReply) {}
}
 
message AddRequest{
int32 n1=1;
int32 n2=2;
}
 
message AddReply{
int32 n1=1;
}
 
message SubstractRequest{
int32 n1=1;
int32 n2=2;
}
 
message SubstractReply{
int32 n1=1;
}
 
message MultiplyRequest{
int32 n1=1;
int32 n2=2;
}
 
message MultiplyReply{
int32 n1=1;
}
 
message DivideRequest{
int32 n1=1;
int32 n2=2;
}
 
message DivideReply{
float f1=1;
}

Outre le fait d’indiquer que nous utilisons la version 3 de « protocol buffers », on commence par définir les services RPC (les quatre opérations arithmétiques) de manière à ce que le compilateur de « protocol buffers » génère le code et les stubs d’interface de service dans la langue choisie. Ainsi, par exemple, on peut définir un service RPC avec une méthode qui prend le message AddRequest et renvoie le message AddReply, avec une définition pour chacun des messages dans le reste du fichier.

Après avoir mis en place un environnement virtuel Python, il convient d’installer les deux packages grpcio et grpcio-tools.

gRPC utilise protoc pour générer du code à partir de notre fichier « proto ». Vous obtenez les fichiers Calc_pb2.py et Calc_pb2_grpc.py.

python -m grpc.tools.protoc  --python_out=. --grpc_python_out=. --proto_path=. Calc.proto

Les fichiers Python générés par le compilateur sont utilisés pour construire le script serveur et le script client.

#!/usr/bin/env python
from concurrent import futures
import time
import grpc
import Calc_pb2
import Calc_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


class Calculator(Calc_pb2_grpc.CalculatorServicer):

    def Add(self, request, context):
        return Calc_pb2.AddReply(n1=request.n1 + request.n2)

    def Substract(self, request, context):
        return Calc_pb2.SubstractReply(n1=request.n1 - request.n2)

    def Multiply(self, request, context):
        return Calc_pb2.MultiplyReply(n1=request.n1 * request.n2)

    def Divide(self, request, context):
        return Calc_pb2.DivideReply(f1=request.n1 / request.n2)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    Calc_pb2_grpc.add_CalculatorServicer_to_server(Calculator(), server)
    server.add_insecure_port('[::]:50050')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()
#!/usr/bin/env python
from __future__ import print_function
import grpc
import Calc_pb2
import Calc_pb2_grpc


def run():
    channel = grpc.insecure_channel('localhost:50050')
    stub = Calc_pb2_grpc.CalculatorStub(channel)
    response = stub.Add(Calc_pb2.AddRequest(n1=20, n2=10))
    print(response.n1)
    response = stub.Substract(Calc_pb2.SubstractRequest(n1=20, n2=10))
    print(response.n1)
    response = stub.Multiply(Calc_pb2.MultiplyRequest(n1=20, n2=10))
    print(response.n1)
    response = stub.Divide(Calc_pb2.DivideRequest(n1=20, n2=10))
    print(response.f1)


if __name__ == '__main__':
    run()

Côté serveur, on importe les librairies et on définit une classe contenant les méthodes pour chaque opération arithmétique. On fait appel au code qui avait été généré par le compilateur. Une boucle attend les requêtes venant du client.

Côté client, on importe les librairies, on appelle chacune des opérations arithmétiques et on leur passe les valeurs n1=20 et n2=10 à traiter.

En lançant le script serveur dans une session terminal et le script client dans une autre, vous obtenez le résultat ci-dessous.

calc
Proto Response