On the advanced use of protobuf in go language

Posted by Guffi on Sun, 24 Oct 2021 12:45:57 +0200

If you just want to play go web stand-alone development, you can refer to it gin project framework

1, Transferring data between microservices

  • 1. In microservices, data can be exchanged not only through input parameters and return parameters, but also through metadata

  • 2. Define a simple proto file

    syntax = "proto3";
    
    option go_package = ".;proto";
    
    service HelloWorld {
      rpc SayHello(HelloRequest) returns(HelloResponse);
    }
    
    message HelloRequest {
      string name = 1;
    }
    
    message HelloResponse {
      string message = 1;
    }
    
  • 3. The server side defines the data of the metadata passed by the interface client

    type server struct {
    }
    
    func (s *server) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
    	md, ok := metadata.FromIncomingContext(ctx)
    	if !ok {
    		log.Fatalf("obtain metadata Data failure")
    	}
    	if nameSlice, ok := md["name"]; ok {
    		fmt.Println("Acquired data", nameSlice[0])
    	}
    	return &proto.HelloResponse{
    		Message: "hello" + in.Name,
    	}, nil
    }
    func main() {
    	listen, err := net.Listen("tcp", ":9000")
    	if err != nil {
    		log.Fatalf("Listening port error:" + err.Error())
    	}
    	service := grpc.NewServer()
    	proto.RegisterHelloWorldServer(service, &server{})
    	err = service.Serve(listen)
    	if err != nil {
    		fmt.Println(err.Error())
    	}
    }
    
  • 4. The client defines metadata and passes parameters to the server

    func main() {
    	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	defer conn.Close()
    
    	c := proto.NewHelloWorldClient(conn)
    	// Custom passed metadata parameters
    	md := metadata.New(map[string]string{
    		"name":     "admin",
    		"password": "123456",
    	})
    	ctx := metadata.NewOutgoingContext(context.Background(), md)
    	response, err := c.SayHello(ctx, &proto.HelloRequest{
    		Name: "admin",
    	})
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	fmt.Println("Data returned by the server" + response.Message)
    }
    

2, Use of interceptor

  • 1. The so-called interceptor intercepts requests and responses in the process of network requests. First configure a simple protobuf network request, and then add an interceptor on this basis to intercept the data sent by the client to the server. It can also be used to count the interface access time

    • 1. proto file
    syntax = "proto3";
    
    option go_package = ".;proto";
    
    service HelloWorld {
      rpc SayHello(HelloRequest) returns(HelloResponse);
    }
    
    message HelloRequest {
      string name = 1;
    }
    
    message HelloResponse {
      string message = 1;
    }
    
    • 2. Generate go file using command
    protoc -I=. --go_out=plugins=grpc,paths=source_relative:. helloWorld.proto
    
    • 3. Server side code

      type server struct {
      }
      
      func (s *server) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
      	return &proto.HelloResponse{
      		Message: "hello" + in.Name,
      	}, nil
      }
      
      func main() {
      	listen, err := net.Listen("tcp", ":9000")
      	if err != nil {
      		log.Fatalf("Listening port error:" + err.Error())
      	}
      	service := grpc.NewServer()
      	proto.RegisterHelloWorldServer(service, &server{})
      	err = service.Serve(listen)
      	if err != nil {
      		fmt.Println(err.Error())
      	}
      }
      
    • 4. Client

      func main() {
      	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
      	if err != nil {
      		fmt.Println(err.Error())
      		return
      	}
      	defer conn.Close()
      
      	c := proto.NewHelloWorldClient(conn)
      	response, err := c.SayHello(context.Background(), &proto.HelloRequest{
      		Name: "admin",
      	})
      	if err != nil {
      		fmt.Println(err.Error())
      		return
      	}
      	fmt.Println("Data returned by the server" + response.Message)
      }
      
      
  • 2. An interceptor is defined on the server side to intercept requests. The of the client remains unchanged. Start the client and server side and observe the server side

    func main() {
    	listen, err := net.Listen("tcp", ":9000")
    	if err != nil {
    		log.Fatalf("Listening port error:" + err.Error())
    	}
    	// Define interceptor
    	interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    		fmt.Println("A new request was received")
    		return handler(ctx, req)
    	}
    	// Use interceptors
    	opt := grpc.UnaryInterceptor(interceptor)
    	service := grpc.NewServer(opt)
    	
    	proto.RegisterHelloWorldServer(service, &server{})
    	err = service.Serve(listen)
    	if err != nil {
    		fmt.Println(err.Error())
    	}
    }
    
  • 3. The interceptor prints the information before and after the request

    ...
    // Define interceptor
    interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
      fmt.Println("Print before request")
      res, err := handler(ctx, req)
      fmt.Println("Request to finish printing")
      return res, err
    }
    // Use interceptors
    opt := grpc.UnaryInterceptor(interceptor)
    service := grpc.NewServer(opt)
    ...
    
  • 4. Use of client interceptors

    func main() {
    	// Define interceptor
    	interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    		start := time.Now()
    		err := invoker(ctx, method, req, reply, cc, opts...)
    		fmt.Printf("time consuming:%s\n", time.Since(start))
    		return err
    	}
    	// Use interceptors
    	opt := grpc.WithUnaryInterceptor(interceptor)
    	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure(), opt)
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	defer conn.Close()
    
    	c := proto.NewHelloWorldClient(conn)
    	response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    		Name: "admin",
    	})
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	fmt.Println("Data returned by the server" + response.Message)
    }
    

3, Use interceptors and metadata for authorization between microservices

  • 1. Here, we can simply do a function similar to the function that users can access only after logging in. Imagine the client as a browser and the server as the familiar gin web development

  • 2. The simple demo is the same as the above, only adding interceptors and metadata on the basis

  • 3. Pass the user name and password to the server in the interceptor on the client side

    func main() {
    	// Define interceptor
    	interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    		start := time.Now()
    		fmt.Printf("time consuming:%s\n", time.Since(start))
    		// Passing data using metadata
    		md := metadata.New(map[string]string{
    			"username": "admin",
    			"password": "123456",
    		})
    		ctx = metadata.NewOutgoingContext(context.Background(), md)
    		err := invoker(ctx, method, req, reply, cc, opts...)
    		return err
    	}
    	// Use interceptors
    	opt := grpc.WithUnaryInterceptor(interceptor)
    	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure(), opt)
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	defer conn.Close()
    
    	c := proto.NewHelloWorldClient(conn)
    	response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    		Name: "admin",
    	})
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	fmt.Println("Data returned by the server" + response.Message)
    }
    
  • 4. The server obtains the data in the metadata in the interception and judges the user name and password

    func main() {
    	listen, err := net.Listen("tcp", ":9000")
    	if err != nil {
    		log.Fatalf("Listening port error:" + err.Error())
    	}
    	// Define interceptor
    	interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    		fmt.Println("Print before request")
    		// Parsing data validation in metadata
    		md, ok := metadata.FromIncomingContext(ctx)
    		if !ok {
    			return resp, status.Error(codes.Unauthenticated, "Invalid parameter")
    		}
    		var (
    			username string
    			password string
    		)
    		if val, ok := md["username"]; ok {
    			username = val[0]
    		}
    		if val, ok := md["password"]; ok {
    			password = val[0]
    		}
    		fmt.Println(username, password, "Received parameters")
    		if username != "admin" || password != "123456" {
    			return resp, status.Error(codes.Unauthenticated, "User name and password error")
    		}
    		res, err := handler(ctx, req)
    		fmt.Println("Request to finish printing")
    		return res, err
    	}
    	// Use interceptors
    	opt := grpc.UnaryInterceptor(interceptor)
    	service := grpc.NewServer(opt)
    
    	proto.RegisterHelloWorldServer(service, &server{})
    	err = service.Serve(listen)
    	if err != nil {
    		fmt.Println(err.Error())
    	}
    }
    

4, Parameter verification

5, Return status

  • 1. Do we have a status at the time of http request? Naturally, we also have a status code at the time of grpc to mark the success or failure of the current request

  • 2,github reference documentation

  • 3. The server returns the status code to the client

    import (
    	...
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/codes"
    	"google.golang.org/grpc/status"
    	"log"
    	"net"
    )
    
    type server struct {
    }
    
    func (s *server) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
    	//return &proto.HelloResponse{
    	//	Message: "hello" + in.Name,
    	//}, nil
    	// Return error
    	return nil, status.Error(codes.NotFound, "Record not found")
    }
    
    func main() {
    	listen, err := net.Listen("tcp", ":9000")
    	if err != nil {
    		log.Fatalf("Listening port error:" + err.Error())
    	}
    
    	service := grpc.NewServer()
    
    	proto.RegisterHelloWorldServer(service, &server{})
    	err = service.Serve(listen)
    	if err != nil {
    		fmt.Println(err.Error())
    	}
    }
    
  • 4. The client receives an error and prompts according to the status code in the error

    func main() {
    
    	conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
    	if err != nil {
    		fmt.Println(err.Error())
    		return
    	}
    	defer conn.Close()
    
    	c := proto.NewHelloWorldClient(conn)
    	response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    		Name: "admin",
    	})
    	if err != nil {
    		str, ok := status.FromError(err)
    		if !ok {
    			panic("Failed to parse error message")
    		}
    		fmt.Println(str.Message(), "Error information obtained")
    		fmt.Println(str.Code(), "Error getting code")
    		fmt.Println(err.Error())
    		return
    	}
    	fmt.Println("Data returned by the server" + response.Message)
    }
    

6, Timeout processing

  • 1. Set a maximum time limit on the client

    // Set time
    ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    response, err := c.SayHello(ctx, &proto.HelloRequest{
      Name: "admin",
    })
    
  • 2. Data returned under server hibernation

Topics: Go