Golang微服务(一)

Posted on 2023-02-20 12:57  呱呱呱呱叽里呱啦  阅读(57)  评论(0编辑  收藏  举报

Golang微服务(一)

一、protobuf常规使用及踩坑记录

1.类型映射关系及零值

关于protobuf的文档见:Protocol Buffers Documentation (protobuf.dev),文档中可见protobuf与go语言的类型映射关系,以及各种类型的零值。当message中的没有提供指定字段,接收端并不会抛出异常,而是会使用该字段的零值。见以下测试:

syntax = "proto3";

option go_package = ".;proto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1; // 此处定义请求字段name
}

message HelloResponse {
  string greeting = 1;
}
// server端

package main

import (
	"context"
	"google.golang.org/grpc"
	"learn/grpc_things/proto"
	"net"
)

type Server struct {
	proto.UnimplementedGreeterServer
}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	return &proto.HelloResponse{
		Greeting: "Hello, " + request.Name,
	}, nil
}

func main() {
	s := grpc.NewServer()
	proto.RegisterGreeterServer(s, &Server{})
	lis, err := net.Listen("tcp", ":6666")
	if err != nil {
		panic(err)
	}
	err = s.Serve(lis)
	if err != nil {
		panic(err)
	}
}
// 客户端
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"learn/grpc_things/proto"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure())
	defer conn.Close()
	if err != nil {
		panic(err)
	}
	c := proto.NewGreeterClient(conn)
	name := proto.HelloRequest{} // 不提供proto文件中定义的Name字段
	resp, errResp := c.SayHello(context.Background(), &name)
	if errResp != nil {
		panic(errResp)
	}
	fmt.Println(resp.Greeting) // 打印服务端的响应:Hello,
}

2.go_package设置

option go_package设置,格式是"生成结果文件存放路径;go项目中的package"。

“生成结果文件存放路径”可以使用".","..","../"之类的标识,当指定路径不存在时则会创建。go_package或者proto中的package用于分别指定当前文件在其同语言中的(类似)作用域,目的应该是确保标识符的唯一性。

3.protobuf的字段编号

在开发中需要保持服务端和客户端proto文件的完全一致,否则可能出现不容易发现的BUG。例如在proto文件中“string name = 1;”中的1指此字段在某个结构中的编号,protobuf通过自动生成其他语言对应的代码完成了一种在具体结构中字段--编号的对应,这样编码时可以通过“编号+值长度+值”代替“键+值”,可以减少用于实际传输的数据量,在另一端解析时则可以通过编号和“字段--编号”的映射顺序取出值并赋给对应的字段。见以下示例说明:

syntax = "proto3";

option go_package = ".;proto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
  string url = 2;
  // 如果另一端的proto文件设置为
  // string url = 1;
  // string name = 2;
  // 此时代码可能可以继续运行并干扰调试
}

message HelloResponse {
  string greeting = 1;
}

4.proto文件的import

可以自定义proto文件以供引用或者引用公共proto(google/protobuf/something),在go中引用公共支持的proto结构时引用proto对应的公共包路径即可,例如(import "github.com/golang/protobuf/ptypes/empty")

// base.proto
// 注意,如果想要在go代码中引用base.proto中的结构,该proto文件也需要通过protoc生成go文件
syntax = "proto3";

option go_package = ".;proto";

message Empty {
}

message Pong {
  string res = 1;
}
// hello.proto
syntax = "proto3";

import "base.proto"
// 公共支持:import "google/protobuf/empty.proto";

option go_package = ".;proto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloResponse);
  rpc Ping(Empty) returns (Pong);
  // 调用公共支持:rpc Ping(google.protobuf.Empty) returns (Pong);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string greeting = 1;
}

5.protobuf的message嵌套

当一个message复杂到需要由其他message构成,而不需要将所有message提取至公共作用域时,可以直接在内部定义。

message HelloResponse {
  string greeting = 1;
  message Detail {
    string info = 1;
    string id = 2;
  }
  repeated Detail data = 2;
}

6.protobuf的枚举、map、timestamp

syntax = "proto3";
option go_package = ".;proto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

// 枚举
enum Gender {
  FEMALE = 0;
  MALE = 1;
}

message HelloRequest {
  string name = 1;
  Gender g = 2;
  google.protobuf.Timestamp ts = 3; 
  // timestamp go代码中可以用timestamppb.New()生成时间戳数据
}

message HelloResponse {
  string greeting = 1;
  map<string, int32> rank = 2; // map
}

二、gRPC常规使用及踩坑记录

1.metadata

类似http中的header,为当次调用提供信息。

type MD map[string][]string

使用

// 实例化metadata
md := metadata.New(map[string]string{"key": "value", })
md1 := metadata.Pairs("key1", "value1", "key2", "value2", )
// 发送metadata
ctx := metadata.NewOutgoingContext(context.Background(), md)
response, err := client.RPCThings(ctx, requestThings)
// 接收metadata
md, ok := metadata.FromIncomingContext(ctx)

2.gRPC拦截器

一元拦截器

server端

	myInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		fmt.Println("新请求")
        res, err := handler(ctx, req)
        fmt.Println("新响应")
        return res, err
	} // 实现拦截器逻辑函数

	itc := grpc.UnaryInterceptor(myInterceptor) // 实例化拦截器
	s := grpc.NewServer(itc) // 初始化server时传入拦截器

client端

	ci := 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)
		fmt.Println("处理用时:", time.Since(start))
		return err
	} // 实现拦截器逻辑函数

	conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithUnaryInterceptor(ci)) // 实例化拦截器并传入拨号函数

3.gRPC Auth认证

  • 手动实现

// 利用metadata和拦截器实现认证
// client端,在拦截器中修改ctx,为每次请求增加验证信息
	ci := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		start := time.Now()
		md := metadata.New(map[string]string{"token": "right"})
		ctx = metadata.NewOutgoingContext(context.Background(), md)
		err := invoker(ctx, method, req, reply, cc)
		fmt.Println("处理用时:", time.Since(start))
		return err
	}

	conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithUnaryInterceptor(ci))

// server端,在拦截器中获取metadata并校验
	myInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		fmt.Println("新请求")
		md, ok := metadata.FromIncomingContext(ctx)
		if !ok {
			return resp, status.Errorf(codes.Unauthenticated, "认证失败")
		}
		if authInfo, got := md["token"]; got && strings.Join(authInfo, "") == "right" {
			res, err := handler(ctx, req)
			return res, err
		} else {
			return resp, status.Errorf(codes.Unauthenticated, "认证失败")
		}

	}

	itc := grpc.UnaryInterceptor(myInterceptor)
	s := grpc.NewServer(itc)
  • 使用PerRPCCredentials接口实现

// client端
// 实现PerRPCCredentials接口
type myCdt struct {
}

func (mc myCdt) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{"token": "right"}, nil
}

func (mc myCdt) RequireTransportSecurity() bool {
	return false
}

// 拨号
	conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithPerRPCCredentials(myCdt{}))
	

4.gRPC err处理

状态码:grpc/statuscodes.md at master · grpc/grpc · GitHub

// server端返回status err
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	fmt.Println(request.G)
	return nil, status.Errorf(codes.InvalidArgument, "参数错误:%s", request.Name)
}
// status.Errorf同样基于status.New()方法,只不过在New()的结果上调用了.Err()方法

// client端解析status err
resp, errResp := c.SayHello(ctx, &name)
	if errResp != nil {
		statusErr, ok := status.FromError(errResp)
		if !ok {
			fmt.Println("解析status err失败")
		} else {
			fmt.Println(statusErr.Message(), statusErr.Code())
		}
		return
	}

5.gRPC超时

重在设置超时的原因:防止服务链中的节点单次任务占用资源过多以及负面作用叠加

常规简单处理:

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)