Créez votre premier serveur gRPC en Golang
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
.protodé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 :
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:
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 :
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
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 :
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.
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
}
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.
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),
}
}
App fait plusieurs choses. Elle un serveur TCP sur le port 9001:listener, err := net.Listen("tcp", ":9001")
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:
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
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;
}
package main
import (
"context"
"github.com/Zadigo/goproto/internal"
)
func main() {
ctx := context.Background()
instance := internal.NewApp(ctx)
instance.Start()
}
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),
}
}
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