Quand on échange des données entre deux applications, on utilise souvent du JSON par défaut. Le format est lisible, simple à manipuler et largement supporté, mais il montre vite certaines limites dès que l'on cherche à optimiser les échanges :
- le format texte prend de la place.
- les noms des clés sont répétés dans chaque message.
- il n'y a pas de contrat strict sur les types de données.
Les Protocol Buffers offrent une solution à ce problème avec un format de sérialisation binaire qui permet de réduire le poids des messages tout en décrivant clairement la structure des données.
Le principe des Protocol Buffers
Un protobuf permet de représenter des données sous une forme binaire. Contrairement au JSON, le message transmis ne contient pas directement des clés comme id, name ou active. Il contient plutôt une suite de champs identifiés par des numéros, associés à un type et à une valeur.
On peut résumer le fonctionnement comme une succession d'informations :
- un numéro de champ.
- un type de donnée.
- la valeur associée.
Ce choix permet de réduire fortement la taille du message. Au lieu de répéter les noms des propriétés à chaque échange, on transmet uniquement les informations nécessaires pour reconstruire la donnée côté réception.
Le rôle du fichier .proto
Pour que ce format binaire soit exploitable, il faut une carte qui explique à quoi correspondent les différents numéros de champs. C'est le rôle du fichier .proto.
Ce fichier décrit les messages indépendamment du langage de programmation utilisé. On y définit le nom du message, les champs disponibles, leurs types et surtout leur position dans le message binaire.
Dans cet exemple, le champ 1 correspond à l'identifiant et le champ 2 correspond au nom. Quand un message binaire est reçu, le programme peut donc savoir que la donnée présente en position 1 doit être interprétée comme id.
Ce fichier .proto devient la source de vérité. Il sert à sérialiser les données avant l'envoi, puis à les désérialiser à la réception. Si les types ne correspondent pas à ce qui est attendu, on peut détecter que le message reçu n'est pas correct.
Un format compact
Un message protobuf est lu octet par octet. Le premier octet permet d'identifier le numéro du champ et son type, puis les octets suivants permettent de lire la valeur correspondante. Si on reprend un champ id placé en position 1, avec une valeur entière 8, on peut le représenter de manière simplifiée comme ceci :
Le premier octet indique que l'on lit le champ numéro 1 et que son type est un entier encodé en VARINT. L'octet suivant contient la valeur 8. Si le nombre est plus grand, plusieurs octets peuvent être utilisés avec un bit de continuation pour savoir si on doit continuer la lecture.
Pour les chaînes de caractères ou les données de taille variable, le message indique la taille à lire. Un champ name placé en position 2 avec une chaîne de 3 caractères peut se lire comme une suite : champ, taille, puis contenu.
C'est cette approche qui rend le format intéressant : on transmet le strict minimum nécessaire pour reconstruire l'information.
Représenter des données complexes
Les protobufs ne se limitent pas à des structures plates. On peut aussi représenter des listes et des objets imbriqués.
Un champ peut être répété pour représenter un tableau. Si un même numéro de champ apparaît plusieurs fois et que le fichier .proto indique qu'il s'agit d'un champ répété, les valeurs sont regroupées dans une collection.
On peut également imbriquer des messages. Par exemple, une réponse peut contenir une liste de personnes. Chaque personne est alors elle-même encodée comme un message protobuf avec ses propres champs.
Le principe reste le même : le message binaire contient des numéros, des types et des valeurs. Le fichier .proto permet ensuite de redonner du sens à ces données.
Générer du code à partir du contrat
Un autre intérêt important des fichiers .proto, c'est qu'ils peuvent servir à générer du code dans différents langages. À partir d'un même fichier, on peut produire des classes ou des structures pour Go, Java, PHP et d'autres langages.
Ces classes générées permettent ensuite :
- de créer un message binaire à partir des données de l'application.
- de lire un message binaire reçu depuis une API.
- de garder le même contrat entre plusieurs services écrits dans des langages différents.
C'est particulièrement utile dans un contexte gRPC, où les protobufs servent à décrire les messages échangés entre le client et le serveur.
Compatibilité dans le temps
Les fichiers .proto permettent aussi de faire évoluer une API sans forcément tout casser. Si on ajoute un nouveau champ, par exemple un âge en position 5, les anciens clients qui ne connaissent pas encore ce champ peuvent continuer à fonctionner. Le serveur utilisera alors une valeur par défaut pour le type concerné, ou pourra détecter l'absence de valeur si le champ est optionnel.
Dans l'autre sens, si un ancien client envoie encore un champ qui n'existe plus côté serveur, ce champ peut être ignoré. Il faut en revanche faire attention à ne pas réutiliser trop vite un numéro de champ déjà utilisé. Un ancien client pourrait encore envoyer cette donnée, ce qui créerait une ambiguïté.
Pour éviter ce problème, protobuf permet de réserver un numéro de champ supprimé. Cela indique que ce numéro a existé par le passé et qu'il ne doit pas être réattribué tant que l'on n'est pas certain que tous les clients ont été mis à jour.
Quand utiliser protobuf ?
Les protobufs sont particulièrement adaptés aux communications entre services lorsque la taille des messages, les performances et la stabilité du contrat sont importantes.
Ce format est au cœur de gRPC, où les fichiers .proto décrivent à la fois les messages et les méthodes disponibles entre un client et un serveur. Plusieurs services peuvent ainsi partager le même contrat, même lorsqu'ils utilisent des technologies différentes.
ConnectRPC repose sur le même principe, mais propose une intégration plus souple avec HTTP et les navigateurs. Il est notamment possible d'utiliser une représentation JSON des messages protobuf, ce qui facilite leur consommation côté Web. On perd alors une partie des bénéfices du format binaire.
Pour aller plus loin et découvrir des notions plus avancées comme les énumérations, les unions ou la génération de code dans différents langages, vous pouvez consulter la documentation officielle sur protobuf.dev.