Retour

Présentation de Protocol Buffers

20 sept. 2017
6mn

Une problématique assez récurrente dans nos applications est la manière de rendre notre donnée, qu'elle soit transmise ou stockée. Le format qu'on utilise souvent aujourd'hui est le JSON. Cependant certains langages le gèrent très mal tel que Java et Go. Google a donc développé un système pour palier à ce problème : Protocol Buffers.

Présentation


Protocol Buffers est un système de sérialisation de données tout comme JSON et XML. Il est disponible pour la plupart des langages. Une application en Java peut envoyer des objets à une application en Go. Le système repose sur un fichier qui va permettre de structurer notre objet, les fichiers .proto. Ce fichier va un peu comme une interface décrire notre objet. Protobuf permet ensuite de générer le code source de l'objet dans plusieurs langages différents.

Pour récapituler, on déclare un fichier proto, on génère notre objet dans notre application serveur et dans notre application client. Nos objets auront dans leur déclaration des méthodes de sérialisation et de de-sérialisation et ce quel que soit le langage.

Notre exemple va être le suivant :

Notre API va retourner un objet Post. Un client va appeler cette api. Nous allons avoir besoin d'un fichier proto qui va générer le code source en Go. Le serveur va sérialiser un objet et le rendre au client. Ce qui nous donne :

Go Struct  ↘                                              ↗ Java Object
             Serialization -> bytes -> Deserialization
Proto file ↗                                              ↖ Proto file

Nous allons maintenant voir étape par étape comment ça fonctionne.

Fonctionnement


Protobuf est un langage qui va permettre de définir comment l'objet va être sérialisé et comment il va générer le code source. Voici un exemple de fichier protobuf :

message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; }

Dans cet exemple notre objet Person est constitué d'un name, d'un id, d'un email et de phones. La déclaration d'une propriété est définie par un type int32 ou string (et bien d'autres), du nom de la propriété puis d'un identifiant unique (la position) de cette propriété.

On peut aussi faire de la composition en créant de nouveaux types comme ici PhoneNumber ou bien des énumérations. Des modèles de données sont aussi disponibles comme les array avec le mot-clé repeated ou bien encore des maps avec map<Key, Value>.

Vous pouvez retrouver tous les types et déclaration sur la documentation de Protobuf.

Une fois notre fichier proto prêt, nous pouvons générer notre fichier Go ou autres avec la commande protoc.

Par exemple :

protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/person.proto

En lui donnant le fichier proto en entrée et le dossier de destination en précisant le langage Go --go_out ou Java --java_out.

Maintenant que notre fichier Go ou Java est généré, nous avons accès à la méthode de sérialisation.

book := &pb.AddressBook{} out, err := proto.Marshal(book)

Pour comprendre comment Protocol Buffers sérialise un objet en binaire nous allons prendre la définition suivante :

message Test1 { int32 a = 1; }

Nous allons assigner à a la valeur 150. Une sérialisation en json donnerait :

{"a": 150}

Donne 7b 22 61 22 3a 31 35 30 7d sur 9 octets.

En sérialisant avec Protobuf on obtient 08 96 01 sur 3 octets. Ce binaire est composé pour chacune des propriétés de notre objet d'un couple clé/valeur.

Clé :

Codage de la clé : (POSITION << 3) | TYPE

La position ici est 1 et le type 0, soit :

(1 << 3) | 0 = 0000 1000
             = 08

Valeur :

Codage de la valeur : groupage en 7 bits avec l'ajout d'un msb (most significant bit), soit :

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)
       → 10010110
       → 2 + 4 + 16 + 128 = 150

Un binaire protobuf sera plus léger qu'un json et donc plus rapidement transmis dans une requête. De plus le parsing est très performant, retrouvez un article sur les performances de Protobuf.

Nous allons maintenant voir tout ça en pratique.

Exemple d'application


Nous allons tout d'abord installer protoc qui permet de générer notre code depuis les fichiers protobuf. Installer la version pour votre système d'exploitation. Une fois ceci fait on va déclarer notre fichier protobuf. Nous allons ensuite installer la librairie qui va permettre de gérer la génération des fichiers Go.

go get -u github.com/golang/protobuf/protoc-gen-go

Le fichier Proto :

syntax = "proto3"; package main; message Post { int32 id = 1; string title = 2; string author = 3; }

Un Post est donc composé d'un id, d'un title et d'un author. Nous allons donc générer le fichier Post grâce à protoc:

protoc --proto_path=. --go_out=. post.proto

Nous devons récupérer la librairie proto qui sera utilisée dans le client et dans le serveur.

go get github.com/golang/protobuf/proto

Le serveur :

Nous allons maintenant passer au code du serveur.

package main import ( "encoding/json" "log" "net/http" proto "github.com/golang/protobuf/proto" ) var post = &Post{ Id: 1, Title: "My awesome article", Author: "Quentin Neyrat", } func protoHandler(w http.ResponseWriter, r *http.Request) { out, err := proto.Marshal(post) if err != nil { log.Fatalln("Failed to serialize post in protobuf:", err) } w.Write(out) } func main() { http.HandleFunc("/posts/1", protoHandler) http.ListenAndServe(":8080", nil) }

Le client :

Nous allons maintenant passer au code du client.

package main import ( "bufio" "bytes" "fmt" "log" "net/http" proto "github.com/golang/protobuf/proto" ) func main() { post := &Post{} resp, err := http.Get("http://127.0.0.1:8080/posts/1") if err != nil { log.Fatal(err) } defer resp.Body.Close() sca := bufio.NewScanner(resp.Body) sca.Split(bufio.ScanRunes) var buf bytes.Buffer for sca.Scan() { buf.WriteString(sca.Text()) } err = proto.Unmarshal(buf.Bytes(), post) if err != nil { log.Fatal("unmarshaling error: ", err) } fmt.Printf("Id: %d \n", post.GetId()) fmt.Printf("Title: %s \n", post.GetTitle()) fmt.Printf("Author: %s \n", post.GetAuthor()) }

Conclusion


Protocol Buffers est un système maintenu par Google qui va permettre de jouer plus facilement avec nos données et de pouvoir travailler avec différents langages. Ceci est relativement important dans une architecture micro-services où chaque service doit communiquer avec d'autres quel que soit le langage.

Points positifs :

  • performance
  • taille du binaire
  • langage-agnostic

Points négatifs :

  • maintenir les fichiers proto
  • debug (message en binaire)

Nous verrons dans un prochain article gRPC, un client RPC qui utilise HTTP2 et protobuf.


Articles sur le même thème

Monitorer ses containers Docker

Les containers sont aujourd'hui largement utilisés du développement jusqu'en production. Cependant un `docker stats` en ssh ne permet pas de gérer correctement son environnement de production. Nous allons donc voir comment répondre à ce besoin de monitoring pour des containers en production.

12 déc. 20175mnQuentin Neyrat

Soigner ses emails transactionnels

L'année dernière un client nous a demandé que les e-mails envoyés par l'api soient plus esthétiques. N'ayant aucun intégrateur dans l'équipe, personne n'était motivé pour s'occuper de ce genre de ticket. Nous avons donc tiré au sort pour savoir qui s'en chargerait. Bien sûr, c'est tombé sur moi. Le but de la fonctionnalité était d'avoir des emails beaucoup plus soignés et responsives.

6 déc. 20173mnQuentin Neyrat

Présentation de gRPC

gRPC a été développé initialement par Google puis rendu open source. Il permet de réaliser des clients et serveurs rpc via HTTP/2 et donc de profiter de ses nouveautés.

28 sept. 20175mnQuentin Neyrat

Découvrez Eleven Labs

L'ESN qui fait décoller vos projets web, mobile & data !

Voir le site web

Contact

Eleven Labs - Paris

102, rue du Faubourg Saint Honoré

75008 Paris

Eleven Labs - Nantes

40, rue la Tour d'Auvergne

44200 Nantes

Eleven Labs - Montréal

1155, Metcalfe St Suite 1500

Montréal, QC H3B 2V6, Canada