自身のメモとしてgRPCをまとめてみました。
gRPCとは
protocol buffersをMessage interchange format(メッセージのI/Oに使うための形式)として使えるリモートプロシージャコールシステムです。
protocol buffersとは
IDL:Interface Definition Language (インタフェース定義言語)を用いたファイルフォーマットです。
gRPCで何ができるのか
クライアントが別のマシンにあるメソッドをまるでローカルにあるかのように使えるようになります。
下の図のように様々なプログラミング言語に対して実装できる。
雑に言うと
今までjsonなどを介してAPIを叩いていたようなところを、もっと確固たる定義をもった上でより高パフォーマンスでやりとりができる…という感じでしょうか。
protocol bufferの定義の仕方
やりとりするmessageの構成は、以下のようにname-valueでfieldを定義します。
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
gRPC Serverの定義はrpcメソッドを使って行います。
// 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
一番シンプルな方法で、単一のメッセージのリクエストに対して、単一のメッセージをレスポンスで返す方法です。
rpc SayHello(HelloRequest) returns (HelloResponse) {
}
Server streaming RPC
単一のメッセージのリクエストに対して、複数のメッセージをレスポンスで返す方法です。
例えばですが、何かのデータの一覧が欲しいなどと言うときに、この方法を使えたりします。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {
}
Client streaming RPC
複数のメッセージのリクエストを受けた上で、単一のメッセージのレスポンスを返します。
greeter.proto
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
Bidirectional streaming RPC
これはリクエストもレスポンスも複数のメッセージをレスポンスを扱う方法ですが、
すべてのメッセージを待って、すべてのメッセージを書き出して返すことも、一つメッセージを受け取る毎に、メッセージを書き出していき、最後にまとめてメッセージを返すことも、サーバー側の実装次第で返ることができます。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {
}
サーバー側の実装
主な処理は以下のような感じです。
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だけ定義している場合)
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が使えます。
message HelloResponse {
repeated string name = 1;
}
こうするとnameの配列を返す事ができます。
複数のmessageを返したい場合はstreamでmessageの中で処理できるならrepeatedというところでしょうか。
oneof
messageに入るのがstringの可能性もあるし、int32の可能性もある…なんて場合に使えます。
message HelloResponse {
oneof name {
string text = 1;
int32 id = 2;
}
}