Introduction

gRPC c'est quoi ?

gRPC a été développé initialement par Google. Il permet de réaliser des clients et serveurs RPC (Remote Procedure Call) via HTTP/2 avec Protocol Buffers.

Je vous invite à lire les articles de notre blog expliquant comment fonctionne Protobuf et comment fonctionne gRPC.

Qu'allons-nous faire ?

Dans ce tutoriel nous allons mettre en place un serveur gRPC en Go utilisant l'API Translate de Google.

Vous pouvez retrouver l'ensemble du code sur le github des donuts-factory.

Le but est de comprendre :

  • la déclaration d'un service gRPC via le fichier protobuf
  • la mise en place d'un serveur gRPC
  • l'utilisation de l'outil prototool
  • la mise en place d'un proxy REST pour pouvoir l'appeler depuis le web

Prérequis

  • Installer Go 1.9 ou 1.10
  • Créer un dossier translator-service dans le dossier $GOPATH/src
  • Installer Dep

Installation des outils

Installer protoc

protoc est un générateur qui va lire vos fichiers Protobuf et générer du code.

Si vous êtes sur Linux :

PROTOC_ZIP=protoc-3.3.0-osx-x86_64.zip curl -OL https://github.com/google/protobuf/releases/download/v3.3.0/$PROTOC_ZIP sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc rm -f $PROTOC_ZIP

Si vous êtes sur Mac OS X :

brew install protobuf

Si vous êtes sur Windows, vous pouvez télécharger l'exécutable ici.

Installer prototool

prototool est une commande qui va vous permettre d'utiliser plus facilement protoc via un fichier yaml. Il intègre aussi un linter et un client gRPC que nous verrons à l'étape 5.

L'installer avec Go :

go get -u github.com/uber/prototool/cmd/prototool

Déclaration du Protobuf

Notre fichier Protobuf

Nous allons créer un fichier translator.proto dans le dossier proto.

syntax = "proto3"; package proto;

Nous allons déclarer dans ce fichier un service gRPC. La méthode Translate aura comme payload TranslateRequest et retournera TranslateResponse.

service Translator { rpc Translate(TranslateRequest) returns (TranslateResponse) {} }

Nous allons maintenant déclarer les messages TranslateRequest et TranslateResponse.

message TranslateRequest { string text = 1; Language language = 2; } message TranslateResponse { string text = 1; }

Petite subtilité ici, les chaînes de caractères ne sont pas compressées avec protobuf. Afin d'optimiser les traitements, on déclare pour language que les valeurs possibles sont en et fr. Pour ce faire on déclare une enum Language.

enum Language { en = 0; fr = 1; }

Notre fichier Protobuf est terminé et devrait ressembler à ça :

syntax = "proto3"; package proto; enum Language { en = 0; fr = 1; } message TranslateRequest { string text = 1; Language language = 2; } message TranslateResponse { string text = 1; } service Translator { rpc Translate(TranslateRequest) returns (TranslateResponse) {} }

Génération avec Prototool

Nous allons commencer par générer le fichier de config de Prototool.

prototool init

Nous allons maintenant éditer la config pour qu'il génère notre service gRPC en Go.

gen: go_options: import_path: translator-service/ plugins: - name: go type: go flags: plugins=grpc output: .

Nous pouvons maintenant générer les fichiers Go.

prototool gen

Dans le dossier proto , nous avons maintenant un fichier translator.pb.go.

Mise en place de la Translate API de Google

Nous allons maintenant mettre en place l'API Translate de Google.

Commencez par récupérer une clé API pour Translate. Il suffit de vous inscrire et de profiter de l'offre gratuite de Google Cloud Platform.

Nous allons créer un package translate qui va utiliser l'API de Google.

// translate.go package translate import ( "context" "log" "cloud.google.com/go/translate" "golang.org/x/text/language" "google.golang.org/api/option" ) type Translator interface { Translate(targetLanguage string, text string) (string, error) } type GoogleTranslator struct { client *translate.Client } func NewGoogleTranslator(apiKey string) *GoogleTranslator { ctx := context.Background() client, err := translate.NewClient(ctx, option.WithAPIKey(apiKey)) if err != nil { log.Fatal(err) } return &GoogleTranslator{ client: client, } } func (t GoogleTranslator) Translate(targetLanguage string, text string) (string, error) { ctx := context.Background() lang, err := language.Parse(targetLanguage) if err != nil { return "", err } res, err := t.client.Translate(ctx, []string{text}, lang, nil) if err != nil { return "", err } return res[0].Text, nil }

Lancez la commande dep ensure pour installer les packages qui vous manquent.

Nous allons modifier la factory de TranslateEndpoint.

// endpoint.go func NewTranslateEndpoint(t translate.Translator) TranslateEndpoint { return func(ctx context.Context, req *proto.TranslateRequest) (*proto.TranslateResponse, error) { text, err := t.Translate(req.Language.String(), req.Text) if err != nil { return nil, err } return proto.TranslateResponse{Text: text}, nil } }

Nous pouvons maintenant modifier le main.go pour ajouter le service à l'endpoint.

// main.go // ... translator := translate.NewGoogleTranslator(os.Getenv("TRANSLATION_API_KEY")) srv := server.NewTranslatorServer(server.Endpoints{ TranslateEndpoint: server.NewTranslateEndpoint(translator), }) // ...

Nous pouvons maintenant compiler notre serveur.

TRANSLATION_API_KEY=yourapitoken go run main.go

Mise en place du client gRPC

gRPC client avec prototool

Nous allons commencer par tester notre service avec prototool. Prototool va permettre de transformer un json en protobuf et d'appeler le serveur gRPC.

Nous allons créer un fichier payload.json.

{ "text": "Salut les astronautes !", "language": "en" }

Nous allons maintenant appeler notre serveur gRPC.

cat payload.json | prototool grpc proto/translator.proto 0.0.0.0:4000 proto.Translator/Translate -

gRPC client avec Go

Nous allons créer un simple fichier client.go pour appeler le serveur gRPC avec le code qui a été généré.

// client.go package main import ( "context" "log" "google.golang.org/grpc" "translator-service/proto" ) func main() { conn, err := grpc.Dial("localhost:4000", grpc.WithInsecure()) if err != nil { log.Fatalln(err) } defer conn.Close() client := proto.NewTranslatorClient(conn) res, err := client.Translate( context.Background(), &proto.TranslateRequest{Text:"Salut les astronautes !", Language: proto.Language_en}, ) if err != nil { log.Fatalln(err) } log.Println(res.Text) }

Nous allons maintenant appeler notre serveur gRPC avec notre client en Go.

go run client.go

Ajout d'un proxy REST et d'une doc Swagger

Nous allons maintenant voir comment exposer un service gPRC comme une API REST. Puisqu'un serveur gRPC n'est pas disponible pour le web, l'une des solutions est de créer un autre service qui va exposer une route REST et appeler le service gRPC.

grpc-gateway est un plugin protoc pour auto-générer un proxy HTTP via de la conf dans le fichier protobuf.

    HTTP request
        |
        v
 --------------                   ---------------
|  HTTP Proxy  |   json/proto    |  gRPC Server  |
|   on :8001   | ------------->  |   on :4000    |
 --------------  <-------------   ---------------

Nous allons commencer par installer les plugins grpc-gateway et swagger pour protoc.

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

Nous allons modifier le fichier proto/translator.proto pour ajouter les directives de génération du proxy HTTP. Pour ce faire, il faut ajouter des options à notre endpoint RPC.

option (google.api.http) = { post: "/v1/translate" body: "*" };

Ici, on définit une route /v1/translate avec le verbe POST.

Ce qui nous donne :

syntax = "proto3"; package proto; import "google/api/annotations.proto"; enum Language { en = 0; fr = 1; } message TranslateRequest { string text = 1; Language language = 2; } message TranslateResponse { string text = 1; } service Translator { rpc Translate(TranslateRequest) returns (TranslateResponse) { option (google.api.http) = { post: "/v1/translate" body: "*" }; } }

Nous allons maintenant modifer le fichier prototool.yaml pour générer le proxy et le json de Swagger.

# prototool.yaml protoc_includes: - ../../src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis gen: go_options: import_path: translator-service/ plugins: - name: go type: go flags: plugins=grpc output: . - name: grpc-gateway type: go output: . - name: swagger type: go output: swagger/.

Nous pouvons maintenant générer les fichiers Go.

prototool gen

Nous allons maintenant utiliser ce proxy qui a été généré et créer un fichier proxy.go. Il suffit de lancer le serveur gRPC dans une goroutine et d'exposer le proxy HTTP.

// proxy.go package main import ( "context" "net/http" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc" "translator-service/proto" ) func main() { lis, err := net.Listen("tcp", "localhost:4000") if err != nil { log.Fatalf("failed to listen: %v", err) } translator := translate.NewGoogleTranslator(os.Getenv("TRANSLATION_API_KEY")) srv := server.NewTranslatorServer(server.Endpoints{ TranslateEndpoint: server.NewTranslateEndpoint(translator), }) s := grpc.NewServer() proto.RegisterTranslatorServer(s, srv) go s.Serve(lis) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} proto.RegisterTranslatorHandlerFromEndpoint(ctx, mux, "localhost:4000", opts) http.ListenAndServe(":8001", mux) }

Nous pouvons maintenant compiler notre serveur.

TRANSLATION_API_KEY=yourapitoken go run main.go

Et vérifier que cela fonctionne bien :

curl -X POST http://localhost:8001/v1/translate \ [±master ✓] -H 'Content-Type: application/json' \ -d '{ "text": "Salut les astronautes !", "language": "en" }'

On peut remarquer qu'un fichier json a été aussi généré dans le dossier swagger/proto. Il s'agit de la documentation Swagger qui a été générée à partir des directives présentes dans le fichier protobuf.

Vous pouvez ouvrir la documentation directement ici ou directement utiliser swagger-ui.

Conclusion

Nous avons maintenant un service documenté accessible via gRPC ou plus classiquement par HTTP. Je vous conseille de regarder plus en détails les plugins protoc notamment gogoprotobuf qui est une autre implémentation de protobuf en Go et go-proto-validators qui permet de valider les messages protobuf comme des champs obligatoires ou des regex.