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 タイプ |
---|---|---|---|---|---|---|---|---|---|
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 | 可変長エンコーディングを使用します。有符号整数値です。通常の int32 よりも負数を効率的にエンコードします。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | 可変長エンコーディングを使用します。有符号整数値です。通常の int64 よりも負数を効率的にエンコードします。 | int64 | long | int/long | int64 | Bignum | long | integer/string | Int64 |
fixed32 | 常に 4 バイトです。値が通常 2^28^ を超える場合、uint32 よりも効率的です。 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 常に 8 バイトです。値が通常 2^56^ を超える場合、uint64 よりも効率的です。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string | Int64 |
sfixed32 | 常に 4 バイトです。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 常に 8 バイトです。 | 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
の概念があります。
したがって、実際の分類に基づいて、各 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
ファイルのpackage
もprotoA
です。
したがって、.;
は現在のディレクトリにあることを示しているのでしょうか???わかりました。
次に、絶対パスディレクトリに変更してみましょう:
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
ファイルを追加してください。
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)
}
実行スクリーンショット:
インターセプター#
使用例 - サーバー:
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.go
のPerRPCCredentials
インターフェースのGetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
とRequireTransportSecurity() bool
メソッドを実装する必要があります。