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.
Comprensión de los plazos y los tiempos de espera de las solicitudes
Plazos
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.
Solicitar tiempos de espera
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.
Establecer fechas límite y tiempos de espera de solicitud en gRPC
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:
Plazos de configuración del lado del cliente
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) }
Manejo del lado del servidor
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) } }
Propagar estructuras personalizadas en contexto
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.
Proceso paso a paso
- Defina su estructura personalizada : defina la estructura personalizada que desea enviar.
- Serializar la estructura : convierta la estructura personalizada en una cadena o matriz de bytes.
- Adjuntar al contexto : agregue los datos serializados a los metadatos del contexto.
- Transmitir : envía la llamada gRPC con el contexto.
- Extraer y deserializar en el servidor : extraiga los metadatos del contexto en el lado del servidor y deserialícelos nuevamente en la estructura personalizada.
Paso 1: Defina su estructura personalizada
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() } }
Implementación de middleware para todas las llamadas gRPC
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.
Interceptores unarios y de streaming
Necesita interceptores unarios y de transmisión para administrar diferentes tipos de llamadas RPC:
- Interceptores unarios : manejan ciclos únicos de solicitud-respuesta.
- Interceptores de transmisión : manejan flujos de solicitudes y respuestas, incluida la transmisión del lado del cliente, la transmisión del lado del servidor y la transmisión bidireccional.
Implementación del interceptor unario
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) }
Implementación del interceptor de transmisión
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.