Licence CC BY-SA

gRPC

Un framework moderne pour connecter des services

gRPC

gRPC

gRPC est un framework RPC de 2016. Son but est de connecter des services.

Il possède de nombreux avantages tels que le streaming entre le client et le serveur. Qu’il soit bidirectionnel, côté serveur, ou côté client.

Ce framework est aussi multilangage. Il supporte la plupart des langages populaires aujourd’hui.
En plus d’être multilangage, il est aussi disponible pour de multiples plateformes. Il est disponible pour les applications backend, web et Android. Il sera aussi bientôt disponible pour Flutter et iOS.

gRPC est open source (le dépôt est ici). Il est initialement développé par google.

RPC

RPC signifie Remote Procedure Call. C’est un protocole requête → réponse (comme http) qui permet la délocalisation de l’exécution du code.

Il existe beaucoup de variations d’implémentation du protocole RPC. Parfois, ces implémentations ne sont pas compatibles entre elles.

HTTP/2

gRPC utilise HTTP/2 comme protocole de transport.

HTTP/2 est une version améliorer du protocole HTTP. C’est un standard du web qui permet de nouvelles fonctionnalités et comporte des améliorations telles que le server push.

Protobuf

Pour séréaliser les données à échanger, gRPC utilise par défaut Protocol Buffers.
C’est un format de sérialisation de donnée comme l’est le JSON, le XML, le YAML, … Protobuf est cependant un format binaire. C’est à dire que contrairement au JSON on ne peut pas le lire directement (nous devons passer par un programme).

Protobuf est compatible avec la plupart des langages populaires aujourd’hui.

Tout comme gRPC, protobuf est initialement développé par google et open source (le dépot est ici)

Un retour en arrière ?

Ici nous avons un nouveau protocole avec un nouveau format de sérialisation. Cela peut faire penser aux anciens formats de données et protocoles propriétaire qui pouvaient être compatible avec seulement un langage de programmation (ici on parle de ce qui pouvait exister avant le JSON ou le XML pour les échanges entre services).

gRPC n’est pas un retour en arrière sur ce niveau-là.
Il n’est pas propriétaire, il est ouvert et son implémentation est multi langage. Faire un projet avec gRPC ne contraint pas tous vos services à utiliser un langage de programmation.
De plus, gRPC est basé sur HTTP/2 qui est un standard du web.
Le framework permet aussi de choisir le format de sérialisation pour les échanges de données.

Des outils pour travailler avec gRPC

Bien que gRPC soit récent, il existe déjà de multiples outils pour travailler avec ce framework et protobuf.

Des lanceurs de requettes

Dans les logicielles pour envoyer des requêtes il y a Postman qui est connu notamment pour faire des requêtes http. Il y a aussi Kreya et Insomnia. Kreya est conçu avec pour premier but d’envoyer des requêtes gRPC. Insomnia et plus simpliste d’utilisation pour des petits projets.

Dans le navigateur

Il existe aussi plusieurs extensions de navigateur pour observer le comportement d’une application web qui utilise gRPC.

Des décodeurs sont disponibles en ligne pour convertir les données binaires protobuf en une représentation que nous, humains, pouvons lire.

Garder du http

Il existe aussi des projets comme gRPC-gateway qui permette très facilement de mettre à disposition un reverse-proxi qui va traduire les appels HTTP et appel gRPC.
Cela permet de ne pas avoir un serveur http et un serveur gRPC à maintenir.

Dans les IDE

Afin d’aider les développeurs, il existe aussi de nombreuses extensions d’IDE pour gRPC et protobuf.

Un projet avec gRPC

Nous allons imaginer un projet simple avec gRPC. Pour cela nous allons crées un écho serveur (en Go) qui permettra aussi aux clients d’espionner ce que les gens lui disent.

Dans un premier temps nous allons rédiger un fichier .proto qui définit notre service ainsi que les messages qui vont être utilisés.

// ./proto/spying_echo.proto

syntax = "proto3";

package spyingecho;

option go_package = "./spyingechopb";

service SpyingEcho {
    // Echo respond the thing you say
    rpc Echo (EchoRequest) returns (EchoReply) {}
    // Spy send all things that are say
    rpc Spy (Empty) returns (stream EchoReply) {}
}

// EchoRequest regroup an used and his message
message EchoRequest {
    string Name = 1;
    string Msg = 2;
}

// EchoReply is the response of an user's request
message EchoReply {
    string Msg = 1;
}

message Empty {}

Maintenant, nous allons utiliser la commande protoc qui permet de générer du code que nous allons utiliser pour notre serveur.
Je vais mettre la commande dans un makefile car elle comporte de multiples options.

# ./Makefile

.PHONY: grpc-go server server-run client client-run

grpc-go:
	protoc --go_out=./grpc --go_opt=paths=import \
		--go-grpc_out=./grpc --go-grpc_opt=paths=import \
		./proto/spying_echo.proto

Nous ne voyons pas l’installation de l’outil protoc ici. C’est volontaire, le site officiel vous expliquera en détaillé tout ce qu’il y a à installer.

A présent, la commande make grpc-go va nous générer des fichiers dans le répertoire ./grpc/spyingechopb/. Il n’est pas nécessaire d’aller voir le code qui a été généré, cependant il est très intéressant afin de comprendre ce que ça faire ce code pour nous.

Le serveur

Maintenant, nous pouvons nous concentrer sur l’implémentation de notre serveur.

Nous allons créés une structure qui représentera notre serveur. Elle implémentera l’interface qui a été générée à partir de notre fichier .proto.

// SypingEchoServer implements the  spyingechopb.SpyingechoServer interface
type SpyingechoServer struct {
	spyingechopb.UnimplementedSpyingEchoServer

	// spies regroup all spies that we'll send messages to
	spies Spies

	// ers is a channel where we put all messages to send to spys
	ers chan *spyingechopb.EchoRequest
}

Une fois notre structure définie, je crée une fonction qui va retourner un serveur initialiser et qui dispatchera les messages aux espions connectés.

func NewSpyingechoServer() *SpyingechoServer {
	server := &SpyingechoServer{
		spies: newSpies(),
		ers:   make(chan *spyingechopb.EchoRequest),
	}

	go server.dispatch()
	return server
}

Il ne nous reste plusqu’à implémenté les routes rpc Echo et Spy.

func (server *SpyingechoServer) Echo(ctx context.Context, echoRequest *spyingechopb.EchoRequest) (*spyingechopb.EchoReply, error) {
	server.ers <- echoRequest // Save the message to send it to all spies

	log.Printf("%s said: %s", echoRequest.GetName(), echoRequest.GetMsg())

	return &spyingechopb.EchoReply{Msg: fmt.Sprintf("You said: %s", echoRequest.GetMsg())}, nil
}

Lorsque le serveur recevra un message, il le mettra dans une file pour l’envoyer aux expions avant de renvoyer qu client le texte qu’il à dit.

func (server *SpyingechoServer) Spy(_ *spyingechopb.Empty, stream spyingechopb.SpyingEcho_SpyServer) error {
	// Add client to our spys list
	server.spies.Add(spy{stream: stream})
	log.Println("A new spy is connected")

	// Infinite loop to keep stream alive
	// Sending is manage by ses.dispatch method
	for {
		time.Sleep(time.Minute * 5)
	}
}

Quand un espion souhaite observer le serveur, nous l’ajoutons à notre liste d’espions. Une boucle infinie est en place afin de garder le stream toujours ouvert. Ce n’est pas cette fonction lit la file de message et qui envoie les messages aux espions. C’est la fonction suivante.

func (server *SpyingechoServer) dispatch() {
	for msg := range server.ers {
		server.spies.Dispatch(msg)
	}
}

Voici donc le fichier complet.

// ./server/spying_echo_server.go

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/albdewilde/spying_echo/grpc/spyingechopb"
)

// SypingEchoServer implements the  spyingechopb.SpyingechoServer interface
type SpyingechoServer struct {
	spyingechopb.UnimplementedSpyingEchoServer

	// spies regroup all spies that we'll send messages to
	spies Spies

	// ers is a channel where we put all messages to send to spys
	ers chan *spyingechopb.EchoRequest
}

func NewSpyingechoServer() *SpyingechoServer {
	server := &SpyingechoServer{
		spies: newSpies(),
		ers:   make(chan *spyingechopb.EchoRequest),
	}

	go server.dispatch()
	return server
}

func (server *SpyingechoServer) Echo(ctx context.Context, echoRequest *spyingechopb.EchoRequest) (*spyingechopb.EchoReply, error) {
	server.ers <- echoRequest

	log.Printf("%s said: %s", echoRequest.GetName(), echoRequest.GetMsg())

	return &spyingechopb.EchoReply{Msg: fmt.Sprintf("You said: %s", echoRequest.GetMsg())}, nil
}

func (server *SpyingechoServer) Spy(_ *spyingechopb.Empty, stream spyingechopb.SpyingEcho_SpyServer) error {
	// Add client to our spys list
	server.spies.Add(spy{stream: stream})
	log.Println("A new spy is connected")
 
	 // Infinite loop to keep stream alive
	// Sending is manage by ses.dispatch method
	for {
		time.Sleep(time.Minute * 5)
	}
}

func (server *SpyingechoServer) dispatch() {
	for msg := range server.ers {
		server.spies.Dispatch(msg)
	}
}

Ici nous avons donc tout ce qu’il faut pour avoir notre serveur en action. Je ne suis volontairement pas allé plus loin dans le fonctionnement de l’application car on s’éloigne de gRPC.

Un client

Ici, nous implémenter un client qui espionnera le serveur.

func main() {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))

	conn, err := grpc.Dial(fmt.Sprintf("%s:%d", HOST, PORT), opts...)
	if err != nil {
		log.Fatalf("fail to dial: %v", err)
	}
	defer conn.Close()

	client := spyingechopb.NewSpyingEchoClient(conn)

	spy(client)
}

Nous commençons par créer une connexion gRPC (lignes 2 à 9). Ensuite, nous créons notre client (ligne 11) et nous espionnons (ligne 13).

Voici l’implémentation de la fonction spy

func spy(c spyingechopb.SpyingEchoClient) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	stream, err := c.Spy(ctx, new(spyingechopb.Empty))
	if err != nil {
		log.Fatal(err)
	}

	for {
		msg, err := stream.Recv()

		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("error when receiving from stream: %s", err.Error())
		}

		fmt.Println(msg.GetMsg())
	}
}

Nous créons un contexte que nous allons annuler à la sortie de la fonction.
On récupère notre stream en appelant la méthode Spy (nom de la route rpc définit dans le fichier .proto) sur le client.
Ensuite, avec notre stream, nous allons, dans une boucle infinie, récupérer ce que le serveur nous envoie pour l’afficher dans la console.

Voici le fichier complet.

package main

import (
	"context"
	"fmt"
	"io"
	"log"

	"github.com/albdewilde/spying_echo/grpc/spyingechopb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	HOST = "0.0.0.0"
	PORT = 10000
)

func main() {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))

	conn, err := grpc.Dial(fmt.Sprintf("%s:%d", HOST, PORT), opts...)
	if err != nil {
		log.Fatalf("fail to dial: %v", err)
	}
	defer conn.Close()

	client := spyingechopb.NewSpyingEchoClient(conn)

	spy(client)
}

func spy(c spyingechopb.SpyingEchoClient) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	stream, err := c.Spy(ctx, new(spyingechopb.Empty))
	if err != nil {
		log.Fatal(err)
	}

	for {
		msg, err := stream.Recv()

		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("error when receiving from stream: %s", err.Error())
		}

		fmt.Println(msg.GetMsg())
	}
}

On remarque qu’avec gRPC nous n’avons eu seulement à implémenter le comportement du serveur. Côté client, nous n’avons pas eu de sérialisation manuelle (conversion en JSON et gestion des possibles erreurs) à faire. De la même manière, en cas d’erreur réseau, nous récupérons directement une erreur dans le langage utilisé pour développer le projet.
De plus, je n’ai pas eu à recréer les mêmes structures de données dans le projet client et dans le projet serveur qui sont échangés par ces derniers. Je les utilise comme une dépendance depuis le code généré par protoc.

Le projet d’exemple complet se trouve ici.

Conclusions

gRPC présente de nombreux avantages. Un des principaux est qu’il permet une abstraction des appels réseaux et sur la sérialisation des objets qui sont échangés entre les services. Il permet aussi un usage très facile du streaming entre le client et le serveur.

De plus, gRPC est multilangage. L’utiliser de nous enferme pas dans un langage de programmation entre tous les services.

Ce framework est basé sur un standard du web. Les risques que le protocole soit abandonné d’un jour à l’autre est assez faible.
Il est aussi ouvert et conçu par google. En utilisant gRPC on profite donc du support de google et de la communauté qui y contribue.


7 commentaires

Merci pour cette présentation ! :)

Une comparaison avec un API REST-like plus courant aurait pu avoir sa place je pense. ^^

+1 -0

Hello, je me permets un petit retour,

Il sera aussi bientôt disponible pour Flutter

Alors pas besoin de le mettre au futur, c’est totalement supporté. Je travaille depuis plus d’une année professionnellement avec une solution Serveur Go <-> gRPC <-> Clients Flutter.

Flutter tout comme Go ayant été développé par Google, ils ont dès le départ de gRPC (ou en tout cas rapidement) été bien supporté.

Il est disponible pour les applications backend, web, …

En revanche la, on ne peut pas dire que le web soit super à l’heure actuel. Nous sommes obligé de passer par un proxy (du type Envoy), car il n’y a actuellement pas d’API au niveau des navigateurs pour gérer la connexion HTTP/2, le proxy va donc servir d’intermédiaire Navigateur <- http/1 -> Envoy <-http/2 - gRPC-> Serveur gRPC.

A noter également que seul le Streaming server est supporté pour le web, le streaming client et bidirectionnel n’est pas encore supporté.


Dans l’introduction, il aurait été bien d’expliquer que des outils vont effectuer de la génération de code à partir des fichiers Protobuf, ce qui nous permet d’avoir un gain de temps face à une API Rest standard.

Voila une excellente techno que gRPC, bien vu de faire un article sur le sujet, j’apporte aussi mes 2 cents, c’est un concurrent de Apache Thrift (une techno moins connue crée par Facebook et utilisée dans la base NoSQL Cassandra). Elle a été open sourcé en 2015, mais je pense que ça faisait un petit bout de temps que c’était utilisé en interne chez Google.

En tout cas, dès que l’on parle perf, c’est à étudier, le package permet "out of the box" avoir des données optimisées (diminution de la bande passante utiliser), la gestion multi-thread qui va bien et tout ce qu’il faut pour faire du stream de manière transparente via HTTP2.

Un outil de cli est fourni directement avec la librairie https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md

Attention :

C’est un format de sérialisation de donnée comme l’est le JSON, le XML, le YAML…

Si c’était vrai, quel serait l’intérêt de l’utiliser ? Si c’est pour avoir un équivalent de JSON en binaire il y a Msgpack qui est également supporté dans tous les langages populaires, infiniment plus simple à utiliser, et très performant.

Protobuf diffère de JSON/XML/YAML/TOML et de Msgpack en ceci que c’est un format de sérialization basé sur un schéma. La définition du protocole en protobuf est ce qui fait foi, elle est partagée par quiconque veut parler à un service donné.

Ce n’est pas rien : ça en fait un point central évident sur lequel les équipes peuvent/doivent se mettre d’accord. Mieux, ça en fait l’unique source de vérité sur le protocole respecté par une application. C’est un point d’importance majeure en termes de collaboration.

+6 -0

Après, gRPC (et les RPC en général) sont quand même très orienté … “Procédures”1 … Ce qui encourage l’utilisation d’un protocole basés sur un schéma comme protobuf.

PS: Quelqu’un à essayé Cap’n Proto ?


  1. Je comprends pas l’utilisation du mot Procédure puisqu’ici, on retourne généralement une valeur typée
+0 -0

Salut, merci pour vos retours et vos commentaires.

Hello, je me permets un petit retour,

Il sera aussi bientôt disponible pour Flutter

Alors pas besoin de le mettre au futur, c’est totalement supporté. Je travaille depuis plus d’une année professionnellement avec une solution Serveur Go <-> gRPC <-> Clients Flutter. […]

Il est disponible pour les applications backend, web, …

En revanche la, on ne peut pas dire que le web soit super à l’heure actuel. Nous sommes obligé de passer par un proxy (du type Envoy), car il n’y a actuellement pas d’API au niveau des navigateurs pour gérer la connexion HTTP/2, le proxy va donc servir d’intermédiaire Navigateur <- http/1 -> Envoy <-http/2 - gRPC-> Serveur gRPC.

[…]

Dans l’introduction, il aurait été bien d’expliquer que des outils vont effectuer de la génération de code à partir des fichiers Protobuf, ce qui nous permet d’avoir un gain de temps face à une API Rest standard.

WinXaito

Sur la partie sur Flutter je me suis basé sur la doc de gRPC. Il y est indique que le framework est compatible avec Dart (qui est le langage avec lequel on utilise Flutter) mais que cependant la documentation de gRPC concernant Flutter est à venir. Je ne connais pas assez Dart ou Flutter pour être précis pour ce sujet. Merci pour tes précisions.

Au niveau web, pareille j’ai surtout suivi la documentation dans aller chercher plus loin. Il est effectivement détaillé qu’il faut configurer un proxi. Je me demande si ce n’est pas cependant plus facile d’utiliser gRPC-gateway qui semble faire plus de chose à notre place. De la sorte, la web app enverra des requettes http classique ce qui ne changera pas les habitudes des développeurs front.

Ce que tu dis à propos de la génération de code rejoins ce que dit nohar ici.

[…]
Protobuf diffère de JSON/XML/YAML/TOML et de Msgpack en ceci que c’est un format de sérialization basé sur un schéma. La définition du protocole en protobuf est ce qui fait foi, elle est partagée par quiconque veut parler à un service donné.

Ce n’est pas rien : ça en fait un point central évident sur lequel les équipes peuvent/doivent se mettre d’accord. Mieux, ça en fait l’unique source de vérité sur le protocole respecté par une application. C’est un point d’importance majeure en termes de collaboration.

nohar

Je pense que c’est la partie tellement grosse et évidente que je suis passé à coté. C’est basé sur un schéma.
Il y a d’autre mécanismes qui peuvent aussi permettre cela, de crée un schéma pour en suité généré son API. Il me semble que c’est faisable avec swagger. En définisant un fichier YAML on peut générer une interface pour notre serveur.

Merci beaucoup à tous pour vos retours.

+0 -0

Il y a d’autre mécanismes qui peuvent aussi permettre cela, de crée un schéma pour en suité généré son API. Il me semble que c’est faisable avec swagger. En définisant un fichier YAML on peut générer une interface pour notre serveur.

pyoroalb

Oui, et ça permet d’ailleurs de mieux comprendre pourquoi gRPC utilise un langage spécifique (protobuf), avec une syntaxe dédiée. Les déclarations de ce genre en YAML (mais en JSON et XML aussi, hein) sont extrêmement verbeuses et vraiment pas commodes à écrire à la main.

+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte