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 Type | Notes | C++ Type | Java/Kotlin Type | Python Type | Go Type | Ruby Type | C# Type | PHP Type | Dart Type | 
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | |
| float | float | float | float | float32 | Float | float | float | double | |
| int32 | 使用可變長度編碼。編碼負數效率低下–如果您的字段可能有負值,請改用 sint32。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | 
| int64 | 使用可變長度編碼。編碼負數效率低下–如果您的字段可能有負值,請改用 sint64。 | int64 | long | int/long | int64 | Bignum | long | integer/string | Int64 | 
| uint32 | 使用可變長度編碼。 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer | int | 
| uint64 | 使用可變長度編碼。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string | Int64 | 
| sint32 | 使用可變長度編碼。有符號整數值。它們比常規的 int32s 更有效地編碼負數。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | 
| sint64 | 使用可變長度編碼。有符號整數值。它們比常規的 int64s 更有效地編碼負數。 | int64 | long | int/long | int64 | Bignum | long | integer/string | Int64 | 
| fixed32 | 總是四個字節。如果值通常大於 2^28^,則比 uint32 更有效。 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer | int | 
| fixed64 | 總是八個字節。如果值通常大於 2^56^,則比 uint64 更有效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string | Int64 | 
| sfixed32 | 總是四個字節。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | 
| sfixed64 | 總是八個字節。 | int64 | long | int/long | int64 | Bignum | long | integer/string | Int64 | 
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
| string | 字串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本,且長度不能超過 2^32^。 | string | String | str/unicode | string | String (UTF-8) | string | string | String | 
| bytes | 可能包含任意順序的位元組數據,但不超過 2^32^。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List | 
你可以在文章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 文件即可。

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)
}
運行截圖:


攔截器#
使用示例 - 服務端:
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.go下PerRPCCredentials接口的GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)和RequireTransportSecurity() bool方法。