1. Le Mécanisme des Métadonnées (metadata) dans gRPC
gRPC permet d'appeler des fonctions distantes comme des fonctions locales. Chaque appel RPC peut transporter des informations utiles via le mécanisme de métadonnées. Les métadonnées sont stockées sous forme de paires clé-valeur, où la clé est une string et la valeur est un tableau de chaînes de caractères ([]string).
Les métadonnées permettent au client et au serveur de s'échanger des informations contextuelles sur un appel, à l'image des en-têtes de requête et de réponse HTTP. La durée de vie des métadonnées est celle de l'appel RPC.
// Création de métadonnées
// Le type MD est une map avec des clés string et des valeurs []string.
type MD map[string][]string
// Première méthode : utiliser New()
md := metadata.New(map[string]string{"cle1": "valeur1", "cle2": "valeur2"})
// Deuxième méthode : utiliser Pairs()
// Les clés sont insensibles à la casse et sont converties en minuscules.
md := metadata.Pairs(
"Cle1", "valeur1",
"Cle1", "valeur1_2", // "Cle1" aura pour valeur []string{"valeur1", "valeur1_2"}
"Cle2", "valeur2",
)
Envoi de métadonnées (côté client)
md := metadata.Pairs("cle", "valeur")
// Créer un contexte avec les métadonnées
ctx := metadata.NewOutgoingContext(context.Background(), md)
// Appel RPC simple
reponse, err := client.MonRPC(ctx, maRequete)
Réception de métadonnées (côté serveur)
func (s *MonServeur) MonRPC(ctx context.Context, in *pb.MaRequete) (*pb.MaReponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
// Traitement des métadonnées
}
}
1.1 Fichier Proto
syntax = "proto3";
option go_package = ".;proto";
service Hello {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string reply = 1;
}
1.2 Génération des fichiers Go
protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
1.3 Code serveur
package main
import (
"context"
"fmt"
"net"
"go_test_learn/meta_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type HelloServer struct{}
func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
for key, values := range md {
fmt.Println(key, values)
}
if pwd, exists := md["password"]; exists {
fmt.Println(pwd[0])
}
} else {
fmt.Println("Échec de récupération des métadonnées")
}
fmt.Println(req.Name)
return &proto.HelloResponse{Reply: "Données reçues du client : " + req.Name}, nil
}
func main() {
g := grpc.NewServer()
proto.RegisterHelloServer(g, &HelloServer{})
listener, err := net.Listen("tcp", "0.0.0.0:50052")
if err != nil {
panic("Échec du démarrage du serveur")
}
g.Serve(listener)
}
1.4 Code client
package main
import (
"context"
"fmt"
"go_test_learn/meta_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("Échec de la connexion au serveur")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
req := &proto.HelloRequest{Name: "alice"}
// Création des métadonnées
md := metadata.New(map[string]string{"name": "alice", "password": "456"})
ctx := metadata.NewOutgoingContext(context.Background(), md)
res, err := client.Hello(ctx, req)
if err != nil {
panic("Échec de l'appel RPC")
}
fmt.Println(res.Reply)
}
2. Les Intercepteurs (Interecptors) dans gRPC
Les intercepteurs sont un mécanisme puissant permettant d'exécuter une logique personnalisée avant ou après un appel RPC, à la manière des middlewares dans Gin. Ils sont utiles pour l'authentification, la journalisation, etc.
2.1 Intercepteur serveur (grpc.UnaryInterceptor)
Le type de la fonction interceptrice est : type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
// Exemple d'intercepteur
intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("Nouvelle requête reçue")
debut := time.Now()
res, err := handler(ctx, req)
fmt.Println("Requête terminée")
fmt.Println("Durée :", time.Since(debut))
return res, err
}
opt := grpc.UnaryInterceptor(intercepteur)
g := grpc.NewServer(opt)
2.2 Intercepteur client (UnaryClientInterceptor)
Le type de la fonction interceptrice est : type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
intercepteur := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
debut := time.Now()
fmt.Println("Intercepteur client")
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("Durée : %s\n", time.Since(debut))
return err
}
opt := grpc.WithUnaryInterceptor(intercepteur)
conn, err := grpc.Dial("127.0.0.1:50052", opt, grpc.WithTransportCredentials(insecure.NewCredentials()))
2.3 Intercepteurs Open Source
La bibliothèque go-grpc-middleware fournit des intercepteurs prêts à l'emploi.
// Exemple avec grpc_auth et grpc_recovery
maFonctionAuth := func(ctx context.Context) (context.Context, error) {
fmt.Println("Authentification effectuée")
return ctx, nil
}
opt := grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_auth.UnaryServerInterceptor(maFonctionAuth),
grpc_recovery.UnaryServerInterceptor(),
))
g := grpc.NewServer(opt)
3. Authentification avec Métadonnées et Intercepteurs
3.1 Implémentation personnalisée
Côté serveur :```
package main
import ( "context" "fmt" "net" "go_test_learn/interpret_proto/proto" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" )
type HelloServer struct{}
func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) { fmt.Println(req.Name) return &proto.HelloResponse{Reply: "Données reçues du client : " + req.Name}, nil }
func main() { intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { fmt.Println("Vérification des identifiants") md, ok := metadata.FromIncomingContext(ctx) if !ok { return resp, status.Error(codes.Unauthenticated, "Aucune information d'authentification") } nom := "" motDePasse := "" if names, exists := md["name"]; exists { nom = names[0] } if pass, exists := md["password"]; exists { motDePasse = pass[0] } if nom == "admin" && motDePasse == "secret" { return handler(ctx, req) } return resp, status.Error(codes.Unauthenticated, "Identifiants incorrects") } g := grpc.NewServer(grpc.UnaryInterceptor(intercepteur)) proto.RegisterHelloServer(g, &HelloServer{}) listener, _ := net.Listen("tcp", "0.0.0.0:50052") g.Serve(listener) }
**Côté client :**```
package main
import (
"context"
"fmt"
"time"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func main() {
intercepteur := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
fmt.Println("Ajout des identifiants via l'intercepteur client")
md := metadata.New(map[string]string{"name": "admin", "password": "secret"})
ctx = metadata.NewOutgoingContext(context.Background(), md)
return invoker(ctx, method, req, reply, cc, opts...)
}
conn, err := grpc.Dial("127.0.0.1:50052",
grpc.WithUnaryInterceptor(intercepteur),
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("Échec de la connexion")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
res, err := client.Hello(context.Background(), &proto.HelloRequest{Name: "test"})
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res.Reply)
}
}
3.2 Utilisation de WithPerRPCCredentials
Côté serveur : (identique à l'exemple précédent)```
// Le code serveur reste le même que dans 3.1
**Côté client (utilisant l'interface intégrée) :**```
package main
import (
"context"
"fmt"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Implémentation de l'interface PerRPCCredentials
type MesIdentifiants struct{}
func (m MesIdentifiants) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"name": "admin",
"password": "secret",
}, nil
}
func (m MesIdentifiants) RequireTransportSecurity() bool {
return false
}
func main() {
opt := grpc.WithPerRPCCredentials(MesIdentifiants{})
conn, err := grpc.Dial("127.0.0.1:50052",
grpc.WithTransportCredentials(insecure.NewCredentials()),
opt)
if err != nil {
panic("Échec de la connexion")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
res, err := client.Hello(context.Background(), &proto.HelloRequest{Name: "alice"})
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res.Reply)
}
}
4. Validation des Champs avec protoc-gen-validate
La bibliothèque protoc-gen-validate permet de générer des validations à partir des annotations dans le fichier proto.
4.1 Fichier Proto
syntax = "proto3";
import "validate.proto";
option go_package = "./;proto";
service Hello {
rpc Hello(Person) returns (Person);
}
message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];
string email = 2 [(validate.rules).string.email = true];
string mobile = 3 [(validate.rules).string = {
pattern: "^1[3-9][0-9]{9}$",
}];
}
4.2 Génération
protoc --go_out=. --validate_out="lang=go:." hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false --validate_out="lang=go:." hello.proto
4.3 Serveur avec validation
package main
import (
"context"
"fmt"
"net"
"awesomeProject/valdiate_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type HelloServer struct{}
func (s *HelloServer) Hello(ctx context.Context, req *proto.Person) (*proto.Person, error) {
if err := req.Validate(); err != nil {
return nil, err
}
return &proto.Person{Id: 1000, Email: "test@example.com", Mobile: "18953675221"}, nil
}
type Validator interface {
Validate() error
}
func main() {
intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if v, ok := req.(Validator); ok {
if err := v.Validate(); err != nil {
return resp, status.Error(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
g := grpc.NewServer(grpc.UnaryInterceptor(intercepteur))
proto.RegisterHelloServer(g, &HelloServer{})
listener, _ := net.Listen("tcp", "0.0.0.0:50052")
g.Serve(listener)
}
4.4 Client
package main
import (
"context"
"fmt"
"awesomeProject/valdiate_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("Échec de la connexion")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
req := &proto.Person{
Id: 1000,
Email: "user@example.com",
Mobile: "18956362551",
}
res, err := client.Hello(context.Background(), req)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res.Mobile)
}
}
5. Codes d'État gRPC
Les codes d'état gRPC standards sont définis dans la documentation officielle : Status Codes.
6. Gestion des Erreurs en gRPC
6.1 Côté serveur
status.Error(codes.Unauthenticated, "Aucune information d'authentification")
status.New(codes.InvalidArgument, "Argument invalide").Err()
status.Newf(codes.NotFound, "Ressource %s introuvable", "mon_id").Err()
6.2 Côté client
s, ok := status.FromError(err)
if !ok {
fmt.Println(s.Message())
fmt.Println(s.Code())
}
7. Gestion des Délais (Timeouts) en gRPC
7.1 Côté client
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
res, err := client.Hello(ctx, &request)
if err != nil {
s, ok := status.FromError(err)
if ok {
fmt.Println(s.Message())
fmt.Println(s.Code())
}
}
7.2 Côté serveur (simulation d'un traitement long)
func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(req.Name)
fmt.Println("Attente de 5 secondes...")
time.Sleep(5 * time.Second)
return &proto.HelloResponse{Reply: "Données reçues : " + req.Name}, nil
}