Créez votre premier serveur gRPC en Golang

Apprenez à créer votre premier serveur gRPC en Golang. Suivez un guide étape par étape pour comprendre les concepts clés, configurer votre environnement de développement, et déployer un microservice fonctionnel.

Un serveur gRPC en Golang est un composant qui écoute les requêtes des clients et y répond en utilisant le protocole gRPC. Il permet de créer des microservices performants et scalables grâce à la communication basée sur HTTP/2 et les protocoles de sérialisation efficaces comme Protocol Buffers.

Pourquoi utiliser gRPC avec Golang

gRPC offre plusieurs avantages lorsqu'il est utilisé avec Golang :

  • Performance : gRPC utilise HTTP/2, ce qui permet des communications plus rapides et plus efficaces entre les services.
  • Sérialisation efficace : Protocol Buffers est un format de sérialisation binaire compact, ce qui réduit la taille des messages et améliore la vitesse de transmission.
  • Contrats stricts : Les fichiers .proto définissent clairement les messages et services, assurant une communication fiable entre les clients et les serveurs.
  • Compatibilité multi-langages : gRPC prend en charge plusieurs langages, facilitant l'interopérabilité entre différents services écrits dans différents langages.
  • Scalabilité : Grâce à HTTP/2 et à la gestion efficace des connexions, gRPC est bien adapté aux architectures de microservices nécessitant une communication intensive entre services.

Création du projet Golang

Créer le projet Golang et initialiser le module avec les dépendances nécessaires pour gRPC et Protocol Buffers :

go mod init insee-grpc
go get google.golang.org/grpc
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

Votre projet devrait avoir la structure suivante :

.
├── go.mod
├── go.sum
├── insee.proto
├── main.go
├── Makefile
└── README.md

Créer le fichier insee.proto pour définir le service gRPC et les messages associés :

insee.proto
syntax = "proto3";

package insee;

option go_package = "services/goproto/insee";

The fichier utilisent deux principaux entêtes : syntax et package. L'entête syntax spécifie la version de Protocol Buffers utilisée, tandis que l'entête package définit le namespace pour les messages et services définis dans le fichier.

Finalement, l'entête option go_package spécifie le chemin du package Go généré pour le fichier .proto. Cela permet de structurer correctement le code généré et de l'importer facilement dans votre projet Go.

Définition des messages et services gRPC

Le serveur grPC communique par voie de messages définis dans le fichier .proto. Chaque message représente une structure de données échangée entre le client et le serveur, et chaque service définit les méthodes RPC disponibles pour interagir avec ces messages.

Nous allons définir un service gRPC appelé InseeService avec des méthodes pour interagir avec les messages SirenRequest, SirenResponse, SiretRequest et SiretResponse:

insee.proto
service InseeService {
    rpc Siren (SirenRequest) returns (SirenResponse);
    rpc Siret (SiretRequest) returns (SiretResponse);
}

message SirenRequest {
    string siren = 1;
}

message SirenResponse {
    repeated Business items = 1;
}

message SiretRequest {
    string siren = 1;
}

message SiretResponse {
    repeated Business items = 1;
}

message Business {
    string name = 1;
    string city = 2;
}

Service

Le service dans un proto est une collection de méthodes RPC qui peuvent être appelées par les clients. Chaque méthode spécifie le type de requête qu'elle attend et le type de réponse qu'elle renvoie.

Messages

Les messages dans un proto définissent la structure des données échangées entre le client et le serveur. Chaque champ d'un message a un type, un nom et un numéro unique. Les champs peuvent être simples (comme string ou int32) ou complexes (comme d'autres messages). Le mot-clé repeated indique une liste de valeurs du type spécifié.

Chaque message intègre un numéro unique pour chaque champ, ce qui permet de les identifier de manière fiable lors de la sérialisation et de la désérialisation des messages.

Voici le fichier complet insee.proto :

insee.proto
syntax = "proto3";

package insee;

option go_package = "services/goproto/insee";

service InseeService {
    rpc Siren (SirenRequest) returns (SirenResponse);
    rpc Siret (SiretRequest) returns (SiretResponse);
}

message SirenRequest {
    string siren = 1;
}

message SirenResponse {
    repeated Business items = 1;
}

message SiretRequest {
    string siren = 1;
}

message SiretResponse {
    repeated Business items = 1;
}

message Business {
    string name = 1;
    string city = 2;
}

Génération du code gRPC

Pour générer le code gRPC à partir du fichier insee.proto, vous pouvez utiliser l'outil protoc avec les plugins appropriés pour votre langage de programmation. Par exemple, pour Go :

protoc --go_out=./inseeproto --go_opt=paths=source_relative --go-grpc_out=./inseeproto --go-grpc_opt=paths=source_relative insee.proto
Si le plugin protoc-gen-go ou protoc-gen-go-grpc n'est pas installé, vous devez l'installer avant de générer le code gRPC.
# 1. Installez les plugins

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# 2. Ajouter le répertoire des plugins Go au PATH

export PATH="$PATH:$(go env GOPATH)/bin"

# 3. Vérifier l'installation

which protoc-gen-go
which protoc-gen-go-grpc

Une fois le code générer, votre répertoire devrait ressembler à ceci :

.
├── insee.proto
├── inseeproto
│   ├── insee.pb.go
│   └── insee_grpc.pb.go

Définition des services gRPC

Une fois le code généré, vous pouvez implémenter le serveur gRPC en Go en créant une structure qui satisfait l'interface générée pour le service InseeService. Vous devrez ensuite enregistrer cette structure auprès d'un serveur gRPC et démarrer le serveur pour écouter les requêtes entrantes.

Voici un exemple de mise en œuvre du serveur gRPC en Go :

internal/server.go
package internal

import (
    "context"

    pb "github.com/Zadigo/goproto/inseeproto"
)

type Server struct {
    pb.UnimplementedInseeServiceServer
}

Dans un premier temps, nous définissons une structure Server qui implémente l'interface InseeServiceServer générée par le code gRPC. Cette structure servira de récepteur pour les méthodes du service gRPC.

Ensuite, vous pouvez ajouter les méthodes Siren et Siret à la structure Server pour gérer les requêtes correspondantes. Ces méthodes doivent respecter les signatures définies dans l'interface InseeServiceServer.

internal/server.go
func (s *Server) Siren(ctx context.Context, req *pb.SirenRequest) (*pb.SirenResponse, error) {
    items := []*pb.Business{
        {
            Name: "Leclerc",
            City: "Lille",
        },
        {
            Name: "Carrefour",
            City: "Lille",
        },
    }

    return &pb.SirenResponse{
        Items: items,
    }, nil
}

func (s *Server) Siret(ctx context.Context, req *pb.SiretRequest) (*pb.SiretResponse, error) {
    items := []*pb.Business{
        {
            Name: "Banque Populaire",
            City: "Paris",
        },
        {
            Name: "Crédit Agricole",
            City: "Marseille",
        },
    }

    return &pb.SiretResponse{
        Items: items,
    }, nil
}
Remarquez comment les fonctions de la structure retournent des réponses conformes aux types définis dans l'interface InseeServiceServer.

Création du routeur

Nous allons maintenant créer un routeur pour notre serveur gRPC afin de gérer les différentes requêtes entrantes. Cela implique de créer un serveur gRPC, d'enregistrer notre service et de démarrer le serveur pour écouter sur un port spécifique.

internal/app.go
package internal

import (
    "context"
    "fmt"
    "log"
    "net"

    pb "github.com/Zadigo/goproto/inseeproto"
    "google.golang.org/grpc"
)

type App struct {
    ctx context.Context
    errCh chan error
}

func (a *App) Start() {
    fmt.Print("Starting server...")

    listener, err := net.Listen("tcp", ":9001")
    if err != nil {
        log.Fatalf("Could not start TCP server %b", err)
    }

    defer listener.Close()

    grpcServer := grpc.NewServer()
    pb.RegisterInseeServiceServer(grpcServer, &Server{})
    a.errCh <- grpcServer.Serve(listener)

    select {
    case err := <- a.errCh:
        fmt.Printf("Error running server %s", err)
    case err :=  <- a.ctx.Done():
        close(a.errCh)
        fmt.Printf("Closing server %s", err)
    }
}

func NewApp(ctx context.Context) *App {
    return &App{
        ctx: ctx,
        errCh: make(chan error, 1),
    }
}
La structure App fait plusieurs choses. Elle un serveur TCP sur le port 9001:
listener, err := net.Listen("tcp", ":9001")
Puis enregistre un serveur gRPC avec le service InseeServiceServer:
grpcServer := grpc.NewServer()
pb.RegisterInseeServiceServer(grpcServer, &Server{})
a.errCh <- grpcServer.Serve(listener)

Lancement du serveur gRPC

Dans notre fichier main.go, nous créons une instance de l'application et démarrons le serveur:

main.go
package main

import (
    "context"

    "github.com/Zadigo/goproto/internal"
)

func main() {
    ctx := context.Background()
    instance := internal.NewApp(ctx)
    instance.Start()
}

Pour vérifier que le serveur fonctionne correctement, vous pouvez ouvrir un client gRPC et envoyer des requêtes aux différentes méthodes exposées par le service InseeServiceServer.


Automatisation du test du serveur gRPC

Vous pouvez automatiser la génération des fichiers nécessaires pour le serveur gRPC en utilisant un Makefile avec les cibles suivantes :

install_proto_tools:
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

build_proto:
    protoc --go_out=./inseeproto --go_opt=paths=source_relative --go-grpc_out=./inseeproto --go-grpc_opt=paths=source_relative insee.proto

Puis:

make install_proto_tools
make build_proto

Projet final

insee.proto
syntax = "proto3";

package insee;

option go_package = "services/goproto/insee";

service InseeService {
    rpc Siren (SirenRequest) returns (SirenResponse);
    rpc Siret (SiretRequest) returns (SiretResponse);
}

message SirenRequest {
    string siren = 1;
}

message SirenResponse {
    repeated Business items = 1;
}

message SiretRequest {
    string siren = 1;
}

message SiretResponse {
    repeated Business items = 1;
}

message Business {
    string name = 1;
    string city = 2;
}
main.go
package main

import (
    "context"

    "github.com/Zadigo/goproto/internal"
)

func main() {
    ctx := context.Background()
    instance := internal.NewApp(ctx)
    instance.Start()
}
internal/app.go
package internal

import (
    "context"
    "fmt"
    "log"
    "net"

    pb "github.com/Zadigo/goproto/inseeproto"
    "google.golang.org/grpc"
)

type App struct {
    ctx context.Context
    errCh chan error
}

func (a *App) Start() {
    fmt.Print("Starting server...")

    listener, err := net.Listen("tcp", ":9001")
    if err != nil {
        log.Fatalf("Could not start TCP server %b", err)
    }

    defer listener.Close()

    grpcServer := grpc.NewServer()
    pb.RegisterInseeServiceServer(grpcServer, &Server{})
    a.errCh <- grpcServer.Serve(listener)

    select {
    case err := <- a.errCh:
        fmt.Printf("Error running server %s", err)
    case err :=  <- a.ctx.Done():
        close(a.errCh)
        fmt.Printf("Closing server %s", err)
    }
}

func NewApp(ctx context.Context) *App {
    return &App{
        ctx: ctx,
        errCh: make(chan error, 1),
    }
}
internal/utils.go
package internal

import (
    "context"

    pb "github.com/Zadigo/goproto/inseeproto"
)

type Server struct {
    pb.UnimplementedInseeServiceServer
}

func (s *Server) Siren(ctx context.Context, req *pb.SirenRequest) (*pb.SirenResponse, error) {
    items := []*pb.Business{
        {
            Name: "Leclerc",
            City: "Lille",
        },
        {
            Name: "Carrefour",
            City: "Lille",
        },
    }

    return &pb.SirenResponse{
        Items: items,
    }, nil
}

func (s *Server) Siret(ctx context.Context, req *pb.SiretRequest) (*pb.SiretResponse, error) {
    items := []*pb.Business{
        {
            Name: "Banque Populaire",
            City: "Paris",
        },
        {
            Name: "Crédit Agricole",
            City: "Marseille",
        },
    }

    return &pb.SiretResponse{
        Items: items,
    }, nil
}

Structure final du projet:

.
├── go.mod
├── go.sum
├── insee.proto
├── inseeproto
│   ├── insee_grpc.pb.go
│   └── insee.pb.go
├── internal
│   ├── app.go
│   └── utils.go
├── main.go
├── Makefile
└── README.md