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.