protocol buffers, gRPC周りの整理

protocol buffers, gRPC周りの整理

この記事に書くこと

書くこと

  • gRPCの簡単な説明
  • protocol buffersの簡単な説明
  • 上記のサンプルコード(公式ドキュメントとほとんど同じ)

最初にざっくりと

  • protocol buffersは読み書きしやすいIDL(XMLみたいなもの)
  • gRPCはクライアント-サーバ間でのメソッド呼び出しを分かりやすく高速に行う機構

gRPC概要

  • クライアントが別環境のサーバーのメソッドをローカルのメソッド呼び出しのように行える仕組み
    • サーバーサイドはインターフェースを実装し、クライアントからの呼び出しをハンドルするためサーバを起動する
    • クライアントサイドは、サーバーサイドと同じメソッドを提供するスタブを持たせる
  • protocol buffers で、型、メソッド、メソッドがどの型を受け取り、どの型を返すかを定義する
    • 型定義はjsonでも問題ないが通常はprotocol buffersを使うことを想定している
  • http/2.0 をベースとしている

protocol buffers 概要

  • 構造化されたデータを自動的に読み書きするための機構
  • XMLみたいなものだが、XMLより可読性高く各言語の構造体へのエンコード、デコードも早い。
  • protoc
    • .protoファイル用コンパイラ
    • protoファイルの定義したデータ構造から任意の言語でのデータアクセス用クラスを生成する

サンプルコード

  • 下記がprotocol buffersの例。
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (GoodByeRequest) returns (GoodByeReply) {}
}

message HelloRequest {
  string name = 1;
  int32 count = 2;
}

message HelloReply {
  string message = 1;
  int32 count = 2;
}

message GoodByeRequest {
  string name = 1;
}

message GoodByeReply {
  string message = 1;
}
  • 詳細はドキュメントを読んでもらうとして、簡単な概要。
    • service Greeterは、rpc SayHelloメソッドと rpc SayGoodByeメソッドを持っている
    • SayHelloメソッドは message HelloRequest型を受け取り、 message HelloReply型を返す
    • Helloメソッドは message HelloRequest型を受け取り、message HelloReply型を返す
    • HelloRequest型は、name(string), count(int32)を持つ型
    • HelloReply型は、message(string), count(int32)を持つ型
    • GoodByeRequest、GoodByeReplyに関しても同様。

この.protoファイルを眺めると、サービスがどんなメソッドを持っていて、何を受け取って何を返すかがすぐに分かる!!

  • protocでgolangのコードを生成すると、下記のようなコードが生成される(一部抜粋)
// -----------------------------------
// 構造体関連
// -----------------------------------
type GoodByeRequest struct {
    Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

func (m *GoodByeRequest) Reset()         { *m = GoodByeRequest{} }
func (m *GoodByeRequest) String() string { return proto.CompactTextString(m) }
func (*GoodByeRequest) ProtoMessage()    {}
func (*GoodByeRequest) Descriptor() ([]byte, []int) {
    return fileDescriptor_17b8c58d586b62f2, []int{2}
}

func (m *GoodByeRequest) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_GoodByeRequest.Unmarshal(m, b)
}
func (m *GoodByeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_GoodByeRequest.Marshal(b, m, deterministic)
}
func (m *GoodByeRequest) XXX_Merge(src proto.Message) {
    xxx_messageInfo_GoodByeRequest.Merge(m, src)
}
func (m *GoodByeRequest) XXX_Size() int {
    return xxx_messageInfo_GoodByeRequest.Size(m)
}
func (m *GoodByeRequest) XXX_DiscardUnknown() {
    xxx_messageInfo_GoodByeRequest.DiscardUnknown(m)
}

var xxx_messageInfo_GoodByeRequest proto.InternalMessageInfo

func (m *GoodByeRequest) GetName() string {
    if m != nil {
        return m.Name
    }
    return ""
}

// -----------------------------------
// クライアント関連
// -----------------------------------

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
    // Sends a greeting
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
    // Sends a greeting again
    SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error)
}

type greeterClient struct {
    cc *grpc.ClientConn
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

func (c *greeterClient) SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error) {
    out := new(GoodByeReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayGoodBye", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

// -----------------------------------
// サーバー関連
// -----------------------------------

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
    // Sends a greeting again
    SayGoodBye(context.Context, *GoodByeRequest) (*GoodByeReply, error)
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}

次に書きたいこと

procol buffers で型に値が入っていない場合とデフォルト値を区別する

  • protocol buffers では型のフィールドに値が入っていない場合、デフォルト値が使われてしまう
    • Stringだと""、int32だと0といった具合にデフォルト値が入る。
  • 値が無い場合とデフォルト値をクライアントが入れてきている場合を区別したい場合があるはずだ。
    • 例えば Person型に int32 age を定義した時、age が不明だからageフィールドを空にして送るときがあるはず。一方で age = 0 を送ることも想定されるが、unknownと0は明確に区別したい、など。
  • 上記のような時に、デフォルト型をラップしてnilを使えるようにする。

grpc-gateway

  • 何らかの理由でクライアント-サーバー間で直接gRPCを使えない時があるかもしれない。
  • だけどprotocol buffersはとても分かりやすいインターフェースなので使いたい。
  • grpc-gatewayを使うと、サーバーの前段にjson API を gRPC に変換してくれるリバースプロキシをprotoco buffersの定義から生成してくれるらしい。

参考