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ファイルの定義したデータ構造から任意の言語でのデータアクセス用クラスを生成する
サンプルコード
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 ""
}
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
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
}
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
SayGoodBye(context.Context, *GoodByeRequest) (*GoodByeReply, error)
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
- サーバー側では、SayHello, SayGoodBye を実装したGreeterServerを実装する。
- クライアント側では、GreeterClientを通じてSayHelloRequest、SayGoodByeを必要に応じて呼び出す
次に書きたいこと
procol buffers で型に値が入っていない場合とデフォルト値を区別する
- protocol buffers では型のフィールドに値が入っていない場合、デフォルト値が使われてしまう
- Stringだと""、int32だと0といった具合にデフォルト値が入る。
- 値が無い場合とデフォルト値をクライアントが入れてきている場合を区別したい場合があるはずだ。
- 例えば Person型に
int32 age
を定義した時、age が不明だからageフィールドを空にして送るときがあるはず。一方で age = 0 を送ることも想定されるが、unknownと0は明確に区別したい、など。
- 上記のような時に、デフォルト型をラップしてnilを使えるようにする。
- 何らかの理由でクライアント-サーバー間で直接gRPCを使えない時があるかもしれない。
- だけどprotocol buffersはとても分かりやすいインターフェースなので使いたい。
- grpc-gatewayを使うと、サーバーの前段にjson API を gRPC に変換してくれるリバースプロキシをprotoco buffersの定義から生成してくれるらしい。
参考