gRPC, un marco de llamada a procedimiento remoto (RPC) de código abierto, permite una comunicación eficiente y escalable entre servicios. Un aspecto crucial de gRPC es la gestión de plazos, tiempos de espera de solicitudes y la propagación del contexto, incluidas las estructuras personalizadas.
Comprender estos mecanismos ayuda a garantizar que los servicios respondan con prontitud, que los recursos no se desperdicien en operaciones que excedan un período de tiempo razonable y que los metadatos personalizados se transmitan de manera efectiva.
Una fecha límite en gRPC especifica el tiempo máximo en el que se debe completar una operación. Si la operación no se completa dentro de este plazo, quedará automáticamente terminada. Los plazos son esenciales para garantizar que los recursos del sistema no queden inmovilizados indefinidamente debido a servicios lentos o que no responden.
Un tiempo de espera de solicitud es un período en el que un cliente está dispuesto a esperar una respuesta del servidor. Si el servidor no responde dentro de este período, la solicitud se cancela. Este mecanismo protege al cliente de colgarse indefinidamente esperando una respuesta.
gRPC proporciona opciones flexibles para establecer plazos y solicitar tiempos de espera tanto en el lado del cliente como en el del servidor. Así es como puedes hacerlo en 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) }
En el lado del servidor, gRPC le permite hacer cumplir los plazos y manejar escenarios en los que se excede el plazo especificado por el cliente:
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) } }
Para enviar estructuras personalizadas a través del contexto en gRPC, debe serializar los datos antes de adjuntarlos al contexto y luego deserializarlos en el extremo receptor. Esto implica convertir sus estructuras personalizadas a un formato que pueda transmitirse a través de la red, como JSON o Protocol Buffers, y luego agregar estos datos serializados a los metadatos de contexto.
type CustomStruct struct { Field1 string Field2 int }
Paso 2: serializar la estructura
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 }
Paso 3: adjuntar al contexto
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 }
Paso 4: transmitir
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) }
Paso 5: extraer y deserializar en el servidor
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() } }
Para manejar la propagación del contexto, incluidas las estructuras personalizadas, de manera consistente en todas las llamadas de gRPC, puede usar interceptores. Los interceptores son middleware que procesan solicitudes y respuestas, agregando funcionalidades como registro, monitoreo y manejo de metadatos de contexto.
Necesita interceptores unarios y de transmisión para administrar diferentes tipos de llamadas RPC:
Interceptor unario del lado del cliente:
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...) }
Interceptor unario del lado del servidor:
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) }
Interceptor de transmisión del lado del cliente:
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
Interceptor de transmisión del lado del servidor:
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) } }
Al crear y registrar interceptores unarios y de transmisión, puede garantizar que la propagación del contexto, incluidas las estructuras personalizadas, se maneje de manera consistente en todas las llamadas de gRPC. Este enfoque garantiza que sus metadatos personalizados se administren y propaguen adecuadamente, lo que le permite crear servicios gRPC sólidos y flexibles.