gRPC, un framework d'appel de procédure à distance (RPC) open source, permet une communication efficace et évolutive entre les services. Un aspect crucial de gRPC est la gestion des délais, des délais d'attente des demandes et la propagation du contexte, y compris les structures personnalisées.
Comprendre ces mécanismes permet de garantir que les services répondent rapidement, que les ressources ne sont pas gaspillées dans des opérations qui dépassent un délai raisonnable et que les métadonnées personnalisées sont transmises efficacement.
Une date limite dans gRPC spécifie le délai maximum dans lequel une opération doit être terminée. Si l’opération n’est pas réalisée dans ce délai, elle sera automatiquement terminée. Les délais sont essentiels pour garantir que les ressources système ne sont pas bloquées indéfiniment en raison de services qui ne répondent pas ou qui sont lents.
Un délai d'attente de requête est une période pendant laquelle un client est prêt à attendre une réponse du serveur. Si le serveur ne répond pas dans ce délai, la requête est abandonnée. Ce mécanisme empêche le client de rester indéfiniment en attente d'une réponse.
gRPC fournit des options flexibles pour définir des délais et demander des délais d'attente tant du côté client que du côté serveur. Voici comment procéder dans Go :
import ( "context" "log" "time" "google.golang.org/grpc" pb "path/to/your/protobuf/package" ) func main() { conn, err := grpc.Dial("server_address", grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := pb.NewYourServiceClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := client.YourMethod(ctx, &pb.YourRequest{}) if err != nil { log.Fatalf("could not call method: %v", err) } log.Printf("Response: %v", resp) }
Côté serveur, gRPC vous permet d'imposer des délais et de gérer les scénarios dans lesquels le délai spécifié par le client est dépassé :
import ( "context" "log" "net" "time" "google.golang.org/grpc" pb "path/to/your/protobuf/package" ) type server struct { pb.UnimplementedYourServiceServer } func (s *server) YourMethod(ctx context.Context, req *pb.YourRequest) (*pb.YourResponse, error) { select { case <-time.After(10 * time.Second): return &pb.YourResponse{}, nil case <-ctx.Done(): return nil, ctx.Err() } } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterYourServiceServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
Pour envoyer des structures personnalisées via le contexte dans gRPC, vous devez sérialiser les données avant de les attacher au contexte, puis les désérialiser du côté récepteur. Cela implique de convertir vos structures personnalisées dans un format pouvant être transmis sur le réseau, tel que JSON ou Protocol Buffers, puis d'ajouter ces données sérialisées aux métadonnées de contexte.
type CustomStruct struct { Field1 string Field2 int }
Étape 2 : sérialiser la structure
import ( "context" "encoding/json" "fmt" "google.golang.org/grpc/metadata" ) func serializeCustomStruct(customStruct CustomStruct) (string, error) { data, err := json.Marshal(customStruct) if err != nil { return "", err } return string(data), nil }
Étape 3 : attacher au contexte
func attachCustomStructToContext(ctx context.Context, customStruct CustomStruct) (context.Context, error) { serializedData, err := serializeCustomStruct(customStruct) if err != nil { return nil, err } md := metadata.Pairs("custom-struct", serializedData) ctx = metadata.NewOutgoingContext(ctx, md) return ctx, nil }
Étape 4 : Transmettre
func main() { conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := pb.NewYourServiceClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() customStruct := CustomStruct{Field1: "value1", Field2: 42} ctx, err = attachCustomStructToContext(ctx, customStruct) if err != nil { log.Fatalf("could not attach custom struct to context: %v", err) } resp, err := client.YourMethod(ctx, &pb.YourRequest{}) if err != nil { log.Fatalf("could not call method: %v", err) } log.Printf("Response: %v", resp) }
Étape 5 : Extraire et désérialiser sur le serveur
func deserializeCustomStruct(data string) (CustomStruct, error) { var customStruct CustomStruct err := json.Unmarshal([]byte(data), &customStruct) if err != nil { return CustomStruct{}, err } return customStruct, nil } func extractCustomStructFromContext(ctx context.Context) (CustomStruct, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return CustomStruct{}, fmt.Errorf("no metadata found in context") } serializedData := md["custom-struct"] if len(serializedData) == 0 { return CustomStruct{}, fmt.Errorf("no custom struct found in metadata") } return deserializeCustomStruct(serializedData[0]) } func (s *server) YourMethod(ctx context.Context, req *pb.YourRequest) (*pb.YourResponse, error) { customStruct, err := extractCustomStructFromContext(ctx) if err != nil { return nil, err } log.Printf("Received custom struct: %+v", customStruct) select { case <-time.After(10 * time.Second): return &pb.YourResponse{}, nil case <-ctx.Done(): return nil, ctx.Err() } }
Pour gérer la propagation du contexte, y compris les structures personnalisées, de manière cohérente dans tous les appels gRPC, vous pouvez utiliser des intercepteurs. Les intercepteurs sont des middlewares qui traitent les requêtes et les réponses, ajoutant des fonctionnalités telles que la journalisation, la surveillance et la gestion des métadonnées contextuelles.
Vous avez besoin d'intercepteurs unaires et de streaming pour gérer différents types d'appels RPC :
Intercepteur unaire côté client :
func unaryClientInterceptor( ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { customStruct, ok := ctx.Value("customStruct").(CustomStruct) if ok { ctx, err := attachCustomStructToContext(ctx, customStruct) if err != nil { return err } } return invoker(ctx, method, req, reply, cc, opts...) }
Intercepteur unaire côté serveur :
func unaryServerInterceptor( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (interface{}, error) { customStruct, err := extractCustomStructFromContext(ctx) if err != nil { return nil, err } ctx = context.WithValue(ctx, "customStruct", customStruct) return handler(ctx, req) }
Intercepteur de streaming côté client :
func streamClientInterceptor( ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption, ) (grpc.ClientStream, error) { customStruct, ok := ctx.Value("customStruct").(CustomStruct) if ok { ctx, err := attachCustomStructToContext(ctx, customStruct) if err != nil { return nil, err } } return
Intercepteur de streaming côté serveur :
import ( "context" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // StreamServerInterceptor handles server-side streaming func streamServerInterceptor( srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, ) error { ctx := ss.Context() customStruct, err := extractCustomStructFromContext(ctx) if err != nil { return err } // Add custom struct to context for server handling newCtx := context.WithValue(ctx, "customStruct", customStruct) wrapped := grpc_middleware.WrapServerStream(ss) wrapped.WrappedContext = newCtx // Handle the request return handler(srv, wrapped) } // Example of using the interceptor in a gRPC server setup func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } // Register the interceptors server := grpc.NewServer( grpc.UnaryInterceptor(unaryServerInterceptor), grpc.StreamInterceptor(streamServerInterceptor), ) // Register your gRPC service implementations here pb.RegisterYourServiceServer(server, &yourServiceServer{}) if err := server.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
En créant et en enregistrant des intercepteurs unaires et de streaming, vous pouvez garantir que la propagation du contexte, y compris les structures personnalisées, est gérée de manière cohérente dans tous les appels gRPC. Cette approche garantit que vos métadonnées personnalisées sont correctement gérées et propagées, vous permettant de créer des services gRPC robustes et flexibles.