It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【gRPC】gRPCを触ってみた

gRPCについて興味があったため触ってみました。
記載内容については誤りがあるかもしれません。ご指摘ありましたらいただけると幸いです。

gRPCとは

Googleによって開発されたRPCフレームワークです。
そもそもRPCとは(remote procedure call)の略で、なかなか上手く説明できないのですが、 例えばあるマシンからHTTPのプロトコルで決まった形式でのやり取りを行って、別のマシンのプログラム(関数)を実行する。ようなものを指します。
RPCには様々な種類が存在しますが今回は割愛します。(JSON-RPC、XML-RPCなど)
gRPCもRPCの一種になります。

Protocol Buffers

gRPCの特徴の一つとしてデータのシリアライズに「Protocol buffers」という技術が使われています。
これはメッセージデータの構造、手続きの構造を定義するためのインタフェース記述言語(IDL: Interface Description Language)に属するもので「.proto」という拡張子のファイルにメッセージタイプを定義します。
現在「prot3 version」までバージョンが進んでいます。詳しくは公式ページを参照ください。

.protoのサンプル

message Person {
  // 基本は[型名] [名前] でデータを定義するみたいです
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  // enumで固定の数値も定義できる
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

gRPCではこの「.proto」ファイルを各言語のコンパイラ(protocol buffer compiler)でコンパイルすることで、インターフェース処理コードを自動生成します。
APIを自動生成するフレームワークSwaggerがありますがイメージは似ているかもしれません。
※ Swagger使ったことないので間違っているかもしれません。
現在は次のプログラミング言語に対応しているようです。

HTTP/2への対応

gRPCはHTTP/2を前提として定義された規格されています。※ HTTP/2については詳細は割愛します。
HTTP/2を前提としているためクライアントとサーバーの双方向通信において柔軟なやりとりが可能というのもメリットの一つのようです。
環境によっては従来のHTTP/1よりも高パフォーマンスを出すことができるかもしれません。

gRPCを使う時の流れ

では実際にどうやって使えばいいのかを公式のQuick Startに沿ってやってみます。
公式ではいくつかサンプルソースが公開されていますが、今回は「helloworld」サンプルを使用します。
対応言語については、今回は言語はGoを選択しました。

※ 細かな手順は公式の方がわかりやすいと思うので、詳細は上記リンクを参照ください
※ Go言語については理解が足りない部分があるため、間違っていたらすみません

前提

  • Goのバージョンは1.6以上が必要のようです

ツールのインストール

  • gRPCの実装には次のツールが必要なのでインストールします

Helloworld

  • gRPCをインストールするとサンプルソースが幾つか梱包されていて、helloworldも以下に格納されています。
$GOPATH/src/google.golang.org/grpc/examples/helloworld

1. 「.protoファイル」にインターフェースを定義する

  • まずは、protocol buffersを使ったインターフェースを定義します。
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

service Greeter {
  // rpcというキーワードでgRPCの定義ができる
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  // データ形式と名前でフィールドを定義。後ろの数字はフィールドを固有に識別するためのタグ。ユニークの必要がある。
  // フィールドの識別タグは、1~15は1バイト、16~2047は2バイトでエンコードされるため頻繁に使うフィールドは1~15にすると良いよう
  string name = 1;
}

message HelloReply {
  string message = 1;
}
  • helloworldサンプルのルートディレクトリ配下のhelloworldディレクトリ内に「helloworld.prot」というファイルがあります。
  • サンプルでは次の定義がされています。
    • Greeter: serviceというキーワードで宣言されていて 1つのRPC手続きが定義されています。(メソッド定義のようなもの)
    • HelloRequest: 1つのフィールドをもち、SayHelloの呼び出し時メッセージとして使われます。
    • HelloReply: 1つのフィールドをもち、SayHelloの返却メッセージとして使われます。

※ protocol buffersの仕様については、公式ページを参照ください

2. 定義ファイルをコンパイル

$ cd $GOPATH/src/google.golang.org/grpc/examples/helloworld
$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
  • 上記コマンドを実行すると「helloworld.prot」と同じディレクトリに「helloworld.pb.go」というgoファイルが出力されます

「helloworld.pb.go」

  • 色々と自動生成でソースが出力されていますが、詳細は省略して定義した箇所に関連しそうな部分だけ抜粋しています
package helloworld

import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"

import (
    context "golang.org/x/net/context"
    grpc "google.golang.org/grpc"
)

// 定義したフィールドのゲッターメソッド
func (m *HelloRequest) GetName() string {
    if m != nil {
        return m.Name
    }
    return ""
}

// 定義したフィールドのゲッターメソッド
func (m *HelloReply) GetMessage() string {
    if m != nil {
        return m.Message
    }
    return ""
}

/*----------- クライアント側のコード --------------------*/
type GreeterClient interface {
    // 定義したメソッドのクライアント側のインターフェース
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
    cc *grpc.ClientConn
}

func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
    return &greeterClient{cc}
}

// クライアントが呼び出すSayHelloメソッド
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

/*----------- サーバー側のコード --------------------*/
type GreeterServer interface {
    // 定義したメソッドのサーバー側のインターフェース
    // メソッドの実装は、サーバー側で記述する
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

// サーバーインターフェースを登録する為のメソッド
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}
  • この様に対応した言語に合わせてインターフェースのコードが自動生成されます

クライアントの実装

  • 上記自動生成されたgoコードを使って実装されているクライアントのコードを見てみます。

greeter_client/main.go

package main

import (
    "log"
    "os"
    "time"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
  // 生成されたpb.goファイルのimport
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

func main() {
    // grpcのコネクションを生成
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
  // GreeterのClientの生成
    c := pb.NewGreeterClient(conn)
  // コマンドラインに引数があればそれをnameとして渡す
  // なければ world
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
  // SayHelloメソッドの呼び出し
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}
  • クライアン側はgrpcのコネクションとともに生成したクライアントからSayHelloメソッドを呼んでいます。

サーバーの実装

  • 上記自動生成されたgoコードを使って実装されているサーバー側のコードを見てみます。

greeter_server/main.go

package main

import (
    "log"
    "net"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc/reflection"
)

const (
    port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// 自動生成されたインターフェースに対してメソッドを実装する
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
  // 自動生成されたHelloReplyを使ってレスポンスを返す
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
  // 生成された登録メソッドにgrpcのサーバーを登録する
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    // おそらくこのreflectionにサーバーを登録することでメッセージをハンドリングしてくれる?
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
  • サーバー側もシンプルでGreeterServerに対してgrpcサーバーを登録するだけのようです。

サンプルを動かす

サーバーの起動

$ go run greeter_server/main.go

クライアントの実行

引数なし

$ go run greeter_client/main.go

出力

2018/05/05 11:56:04 Greeting: Hello world

引数あり

$ go run greeter_client/main.go hoge

出力

2018/05/05 11:56:45 Greeting: Hello hoge

感想

  • 詳細はもう少し勉強する必要がありますが、手順は簡単でした
  • .protoは可読性が高いのでメンテナンス性は良さそうです
  • ソースコードが自動生成されるのも運用的には良さそうです

参考