Contactez-nous

gRPC et Protocol Buffers

Maîtrisez gRPC et Protocol Buffers en Go pour des microservices rapides, robustes et interopérables. Découvrez définition de services, génération de code, client, serveur et cas d'usage.

Introduction à gRPC et Protocol Buffers : Le duo gagnant pour les APIs performantes

Dans le monde des microservices et des architectures distribuées, la communication efficace et performante entre les services est primordiale. gRPC (gRPC Remote Procedure Call) et Protocol Buffers (protobuf) forment un duo gagnant développé par Google, offrant une solution complète et optimisée pour la construction d'APIs inter-services rapides, robustes, interopérables et évolutives.

gRPC est un framework RPC (Remote Procedure Call) moderne et performant, basé sur le protocole HTTP/2 et utilisant Protocol Buffers comme format de sérialisation par défaut. gRPC permet de définir des APIs sous forme de services et de méthodes (comme des fonctions), et de générer automatiquement le code client et serveur correspondant dans différents langages de programmation (dont Go). Protocol Buffers, quant à lui, est un format de sérialisation binaire léger et efficace, conçu pour sérialiser des données structurées de manière compacte et rapide, optimisant ainsi les performances et la bande passante.

Ce chapitre vous propose un guide complet sur gRPC et Protocol Buffers en Go. Nous allons explorer en détail les concepts de gRPC et protobuf, comment définir des services gRPC avec le langage protobuf, comment générer le code Go client et serveur à partir des définitions protobuf, comment implémenter un serveur gRPC en Go pour exposer vos APIs, comment construire un client gRPC en Go pour consommer des APIs gRPC, et les avantages et cas d'utilisation typiques de gRPC et protobuf pour les microservices et les applications distribuées. Que vous soyez novice ou expérimenté en architectures microservices, ce guide vous fournira les compétences nécessaires pour maîtriser gRPC et protobuf avec Go et construire des APIs inter-services performantes et modernes.

Protocol Buffers (protobuf) : Définir des messages et des services

Protocol Buffers (protobuf), souvent abrégé en protobuf, est un langage de définition d'interface (IDL) et un format de sérialisation de données développé par Google. Protobuf permet de définir des structures de données structurées (messages) et des services RPC de manière indépendante du langage de programmation, et de générer automatiquement le code de sérialisation et de désérialisation (et le code client/serveur pour gRPC) dans différents langages, dont Go.

Définition de messages (structures de données) avec protobuf :

Vous définissez des structures de données (messages) dans des fichiers .proto en utilisant le langage protobuf. Un message protobuf est similaire à un struct en Go, mais avec des règles de définition et de sérialisation spécifiques. Un fichier .proto peut contenir la définition de plusieurs messages.

Syntaxe de définition d'un message protobuf :

syntax = "proto3"; // Spécifier la syntaxe protobuf (proto3 recommandée)

message NomDuMessage {
  TypeDuChamp1 nom_du_champ_1 = 1;
  TypeDuChamp2 nom_du_champ_2 = 2;
  // ...
}

  • syntax = "proto3"; : Spécifie la version de la syntaxe protobuf utilisée (proto3 est la version recommandée et la plus récente).
  • message NomDuMessage { ... } : Déclare un nouveau message protobuf nommé NomDuMessage (PascalCase par convention).
  • TypeDuChamp1 nom_du_champ_1 = 1; : Déclare un champ du message.
    • TypeDuChamp1 : Le type de données du champ (type protobuf, comme int32, string, bool, message, enum, etc.).
    • nom_du_champ_1 : Le nom du champ (snake_case par convention).
    • = 1 : Le numéro de champ (field number). Un entier unique qui identifie le champ lors de la sérialisation et de la désérialisation. Les numéros de champ doivent être uniques au sein d'un message et sont utilisés pour la compatibilité ascendante et descendante (évolution du schéma).

Types de données protobuf courants :

Protobuf propose un ensemble de types de données de base, similaires aux types de données Go, mais avec des noms et des caractéristiques spécifiques à protobuf :

  • Types numériques : int32, int64, uint32, uint64, float, double, fixed32, fixed64, sfixed32, sfixed64.
  • Types booléens et chaînes de caractères : bool, string, bytes (pour les données binaires).
  • Messages imbriqués : Vous pouvez utiliser d'autres messages protobuf comme types de champs, permettant de définir des structures de données complexes et imbriquées.
  • Enums (énumérations) : Protobuf supporte les énumérations (enum) pour définir des listes de valeurs nommées.
  • Listes (repeated fields) : Pour définir des champs qui sont des listes d'éléments (similaires aux slices en Go), utilisez le mot-clé repeated devant le type du champ.
  • Maps (tableaux associatifs) : Protobuf supporte les maps (tableaux associatifs) pour définir des champs qui sont des dictionnaires clé-valeur.

Définition de services RPC avec protobuf :

En plus de définir des messages, les fichiers .proto servent également à définir des services RPC (Remote Procedure Call) avec le langage protobuf. Un service protobuf définit un ensemble de méthodes RPC (comme des fonctions) que le serveur expose et que le client peut appeler à distance.

Syntaxe de définition d'un service protobuf :

service NomDuService {
  rpc NomDeLaMethodeRPC (TypeRequete) returns (TypeReponse) {}
  // ...
}

  • service NomDuService { ... } : Déclare un nouveau service RPC protobuf nommé NomDuService (PascalCase par convention).
  • rpc NomDeLaMethodeRPC (TypeRequete) returns (TypeReponse) {} : Déclare une méthode RPC du service.
    • rpc : Mot-clé indiquant qu'il s'agit d'une méthode RPC.
    • NomDeLaMethodeRPC : Le nom de la méthode RPC (PascalCase par convention). Ce nom sera utilisé pour appeler la méthode à distance depuis le client.
    • (TypeRequete) : Le type du message de requête (input message) attendu par la méthode RPC. Doit être un message protobuf défini dans le même fichier .proto ou dans un fichier importé.
    • returns (TypeReponse) : Le type du message de réponse (output message) retourné par la méthode RPC. Doit également être un message protobuf.

Exemple de fichier .proto définissant un message et un service :

syntax = "proto3";

package helloworld;

option go_package = "./pb";

// Définition du message 'HelloRequest'
message HelloRequest {
  string nom = 1;
}

// Définition du message 'HelloReply'
message HelloReply {
  string message = 1;
}

// Définition du service 'Greeter'
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {} // Méthode RPC 'SayHello'
}

Ce fichier .proto définit un message HelloRequest (requête) avec un champ nom, un message HelloReply (réponse) avec un champ message, et un service Greeter avec une méthode RPC SayHello qui prend un HelloRequest en entrée et retourne un HelloReply en sortie. Ce fichier .proto servira de base pour la génération du code Go client et serveur gRPC dans les sections suivantes.

Génération de code Go gRPC avec protoc et protoc-gen-go-grpc

Une fois que vous avez défini vos messages et services protobuf dans un fichier .proto, l'étape suivante consiste à générer le code Go gRPC correspondant à partir de cette définition. La génération de code est réalisée à l'aide du compilateur protobuf protoc et du plugin Go protoc-gen-go-grpc.

Outils nécessaires pour la génération de code gRPC Go :

  • Compilateur Protocol Buffers protoc : Vous devez installer le compilateur protoc (protoc compiler) sur votre système. Téléchargez la version appropriée pour votre OS depuis le dépôt GitHub de Protocol Buffers ([https://github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases)) et suivez les instructions d'installation. Assurez-vous que la commande protoc est accessible dans votre PATH.
  • Plugin Go gRPC protoc-gen-go-grpc : Vous devez installer le plugin Go gRPC protoc-gen-go-grpc, qui est un plugin protoc spécifique pour générer du code Go compatible avec gRPC. Installez-le avec la commande go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest. Assurez-vous que le binaire protoc-gen-go-grpc est également accessible dans votre PATH (généralement dans $GOPATH/bin ou $HOME/go/bin).

Commande protoc pour la génération de code Go gRPC :

Pour générer le code Go gRPC à partir d'un fichier .proto, utilisez la commande protoc avec les options et les plugins appropriés :

protoc --go_out=. --go-grpc_out=. votre_fichier.proto

  • protoc : La commande pour lancer le compilateur protobuf.
  • --go_out=. : Option pour spécifier le plugin Go protoc-gen-go (génération du code Go protobuf de base) et le répertoire de sortie (. pour le répertoire courant).
  • --go-grpc_out=. : Option pour spécifier le plugin Go gRPC protoc-gen-go-grpc (génération du code Go gRPC serveur et client) et le répertoire de sortie (. pour le répertoire courant).
  • votre_fichier.proto : Le chemin vers votre fichier .proto contenant la définition des messages et services protobuf.

Exemple de génération de code Go gRPC (avec le fichier helloworld.proto) :

Supposons que vous ayez créé un fichier helloworld.proto dans un répertoire proto à la racine de votre projet Go. Pour générer le code Go gRPC correspondant, naviguez jusqu'à la racine de votre projet dans le terminal et exécutez la commande :

protoc --go_out=pb --go-grpc_out=pb proto/helloworld.proto

Cette commande va générer deux fichiers Go dans un nouveau répertoire pb (spécifié par l'option --go_out=pb et --go-grpc_out=pb) :

  • pb/helloworld.pb.go : Contient le code Go généré pour les messages protobuf (HelloRequest, HelloReply) définis dans helloworld.proto. Ce fichier inclut les structures Go correspondant aux messages protobuf, les fonctions de sérialisation/désérialisation, et d'autres fonctions utilitaires.
  • pb/helloworld_grpc.pb.go : Contient le code Go généré pour le service gRPC (Greeter) défini dans helloworld.proto. Ce fichier inclut les interfaces et les stubs (clients) pour le service gRPC, ainsi que le squelette de code serveur (interfaces pour l'implémentation du service côté serveur).

Une fois le code Go gRPC généré, vous pouvez l'utiliser pour implémenter votre serveur gRPC en Go et pour construire des clients gRPC Go pour consommer votre service.

Serveur gRPC en Go : Implémenter et exposer le service

Après avoir généré le code Go gRPC à partir de votre fichier .proto, vous pouvez implémenter le serveur gRPC en Go. L'implémentation d'un serveur gRPC consiste à créer un serveur gRPC, à enregistrer votre service implémenté auprès de ce serveur, et à le mettre en écoute pour recevoir les requêtes des clients.

Etapes pour implémenter un serveur gRPC en Go :

  1. Créer un serveur gRPC : Créez une instance de grpc.Server en utilisant la fonction grpc.NewServer() du package google.golang.org/grpc. Le grpc.Server est le composant principal qui gère les connexions gRPC entrantes et le dispatch des requêtes vers les handlers de service.
  2. Implémenter l'interface de service gRPC : Le plugin protoc-gen-go-grpc génère une interface Go (GreeterServer dans l'exemple helloworld.proto) qui définit les méthodes du service gRPC (SayHello). Vous devez créer un struct Go qui implémente cette interface, en fournissant une implémentation concrète pour chaque méthode RPC du service. Les méthodes d'implémentation doivent avoir la signature spécifiée dans l'interface (par exemple, func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error)).
  3. Enregistrer le service auprès du serveur gRPC : Utilisez la fonction pb.RegisterGreeterServer(grpcServer *grpc.Server, server GreeterServer) (générée par protoc-gen-go-grpc dans le fichier pb/helloworld_grpc.pb.go) pour enregistrer votre implémentation de service (le struct Go qui implémente l'interface GreeterServer) auprès du serveur gRPC (grpcServer). L'enregistrement du service associe le nom du service et les méthodes RPC à votre implémentation de service.
  4. Mettre le serveur gRPC en écoute : Créez un listener réseau TCP (net.Listen("tcp", adresse)) pour écouter les connexions entrantes sur l'adresse spécifiée (adresse, au format "host:port"). Utilisez la méthode grpcServer.Serve(listener net.Listener) error pour démarrer le serveur gRPC et le mettre en écoute sur le listener réseau. grpcServer.Serve est une fonction bloquante : elle démarre le serveur et bloque l'exécution du programme appelant jusqu'à ce que le serveur s'arrête (en cas d'erreur ou d'arrêt manuel).

Exemple de serveur gRPC en Go (implémentation du service Greeter) :

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "./pb" // Chemin vers le package Go généré par protoc
)

// Définition du struct 'server' qui implémente l'interface 'pb.GreeterServer'
type server struct {
    pb.UnimplementedGreeterServer // Embed pour la compatibilité ascendante (si l'interface 'GreeterServer' évolue)
}

// Implémentation de la méthode RPC 'SayHello' du service 'Greeter'
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Reçu : %v", req.GetNom())
    return &pb.HelloReply{Message: "Bonjour " + req.GetNom()}, nil // Retourne un message de réponse 'HelloReply'
}

func main() {
    adresseServeur := ":50051" // Adresse d'écoute du serveur gRPC
    listener, err := net.Listen("tcp", adresseServeur) // Création du listener TCP
    if err != nil {
        log.Fatalf("Echec de l'écoute: %v", err)
    }

    grpcServer := grpc.NewServer() // Création du serveur gRPC
    pb.RegisterGreeterServer(grpcServer, &server{}) // Enregistrement de l'implémentation du service auprès du serveur gRPC

    log.Printf("Serveur gRPC en écoute sur %s", adresseServeur)
    if err := grpcServer.Serve(listener); err != nil {
        log.Fatalf("Echec du démarrage du serveur gRPC: %v", err)
    }
}

Cet exemple illustre l'implémentation d'un serveur gRPC simple en Go, exposant le service Greeter (défini dans helloworld.proto) avec une méthode RPC SayHello. Le serveur gRPC est lancé et mis en écoute sur le port 50051, prêt à recevoir les requêtes des clients gRPC.

Client gRPC en Go : Se connecter et appeler le service

Pour consommer un service gRPC exposé par un serveur gRPC, vous devez construire un client gRPC en Go. Le client gRPC permet d'établir une connexion avec le serveur gRPC et d'appeler les méthodes RPC du service à distance, comme s'il s'agissait de fonctions locales.

Etapes pour créer un client gRPC en Go :

  1. Etablir une connexion gRPC avec le serveur : Utilisez la fonction grpc.Dial(adresse string, options ...DialOption) (*grpc.ClientConn, error) du package google.golang.org/grpc pour établir une connexion gRPC avec le serveur. Dial prend en arguments l'adresse du serveur gRPC ("host:port") et des options de connexion (DialOption). Elle retourne une connexion client *grpc.ClientConn (si la connexion réussit) et une erreur error (en cas d'échec de la connexion). Pour les connexions non sécurisées (sans TLS), vous pouvez utiliser l'option grpc.WithInsecure(). Il est recommandé de fermer la connexion client après utilisation avec defer conn.Close().
  2. Créer un client stub (client-side stub) : Utilisez la fonction pb.NewGreeterClient(conn *grpc.ClientConn) pb.GreeterClient (générée par protoc-gen-go-grpc dans le fichier pb/helloworld_grpc.pb.go) pour créer un client stub (client-side stub) pour le service gRPC Greeter. Le client stub est une interface Go (pb.GreeterClient) qui fournit des méthodes pour appeler les méthodes RPC du service à distance.
  3. Appeler les méthodes RPC du service via le client stub : Appelez les méthodes RPC du service (comme SayHello) via le client stub (pb.GreeterClient). Les méthodes RPC du client stub prennent en arguments un contexte context.Context (pour gérer les timeouts, l'annulation, etc.) et un message de requête (par exemple, *pb.HelloRequest), et retournent un message de réponse (par exemple, *pb.HelloReply) et une erreur error. L'appel d'une méthode RPC via le client stub envoie une requête gRPC au serveur, attend la réponse du serveur, et retourne la réponse et l'erreur éventuelle.

Exemple de client gRPC en Go (appel du service Greeter) :

package main

import (
    "context"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "./pb" // Chemin vers le package Go généré par protoc
)

func main() {
    adresseServeur := "localhost:50051" // Adresse du serveur gRPC

    // Etablir une connexion gRPC non sécurisée avec le serveur
    conn, err := grpc.Dial(adresseServeur, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("Echec de la connexion au serveur gRPC: %v", err)
        os.Exit(1)
    }
    defer conn.Close() // Fermer la connexion client à la sortie de la fonction main

    client := pb.NewGreeterClient(conn) // Création du client stub 'GreeterClient'

    nom := "MonClientGo"
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Contexte avec timeout de 5 secondes
    defer cancel()

    // Appel de la méthode RPC 'SayHello' du service 'Greeter'
    reponse, err := client.SayHello(ctx, &pb.HelloRequest{Nom: nom})
    if err != nil {
        log.Fatalf("Echec de l'appel RPC: %v", err)
        os.Exit(1)
    }

    log.Printf("Réponse du serveur gRPC: %s", reponse.GetMessage())
}

Cet exemple illustre la création d'un client gRPC simple en Go, capable de se connecter au serveur gRPC (implémenté dans l'exemple précédent), d'appeler la méthode RPC SayHello du service Greeter, et d'afficher la réponse reçue du serveur. Le client utilise le client stub pb.GreeterClient généré par protoc-gen-go-grpc et la fonction grpc.Dial pour établir la connexion et effectuer l'appel RPC.

Avantages de gRPC et Protocol Buffers : Performance, interopérabilité et plus

L'utilisation de gRPC et Protocol Buffers pour la construction d'APIs inter-services en Go offre de nombreux avantages significatifs par rapport aux APIs RESTful traditionnelles (basées sur JSON/HTTP) :

  • Performance et rapidité : gRPC, basé sur HTTP/2 et protobuf, est conçu pour la performance et la rapidité. Le protocole HTTP/2 offre des améliorations de performance par rapport à HTTP/1.1 (multiplexage, compression des headers, etc.). Le format de sérialisation binaire protobuf est beaucoup plus léger et plus rapide à sérialiser/désérialiser que JSON ou XML, réduisant la latence et la consommation de bande passante. gRPC est particulièrement performant pour les communications inter-services internes, où la latence et le débit sont critiques.
  • Interopérabilité et multi-langage : gRPC et protobuf sont indépendants du langage de programmation. Vous pouvez définir vos APIs en protobuf et générer le code client et serveur correspondant dans différents langages (Go, Java, Python, C++, etc.). Cela facilite l'interopérabilité entre les microservices et les applications écrites dans différents langages, permettant de construire des architectures polyglottes.
  • Typage fort et validation des données : Protobuf impose un typage fort des données et des APIs, grâce à son langage de définition d'interface (IDL). Les messages et les services sont définis avec des types de données précis, et le compilateur protobuf vérifie la cohérence et la validité des définitions. La validation des données est intégrée au processus de sérialisation/désérialisation, réduisant les erreurs et améliorant la robustesse des APIs.
  • Génération automatique de code : Les outils protoc et protoc-gen-go-grpc génèrent automatiquement une grande partie du code boilerplate nécessaire pour les APIs gRPC (code de sérialisation/désérialisation, code client et serveur, interfaces, stubs, etc.). Cela simplifie considérablement le développement d'APIs gRPC, réduit la quantité de code à écrire manuellement, et améliore la productivité des développeurs.
  • Streaming bidirectionnel : gRPC supporte nativement le streaming bidirectionnel, permettant au client et au serveur d'envoyer et de recevoir des flux de messages en temps réel, dans les deux sens, sur une seule connexion persistante. Le streaming bidirectionnel est idéal pour les applications temps réel, les communications push, et les scénarios où un flux continu de données doit être échangé entre le client et le serveur.
  • Support intégré pour HTTP/2 et TLS/SSL : gRPC est basé sur HTTP/2, un protocole HTTP moderne qui offre des améliorations de performance et de fonctionnalités par rapport à HTTP/1.1. gRPC supporte également nativement TLS/SSL pour sécuriser les communications avec le chiffrement et l'authentification, facilitant la mise en place d'APIs sécurisées.

gRPC et Protocol Buffers sont un excellent choix pour la construction d'APIs inter-services performantes, robustes, interopérables et modernes en Go, en particulier pour les architectures de microservices et les applications distribuées où la performance, la scalabilité et la fiabilité sont des exigences clés.

Bonnes pratiques pour la conception et l'implémentation d'APIs gRPC

Pour concevoir et implémenter des APIs gRPC efficaces, robustes et maintenables en Go, voici quelques bonnes pratiques à suivre :

  • Définir des APIs claires et bien conçues avec Protocol Buffers : Passez du temps à concevoir soigneusement vos fichiers .proto, en définissant des messages et des services clairs, cohérents et bien documentés. Utilisez des noms de messages et de champs descriptifs et significatifs. Organisez vos fichiers .proto de manière logique et modulaire, en regroupant les messages et les services connexes dans des fichiers séparés si nécessaire.
  • Utiliser la sémantique de version (versioning) pour les APIs protobuf : Appliquez les principes de la sémantique de version (SemVer) à vos APIs protobuf. Lorsque vous apportez des modifications incompatibles à une API protobuf, incrémentez la version MAJEURE de l'API et créez une nouvelle version du service (par exemple, v2.Greeter au lieu de v1.Greeter). Maintenez la compatibilité ascendante autant que possible pour faciliter la migration des clients vers les nouvelles versions.
  • Générer le code Go gRPC avec protoc et protoc-gen-go-grpc : Utilisez systématiquement les outils protoc et protoc-gen-go-grpc pour générer automatiquement le code Go gRPC à partir de vos fichiers .proto. Evitez de modifier manuellement le code généré, car il sera écrasé lors des prochaines générations. Si vous avez besoin de personnaliser le comportement du code généré, utilisez les mécanismes d'extension et d'interception de gRPC (middleware, interceptors) plutôt que de modifier le code généré directement.
  • Implémenter les serveurs gRPC de manière robuste et performante : Implémentez vos serveurs gRPC en Go en suivant les principes de robustesse et de performance. Gérez correctement les erreurs, les timeouts, l'annulation, la concurrence, la validation des données, la sécurité, et les aspects liés à la performance (optimisation du code, caching, load balancing, etc.).
  • Construire des clients gRPC idiomatiques et faciles à utiliser : Créez des clients gRPC Go idiomatiques et faciles à utiliser pour consommer vos APIs gRPC. Fournissez des exemples de code clairs et concis pour illustrer l'utilisation des clients gRPC. Encapsulez la logique de connexion et de gestion des erreurs dans des fonctions utilitaires pour simplifier l'utilisation des clients gRPC par les développeurs.
  • Documenter clairement vos APIs gRPC : Documentez clairement vos APIs gRPC, en décrivant les services, les méthodes RPC, les messages de requête et de réponse, les types de données utilisés, les erreurs potentielles, et les exemples d'utilisation. Une bonne documentation est essentielle pour rendre vos APIs gRPC utilisables et compréhensibles par les développeurs qui souhaitent les consommer. Envisagez d'utiliser des outils comme gRPC Gateway pour exposer également vos APIs gRPC en tant qu'APIs RESTful/JSON pour faciliter leur consommation par les clients web traditionnels.
  • Sécuriser vos APIs gRPC avec TLS/SSL et l'authentification : Sécurisez vos APIs gRPC en production en activant TLS/SSL pour chiffrer les communications et en mettant en place des mécanismes d'authentification et d'autorisation appropriés (par exemple, authentification basée sur les certificats, OAuth 2.0, tokens JWT, etc.) pour contrôler l'accès à vos services gRPC et protéger les données sensibles.

En appliquant ces bonnes pratiques, vous concevrez et implémenterez des APIs gRPC robustes, performantes, interopérables et faciles à utiliser en Go, en tirant pleinement parti des avantages de gRPC et Protocol Buffers pour la construction de microservices et d'applications distribuées modernes.