Premiers pas avec gRPC-Python

1. Introduction

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

À 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.

Le service est défini 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 Python 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

  • Python 3.9 ou version ultérieure. Nous vous recommandons Python 3.13. Pour obtenir des instructions d'installation spécifiques à la plate-forme, consultez Configuration et utilisation de Python. Vous pouvez également installer un Python non système à l'aide d'outils tels que uv ou pyenv.
  • pip pour installer les packages Python.
  • venv pour créer des environnements virtuels Python.

Les packages ensurepip et venv font partie de la bibliothèque standard Python et sont généralement disponibles par défaut.

Toutefois, certaines distributions basées sur Debian (y compris Ubuntu) choisissent de les exclure lors de la redistribution de Python. Pour installer les packages, exécutez la commande suivante :

sudo apt install python3-pip python3-venv

Obtenir le code

Pour faciliter votre apprentissage, cet atelier de programmation propose une structure de code source prédéfinie pour vous aider à démarrer. Les étapes suivantes vous guideront dans la création de l'application, y compris la génération de code gRPC à l'aide du plug-in du compilateur Protocol Buffer grpc_tools.protoc.

grpc-codelabs

Le code source du scaffold pour cet atelier de programmation est disponible dans le répertoire codelabs/grpc-python-getting-started/start_here. Si vous préférez ne pas implémenter le code vous-même, le code source complet est disponible dans le répertoire completed.

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

mkdir grpc-python-getting-started && cd grpc-python-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-python-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.

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 du langage de définition d'interface 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 protos/route_guide.proto du code source fourni.

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

Types de messages

Dans le fichier protos/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, générez le code gRPC standard pour le client et le serveur à partir du fichier .proto à l'aide du compilateur de tampons de protocole.

Pour la génération de code Python gRPC, nous avons créé grpcio-tools. Il inclut les éléments suivants :

  1. Le compilateur protoc standard qui génère du code Python à partir des définitions message.
  2. Plug-in protobuf gRPC qui génère du code Python (stubs client et serveur) à partir des définitions service.

Nous allons installer le package Python grpcio-tools à l'aide de pip. Créons un environnement virtuel Python (venv) pour isoler les dépendances de votre projet des packages système :

python3 -m venv --upgrade-deps .venv

Pour activer l'environnement virtuel dans le shell bash/zsh :

source .venv/bin/activate

Pour Windows et les shells non standards, consultez le tableau à l'adresse https://docs.python.org/3/library/venv.html#how-venvs-work.

Ensuite, installez grpcio-tools (cela installe également le package grpcio) :

pip install grpcio-tools

Utilisez la commande suivante pour générer le code standard Python :

python -m grpc_tools.protoc --proto_path=./protos  \
 --python_out=. --pyi_out=. --grpc_python_out=. \
 ./protos/route_guide.proto

Cette opération génère les fichiers suivants pour les interfaces que nous avons définies dans route_guide.proto :

  1. route_guide_pb2.py contient le code qui crée dynamiquement des classes générées à partir des définitions message.
  2. route_guide_pb2.pyi est un fichier stub ou un fichier d'indication de type généré à partir des définitions message. Il ne contient que les signatures sans implémentation. Les fichiers de stub peuvent être utilisés par les IDE pour améliorer l'autocomplétion et la détection des erreurs.
  3. route_guide_pb2_grpc.py est généré à partir des définitions service et contient des classes et des fonctions spécifiques à gRPC.

Le code spécifique à gRPC contient :

  1. RouteGuideStub, qui peut être utilisé par un client gRPC pour appeler les RPC RouteGuide.
  2. RouteGuideServicer, qui définit l'interface pour les implémentations du service RouteGuide.
  3. La fonction add_RouteGuideServicer_to_server est utilisée pour enregistrer un RouteGuideServicer sur un serveur gRPC.

5. Créer le service

Commençons par voir comment créer un serveur RouteGuide. La création et l'exécution d'un serveur RouteGuide se décomposent en deux tâches :

  • Implémenter l'interface de service générée à partir de notre définition de service avec des fonctions qui effectuent le "travail" réel du service.
  • Exécuter un serveur gRPC sur un port spécifique pour écouter les requêtes des clients et transmettre les réponses.

Vous trouverez le serveur RouteGuide initial dans start_here/route_guide_server.py.

Implémenter RouteGuide

route_guide_server.py comporte une classe RouteGuideServicer qui est une sous-classe de la classe générée route_guide_pb2_grpc.RouteGuideServicer :

# RouteGuideServicer provides an implementation
# of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):

RouteGuideServicer implémente toutes les méthodes de service RouteGuide.

Examinons en détail une implémentation RPC simple. La méthode GetFeature obtient un Point du client et renvoie les informations correspondantes sur les caractéristiques de sa base de données dans Feature.

def GetFeature(self, request, context):
    feature = get_feature(self.db, request)
    if feature is None:
        return route_guide_pb2.Feature(name="", location=request)
    else:
        return feature

La méthode reçoit une requête route_guide_pb2.Point pour le RPC et un objet grpc.ServicerContext qui fournit des informations spécifiques au RPC, telles que les limites de délai d'attente. Il renvoie une réponse route_guide_pb2.Feature.

Démarrer le serveur

Une fois que vous avez implémenté toutes les méthodes RouteGuide, l'étape suivante consiste à démarrer un serveur gRPC afin que les clients puissent réellement utiliser votre service :

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
        RouteGuideServicer(),
        server,
    )
    listen_addr = "localhost:50051"
    server.add_insecure_port(listen_addr)
    print(f"Starting server on {listen_addr}")
    server.start()
    server.wait_for_termination()

La méthode start() du serveur n'est pas bloquante. Un nouveau thread sera instancié pour gérer les requêtes. Le thread appelant server.start() n'aura souvent aucune autre tâche à effectuer en attendant. Dans ce cas, vous pouvez appeler server.wait_for_termination() pour bloquer proprement le thread appelant jusqu'à ce que le serveur se termine.

6. Créer le client

Dans cette section, nous allons créer un client pour notre service RouteGuide. Vous pouvez consulter le code client initial dans start_here/route_guide_client.py.

Créer un stub

Pour appeler des méthodes de service, nous devons d'abord créer un stub.

Nous instancions la classe RouteGuideStub du module route_guide_pb2_grpc, générée à partir de notre .proto dans le fichier route_guide_client.py.

channel = grpc.insecure_channel("localhost:50051")
stub = route_guide_pb2_grpc.RouteGuideStub(channel)

Appeler des méthodes de service

Pour les méthodes RPC qui renvoient une seule réponse (appelées méthodes response-unary), gRPC Python est compatible avec les sémantiques de flux de contrôle synchrone (bloquant) et asynchrone (non bloquant).

RPC simple

Commençons par définir un Point pour appeler le service. Il suffit d'instancier un objet à partir du package route_guide_pb2 avec certaines propriétés :

point = route_guide_pb2.Point(latitude=412346009, longitude=-744026814)

Un appel synchrone au RPC simple GetFeature est presque aussi simple que l'appel d'une méthode locale. L'appel RPC attend la réponse du serveur et renvoie une réponse ou génère une exception. Nous pouvons appeler la méthode et voir la réponse comme suit :

feature = stub.GetFeature(point)
print(feature)

Vous pouvez inspecter les champs de l'objet Feature et afficher le résultat de la requête :

if feature.name:
    print(f"Feature called '{feature.name}' at {format_point(feature.location)}")
else:
    print(f"Found no feature at at {format_point(feature.location)}")

7. Essayer

Exécutez le serveur :

python route_guide_server.py

Depuis un autre terminal, activez à nouveau l'environnement virtuel, puis exécutez le client :

python route_guide_client.py

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

name: "16 Old Brook Lane, Warwick, NY 10990, USA"
location {
  latitude: 412346009
  longitude: -744026814
}

Feature called '16 Old Brook Lane, Warwick, NY 10990, USA' at latitude: 412346009, longitude: -744026814

8. Étape suivante