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 タイプメモC++ タイプJava/Kotlin タイプPython タイプGo タイプRuby タイプC# タイプPHP タイプDart タイプ
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可変長エンコーディングを使用します。有符号整数値です。通常の int32 よりも負数を効率的にエンコードします。int32intintint32Fixnum or Bignum (as required)intintegerint
sint64可変長エンコーディングを使用します。有符号整数値です。通常の int64 よりも負数を効率的にエンコードします。int64longint/longint64Bignumlonginteger/stringInt64
fixed32常に 4 バイトです。値が通常 2^28^ を超える場合、uint32 よりも効率的です。uint32intint/longuint32Fixnum or Bignum (as required)uintintegerint
fixed64常に 8 バイトです。値が通常 2^56^ を超える場合、uint64 よりも効率的です。uint64longint/longuint64Bignumulonginteger/stringInt64
sfixed32常に 4 バイトです。int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64常に 8 バイトです。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の概念があります。

したがって、実際の分類に基づいて、各 proto ファイルに 1 つのパッケージを定義します。通常、このパッケージは proto が存在するフォルダ名と一致します。

例えば、例の中でファイルがprotoフォルダにある場合、使用するパッケージは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 言語パッケージのパッケージが何であるかを見てみましょう。生成された Go ファイルを開くと:

# vi hello.pb.go

package protoA

...

protoAであることがわかります。つまり、Go のパッケージはoption go_packageの影響を受けます。したがって、これを申請しない場合、システムは proto ファイルのパッケージ名を使用し、同じ go_package 名を追加するように促します。

次に、=".;proto"は何を意味するのでしょうか。変更してみましょう:

option go_package = "./protoA";

実行後、protoAフォルダが生成され、その中のhello.pb.goファイルのpackageprotoAです。

したがって、.;は現在のディレクトリにあることを示しているのでしょうか???わかりました。

次に、絶対パスディレクトリに変更してみましょう:

option go_package = "/";

したがって、まとめると:

package protoB; // これはprotoファイル自身のパッケージを設定するためのものです

option go_package = ".;protoA";  // これは生成されたGoファイルのパッケージです。一般的には、これら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 の 3 行目に内容を追加します:

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 のネストは、インポートされた 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 のタイプを指定してください。

タイムスタンプの使用#

まず、proto ファイルにタイムスタンプをインポートします。

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)
}

メタデータメカニズム#

HTTP のリクエストヘッダーとして理解でき、トークンなどのリクエスト情報を運ぶために使用され、ビジネス情報とは分けて理解できます。

注意:インポートするパッケージは"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("メタデータの取得に失敗しました")
	}
	fmt.Println("メタデータを取得:", 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("リスニングに失敗しました:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("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(){
	//ストリーム
	var opts []grpc.DialOption

	opts = append(opts, grpc.WithInsecure())
	// クライアントインターセプターを指定
	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("メタデータの取得に失敗しました")
		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("メタデータを取得:", 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("リスニングに失敗しました:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("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())
	// クライアントインターセプターを指定
	//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メソッドを実装する必要があります。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。