3
0

More than 3 years have passed since last update.

gRPCメモ

Last updated at Posted at 2020-01-16

自身のメモとしてgRPCをまとめてみました。

gRPCとは

protocol buffersをMessage interchange format(メッセージのI/Oに使うための形式)として使えるリモートプロシージャコールシステムです。

protocol buffersとは

IDL:Interface Definition Language (インタフェース定義言語)を用いたファイルフォーマットです。

gRPCで何ができるのか

クライアントが別のマシンにあるメソッドをまるでローカルにあるかのように使えるようになります。
下の図のように様々なプログラミング言語に対して実装できる。

image.png

雑に言うと

今までjsonなどを介してAPIを叩いていたようなところを、もっと確固たる定義をもった上でより高パフォーマンスでやりとりができる…という感じでしょうか。

protocol bufferの定義の仕方

やりとりするmessageの構成は、以下のようにname-valueでfieldを定義します。

person.proto
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

gRPC Serverの定義はrpcメソッドを使って行います。

greeter.proto
// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

protoファイルののコンパイル

上のgreeter.protoをGo言語ようにコンパイルする場合は以下のようなコマンドを叩きます。
protoc --go_out=plugins=grpc:./ ./greeter.proto

このコンパイルでClient側のメソッドもServer側のメソッドも作成されます。

もし、何かのディレクトリの中にあるすべての.protoをコンパイルしたい場合は、以下のような感じでワイルドカードを使うこともできます。
protoc --go_out=plugins=grpc:./ ./*.proto

もし、どこかのディレクトリに入れたいという場合は、grpc:後のところを書き換えます。
以下の場合はprotoディレクトリの中にコンパイルされたデータが書き出されます。
protoc --go_out=plugins=grpc:proto ./*.proto

もし、どこかのディレクトリからどこかのディレクトリに書き出したい場合は、以下のような書き方になります。
protoc -I protobuf --go_out=plugins=grpc:proto protobuf/*.proto
-I が無いとprotoディレクトリ配下にprotbufディレクトリが作成され、そのなかにコンパイルされたデータが書き出されます。
-I はimportの基点となる場所をしてしてやるオプションと考えてもらえば良いかもしれません。

--go_out=plugins=grpc:と書いているのですが、protocol buffer自体は別にgRPCのためだけのものではないので、このようにgRPCのための書き出しですよーというオプションを書いておく必要があります。

Ruby用にコンパイルしたい場合は --go_outを書き換えます。(複数書けば複数の言語のファイルを同時に書き出せます。)
protoc -I protobuf --ruby_out=plugins=grpc:proto protobuf/*.proto

gRPCサーバーのリクエストとレスポンスの種類

種類は4つあります。

Unary RPC

一番シンプルな方法で、単一のメッセージのリクエストに対して、単一のメッセージをレスポンスで返す方法です。

greeter.proto
rpc SayHello(HelloRequest) returns (HelloResponse) {
}

Server streaming RPC

単一のメッセージのリクエストに対して、複数のメッセージをレスポンスで返す方法です。
例えばですが、何かのデータの一覧が欲しいなどと言うときに、この方法を使えたりします。

greeter.proto
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {
}

Client streaming RPC

複数のメッセージのリクエストを受けた上で、単一のメッセージのレスポンスを返します。
greeter.proto
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

Bidirectional streaming RPC

これはリクエストもレスポンスも複数のメッセージをレスポンスを扱う方法ですが、
すべてのメッセージを待って、すべてのメッセージを書き出して返すことも、一つメッセージを受け取る毎に、メッセージを書き出していき、最後にまとめてメッセージを返すことも、サーバー側の実装次第で返ることができます。

greeter.proto
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {
}

サーバー側の実装

主な処理は以下のような感じです。

main.go
package main
import (
    pb "github.com/{リポジトリ名}/{デフォルトならproto}"
    "flag"
    "net"
    "log"
    "sync"
    "fmt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/testdata"
)

var (
    tls        = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
    certFile   = flag.String("cert_file", "", "The TLS cert file")
    keyFile    = flag.String("key_file", "", "The TLS key file")
    jsonDBFile = flag.String("json_db_file", "", "A json file containing a list of features")
    port       = flag.Int("port", 10000, "The server port")
)

func main() {
    flag.Parse() // 引数を入れて実行したい時用にflagを使ってます。

    // hostとportを設定します。
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // 必要であればcredentialの設定をします。
    var opts []grpc.ServerOption
    if *tls {
        if *certFile == "" {
            *certFile = testdata.Path("server1.pem")
        }
        if *keyFile == "" {
            *keyFile = testdata.Path("server1.key")
        }
        creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
        if err != nil {
            log.Fatalf("Failed to generate credentials %v", err)
        }
        opts = []grpc.ServerOption{grpc.Creds(creds)}
    }

    // gRPCのサーバーを初期化
    grpcServer := grpc.NewServer(opts...)
    // protocでコンパイルされたコードからサーバーを登録するメソッドを実行
    pb.RegisterGreeterServer(grpcServer, newServer())
    // サーバーを起動
    grpcServer.Serve(lis)
}

上の記述で書いていないのですがnewServer()という関数が重要なのですが、Register*Serverの第二引数にはprotoのServer内で定義したメソッドを持ったinterfaceを入れる形になっています。

その為に以下のような形の定義もmainに入れておきます。(例えば、Server streaming RPCだけ定義している場合)

main.go
type greeterServer struct {}
// nilかerrを返せばstreamingが終了します。
func(s *greeterServer) LotsOfReplies(req *pb.LotsOfRepliesRequest, stream pb.Greeter_LotsOfRepliesServer) error {
    hs := []pb.HelloReply{
        pb.HelloReply{ Name: "ohayo" },
        pb.HelloReply{ Name: "konnichiwa" },
        pb.HelloReply{ Name: "konbanwa" },
    }
    for _, h := range hs {
        if err := stream.Send(&h); err != nil {
            return err
        }
    }
    return nil
}

func newServer() *greeterServer {
    s := &greeterServer{}
    return s
}

その他

repeated

protoの定義の中でmessageの中に配列をいれたい場合はRepeatedが使えます。

greeter.proto
message HelloResponse {
  repeated string name = 1;
}

こうするとnameの配列を返す事ができます。
複数のmessageを返したい場合はstreamでmessageの中で処理できるならrepeatedというところでしょうか。

oneof

messageに入るのがstringの可能性もあるし、int32の可能性もある…なんて場合に使えます。

greeter.proto
message HelloResponse {
    oneof name {
        string text = 1;
        int32 id = 2;
    }
}

リファレンス

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0