banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

protobuf和gRPC进阶

title: protobuf 和 gRPC 进阶
date: 2021-09-14 15:05:00
toc: true
category:

  • Golang
  • gRPC
    tags:
  • Golang
  • gRPC
  • Go
  • protobuf
  • proto
  • 类型
  • 默认值
  • 命令
  • package
  • 同步
  • Map
  • 枚举
  • 泛型
  • message
  • 嵌套
  • 引用
  • 对象
  • 属性

protobuf 的基本类型和默认值#

参考下表:

标量消息字段可以具有以下类型之一–该表显示了文件中指定的类型,以及自动生成的类中相应的类型:.proto

.proto TypeNotesC++ TypeJava/Kotlin TypePython TypeGo TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32使用可变长度编码。编码负数效率低下–如果您的字段可能有负值,请改用 sint32。int32intintint32Fixnum or Bignum (as required)intintegerint
int64使用可变长度编码。编码负数效率低下–如果您的字段可能有负值,请改用 sint64。int64longint/longint64Bignumlonginteger/stringInt64
uint32使用可变长度编码。uint32intint/longuint32Fixnum or Bignum (as required)uintintegerint
uint64使用可变长度编码。uint64longint/longuint64Bignumulonginteger/stringInt64
sint32使用可变长度编码。有符号整数值。它们比常规的 int32s 更有效地编码负数。int32intintint32Fixnum or Bignum (as required)intintegerint
sint64使用可变长度编码。有符号整数值。它们比常规的 int64s 更有效地编码负数。int64longint/longint64Bignumlonginteger/stringInt64
fixed32总是四个字节。如果值通常大于 2^28^,则比 uint32 更有效。uint32intint/longuint32Fixnum or Bignum (as required)uintintegerint
fixed64总是八个字节。如果值通常大于 2^56^,则比 uint64 更有效。uint64longint/longuint64Bignumulonginteger/stringInt64
sfixed32总是四个字节。int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64总是八个字节。int64longint/longint64Bignumlonginteger/stringInt64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,且长度不能超过 2^32^。stringStringstr/unicodestringString (UTF-8)stringstringString
bytes可能包含任意顺序的字节数据,但不超过 2^32^。stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstringList

你可以在文章Protocol Buffer 编码中,找到更多 “序列化消息时各种类型如何编码” 的信息。

option go_package 的作用#

我们在定义一个.proto 文件时,需要申明这个文件属于哪个包,主要是为了规范整合以及避免重复,这个概念在其他语言中也存在,比如 php 中的 namespace的概念,go 中的 package概念。

所以,我们根据实际的分类情况,给每 1 个 proto 文件都定义 1 个包,一般这个包和 proto 所在的文件夹名子,保持一致。

比如例子中,文件在 proto文件夹中,那我们用的 package 就为: proto;

option 单独看这个名字,就知道是选项和配置的意思,常见的选项是配置 go_package

option go_package = ".;proto";

现在 protoc 命令生成 go 包的时候,如果这一行没加上,会提示错误:

➜  proto git:(master) ✗ protoc --go_out=:. hello.proto
2020/05/21 15:59:40 WARNING: Missing 'go_package' option in "hello.proto", please specify:
        option go_package = ".;proto";
A future release of protoc-gen-go will require this be specified.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.

所以,这个 go_package和上面那个 package proto;有啥区别呢?有点蒙啊。

尝试这样改一下:

syntax = "proto3";

package protoB;

option go_package = ".;protoA";

看下生成的 go 语言包的 package 到底是啥?打开,生成后的 go 文件:

# vi hello.pb.go

package protoA

...

发现是 protoA,说明,go 的 package 是受 option go_package影响的。所以,在我们没有申请这一句的时候,系统就会用 proto 文件的 package 名字,为提示,让你也加上相同的 go_package 名字了。

再来看一下,这个 =".;proto" 是啥意思。改一下:

option go_package = "./protoA";

执行后,发现,生成了一个 protoA文件夹。里面的 hello.pb.go 文件的 package 也是 protoA

所以,.;表示的是就在本目录下的意思么???行吧。

再来看一下,我们改成 1 个绝对的路径目录:

option go_package = "/";

所以,总结一下:

package protoB; // 这个用来设定proto文件自身的package

option go_package = ".;protoA";  // 这个用来生成的go文件package。一般情况下,可以把这2个设置成一样

proto 文件同步时的坑#

简而言之就是客户端与服务端所使用的 proto 文件中对象属性的序号不同,导致业务逻辑混乱,且无法排查。

例如:

message StreamReqData{
    string name = 1;
    string url = 2;
}

protobuf 在序列化时的逻辑大概为:

name = "biuaxia", -> 17biuaxia,其中的 1 为属性 name 的序号,7 为内容长度。

同理,url = "biuaxia.cn", -> 210biuaxia.cn

如果服务端与客户端的 proto 文件中属性的序号不同则会造成难以排查的问题。

对此最好的解决办法就是不要修改 proto 文件,而是接收针对 proto 文件的统一分发。

proto 文件嵌套引用#

例如 hello.proto 引用 base.proto。

hello.proto 文件如下:

syntax = "proto3";

option go_package = "../proto";

service Greeter {
    rpc Ping(Empty) returns (Pong);
}

base.proto 文件如下:

syntax = "proto3";

message Empty {
}

message Pong {
    string id = 1;
}

直接生成或 IDE 中可以看到 hello.proto 的第 6 行 (即 Empty 和 Pong) 会报错,可以通过 import 关键字来实现。

修改 hello.proto 在其第三行追加内容:

syntax = "proto3";

import "base.proto";

option go_package = "../proto";

service Greeter {
    rpc Ping(Empty) returns (Pong);
}

注意:import 文件时,后面的内容为当前文件的相对目录。

也可以用于引入带包结构的 proto 文件且使用,例如:

syntax = "proto3";

import "base.proto";
import "google/protobuf/empty.proto";

option go_package = "../proto";

service Greeter {
    rpc Ping(google.protobuf.Empty) returns (Pong);
}

注意:外部文件需要写完整的包名,本地文件不需要。

想要查询 proto 自带有哪些数据可以通过 Idea 查看下图的位置,将红色的位置追加上 .proto 文件即可。

image.png

message 嵌套#

可以直接嵌套,如:

syntax = "proto3";

option go_package = "../proto";

message Hello {
    message Result {
        string code = 1;
        string msg = 2;
    }
  
    string ret = 1;
    Result msg = 2;
    Result data = 3;
}

注意:import 嵌套会导致被 import 的 proto 文件包含的 message 不被生成。解决办法就是再手动生成被 proto 的文件。

使用枚举类型#

直接定义,如:

syntax = "proto3";

option go_package = "../proto";

enum Gender{
    MALE = 1;
    FEMALE = 2;
}

message Request {
    Gender g = 1;
}

即可。

使用 Map#

使用示例:

syntax = "proto3";

option go_package = "../proto";

message Request {
    map<string, string> mp = 1;
}

注意:一定要和泛型一样指定 key 和 value 的类型

使用时间戳 timestamp#

首先在 proto 文件中引入 timestamp

syntax = "proto3";

import "google/protobuf/timestamp.proto";

option go_package = "../proto";

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

message HelloRequest {
  string name = 1;
  google.protobuf.Timestamp addTime = 2;
}

message HelloReply {
  string message = 1;
}

然后客户端直接调用 "google.golang.org/protobuf/types/known/timestamppb" 此目录下的 New(time.Time) 即可生成对应的类型,如果不知道 New(time.Time) 是怎么查询到的,建议使用 Goland,它会自动有提示。

package main

import (
	"biuaxia.cn/demo/grpc_test/proto"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"
	"time"
)

func main() {
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	reply, err := c.SayHello(context.Background(), &proto.HelloRequest{
		Name:    "biuaxia",
		AddTime: timestamppb.New(time.Now()),
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(reply.Message)
}

服务端无需修改:

package main

import (
	"biuaxia.cn/demo/grpc_test/proto"
	"context"
	"google.golang.org/grpc"
	"net"
)

type Server struct {
}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
	time := request.AddTime.AsTime().Format("2006-01-02 15:04:05.000")
	return &proto.HelloReply{
		Message: "hello " + request.Name + "_" + time,
	}, nil
}

func main() {
	s := grpc.NewServer()
	proto.RegisterGreeterServer(s, &Server{})

	listen, err := net.Listen("tcp", "localhost:1234")
	if err != nil {
		panic(err)
	}

	_ = s.Serve(listen)
}

metadata 机制#

可以理解为 http 的请求头,用于携带 token 等请求信息,与业务信息隔开理解即可。

注意:引入的包为 "google.golang.org/grpc/metadata"

使用示例 - 客户端:

package main

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/types/known/timestamppb"

	"biuaxia.cn/demo/grpc_test/proto"
)

func main() {
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)

	// md := metadata.Pairs("timestamp", time.Now().Format("2006-01-02 15:04:05.000"))
	md := metadata.New(map[string]string{
		"name": "biuaxia",
	})
	ctx := metadata.NewOutgoingContext(context.Background(), md)

	reply, err := c.SayHello(ctx, &proto.HelloRequest{
		Name:    "biuaxia",
		AddTime: timestamppb.New(time.Now()),
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(reply.Message)
}

使用示例 - 服务端:

package main

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"

	"biuaxia.cn/demo/grpc_test/proto"
)

type Server struct {
}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		fmt.Println("get metadata failed")
	}
	fmt.Println("get metadata:", md)
	time := request.AddTime.AsTime().Format("2006-01-02 15:04:05.000")
	return &proto.HelloReply{
		Message: "hello " + request.Name + "_" + time,
	}, nil
}

func main() {
	s := grpc.NewServer()
	proto.RegisterGreeterServer(s, &Server{})

	listen, err := net.Listen("tcp", "localhost:1234")
	if err != nil {
		panic(err)
	}

	_ = s.Serve(listen)
}

运行截图:

服务端.png

客户端.png

拦截器#

使用示例 - 服务端:

package main

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"

	"awesomeProject/grpc/interceptor/main/proto"
)

type Server struct{}

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

func interceptor(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
}

func main() {
	var opts []grpc.ServerOption
	opts = append(opts, grpc.UnaryInterceptor(interceptor))

	g := grpc.NewServer(opts...)
	proto.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", "0.0.0.0:50051")
	if err != nil {
		panic("failed to listen:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("failed to start grpc:" + err.Error())
	}
}

使用示例 - 客户端:

package main

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"

	"awesomeProject/grpc/interceptor/main/proto"
)

func interceptor(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("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
	return err
}

func main(){
	//stream
	var opts []grpc.DialOption

	opts = append(opts, grpc.WithInsecure())
	// 指定客户端interceptor
	opts = append(opts, grpc.WithUnaryInterceptor(interceptor))

	conn, err := grpc.Dial("localhost:50051", opts...)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name:"bobby"})
	if err != nil {
		panic(err)
	}
	fmt.Println(r.Message)
}

验证请求信息#

使用示例 - 服务端:

package main

import (
	proto2 "awesomeProject/grpc/interceptor/samples/auth_verify/proto"
	"context"
	"fmt"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"net"

	"google.golang.org/grpc"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto2.HelloRequest) (*proto2.HelloReply,
	error) {
	return &proto2.HelloReply{
		Message: "hello " + request.Name,
	}, nil
}

func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		fmt.Println("get metadata failed")
		return resp, status.Error(codes.Unauthenticated, "无授权认证信息")
	}

	var (
		appkey    string
		appsecret string
	)

	if val, ok := md["appkey"]; ok {
		appkey = val[0]
	}
	if val, ok := md["appsecret"]; ok {
		appsecret = val[0]
	}

	if appkey != "vditor" || appsecret != "b1d0d1ad98acdd5d7d846d" {
		return resp, status.Error(codes.Unauthenticated, "授权认证信息有误")
	}

	fmt.Println("get metadata:", md)

	// 继续处理请求
	fmt.Println("接收到新请求")
	res, err := handler(ctx, req)
	fmt.Println("请求处理完成")
	return res, err
}

func main() {
	var opts []grpc.ServerOption
	opts = append(opts, grpc.UnaryInterceptor(interceptor))

	g := grpc.NewServer(opts...)
	proto2.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", "0.0.0.0:50051")
	if err != nil {
		panic("failed to listen:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("failed to start grpc:" + err.Error())
	}
}

使用示例 - 客户端:

package main

import (
	proto2 "awesomeProject/grpc/interceptor/samples/auth_verify/proto"
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

func interceptor(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{
		"appkey":    "vditor",
		"appsecret": "b1d0d1ad98acdd5d7d846d",
	})
	ctx = metadata.NewOutgoingContext(context.Background(), md)

	err := invoker(ctx, method, req, reply, cc, opts...)
	fmt.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
	return err
}

type customCredential struct {
}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"appkey":    "vditor",
		"appsecret": "b1d0d1ad98acdd5d7d846d",
	}, nil
}

func (c customCredential) RequireTransportSecurity() bool {
	return false
}

func main() {
	var opts []grpc.DialOption

	opts = append(opts, grpc.WithInsecure())
	// 指定客户端interceptor
	//opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
	opts = append(opts, grpc.WithPerRPCCredentials(customCredential{}))

	conn, err := grpc.Dial("localhost:50051", opts...)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := proto2.NewGreeterClient(conn)
	r, err := c.SayHello(context.Background(), &proto2.HelloRequest{Name: "bobby"})
	if err != nil {
		panic(err)
	}
	fmt.Println(r.Message)
}

grpc/interceptor/samples/auth_verify/client/client.go:46 被注释的内容是原生的调用方式,47 行是 grpc 自己封装的方式;需要对象实现 google.golang.org/grpc/credentials/credentials.goPerRPCCredentials 接口的 GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)RequireTransportSecurity() bool 方法。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。