gRPCとは
gRPCの公式サイトでは。
gRPC is a modern open source high performance RPC framework that can run in any environment.
とかかれています。
どうやら、オープンソースでパフォーマンスがよくていろんな環境で動作させることができる夢のようなRPCフレームワークらしい。 それだけでも十分魅力的なんですが。私が一番気になったのは、プロトコル定義ファイル(protoファイルって言う)ってのを書き、それをコンパイルすることでいろんな言語のサーバとクライアントプログラムが作れるっぽいところ。 (実際にgRPCを使ってみてわかったんですが、この表現は言い過ぎかな?と思ってます。)
よくある案件だと。サーバプログラムはGoで実装して、クライアントはJavaScriptとか。この場合、サーバプログラムとクライアントプログラムをそれぞれ開発しなきゃいけないので色々とコストがかかる。そもそも弊社ではエンジニアが私一人ですし、Goは得意だけどできればJavaScriptは書きたくないし、でも書かなきゃだし。。。
とにかく、コンパイルすることでいろんな言語、サーバとクライアントができるってのはすごく魅力的だったので試してみた。
gRPCをつかうために必要なものをインストールする
公式ページのQuickStartにやり方が載っているので、その通り進めていきます。
Protocol Buffersをインストール
gRPCではProtocol Buffersというものを使っています。これは何かというと、例えばサーバとクライアント間のやり取りをするときに、今まではJSONデータを送受信することでやり取りしていることが一般的でしたが、gRPCではProtocol Buffersという仕組みをつかって、データをシリアライズして送信し、受信した側はデシリアライズしてデータを読むようなことをしている。
JSONをやり取りするとどうしてもデータ量が増えてしまうが、Protocol Buffersだとシリアライズされたバイナリのようなデータをやり取りするのでデータ量が減る。データ量が減るということは通信速度も上がってみんなハッピー。気になるのはシリアライズとデシリアライズにどのくらいオーバーヘッドがあるのか。 でも、よく考えたらJSONとかもパースしてるしあんまり気になんないかもしれない。どちらにしろ、シリアライズ・デシリアライズにかかる時間的なコストと、でかいデータを通信する際の時間的なコストを比べると。ね。
と、いうことでProtocol Buffersをインストールします。これをインストールすればprotocというコマンドが使えるようになり、このprotocが.protoファイルをコンパイルしてgRPCで利用できるようなコードを生成するコマンドだと思えば良いです。 Macだとhomebrewですぐにインストールできます。
brew install protobuf
# 確認
protoc --version
libprotoc 3.4.0
protoファイルからGoのコードを生成するためのプラグインをインストールする
protocでprotoファイルからGoのコードを生成するためのプラグインが公式からリリースされているのでそれを使います。
go get -u github.com/golang/protobuf/protoc-gen-go
これで準備は完了です。
gRPCに必要なprotoファイルを作る
とりあえず必要最低限なprotoファイルを作る。参考にしたのはgRPCのgithubリポジトリ。このリポジトリのexampleってところにサンプルがある。
mkdir test_project
# protoファイルにプロトコルを書いていく
nvim test_project/helloworld.proto
# ファイルの中身は↓のような感じ
cat test_project/helloworld.proto
// これはお約束
syntax = "proto3";
// パッケージ名を記載している
package helloworld;
// serviceで定義したものが一つのAPIサーバになるようなイメージ
// コンパイルされたサーバプログラムのコードを読めばよく分かる
service Greeter {
// ここでRPCのメソッドを定義する
// このメソッドはHelloRequestを受け取ってHelloReplyを返す
// 受け取るデータと返却するデータはさらに下の方のmessageで定義しているもの
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// GreeterのSayHelloで受け取るデータ
message HelloRequest {
// nameという項目名で文字列を受け取る
string name = 1;
}
// GreeterのSayHelloで返すデータ
message HelloReply {
// messageという項目名で文字列を返却する
string message = 1;
}
protoファイルをコンパイルしてgRPC用のコードを生成する
protocコマンドを使ってprotoファイルをコンパイルする。コンパイルするときに出力する言語を指定する。
# ディレクトリ構成
tree
.
└── test_project
└── helloworld.proto
# protocコマンドを使ってprotoファイルをコンパイルする
# --go_outっていうのはGo言語へコンパイルするのを指定していて、pluginsとかgrpcとかはお約束。
protoc -I ./test_project test_project/helloworld.proto --go_out=plugins=grpc:test_project
tree
.
└── test_project
├── helloworld.pb.go # pb.goというGoのソースコードが吐かれる
└── helloworld.proto
コンパイルしたコードをつかってgRPCサーバのプログラムを作る
サーバとクライアント間の通信プロトコル部分のGoコードは↑のprotocコマンドで作成済み。 これを使ってサーバプログラムを実装していく。
今回もgRPCのgithubリポジトリを参考にした。
mkdir server
nvim server/main.go # サーバプログラムを書いていく
cat server/main.go
package main
import (
"context"
"log"
"net"
pb "github.com/path/to/test_project"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
// server 構造体
// この構造体にprotoファイルで定期したRPCのメソッドを実装していく
type server struct{}
// SayHello メソッド
// protoファイルのservice部分に記載したRPCメソッドを実装する
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
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)
}
s := grpc.NewServer()
// server構造体をGreeterとしてProtocol Bufferに登録する
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
とりあえずこれでサーバプログラムはOK
コンパイルしたコードを使ってgRPCクライアントのプログラムを作る
サーバプログラムの次はクライアントプログラムを実装していく。
こちらもgRPCのgithubリポジトリを参考にした。
mkdir client
nvim client/main.go # クライアントプログラムを書いていく
cat client/main.go
package main
import (
"context"
"log"
"os"
pb "github.com/path/to/test_project"
"google.golang.org/grpc"
)
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()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// gRPCサーバのSayHelloメソッドを呼び出している
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
これでクライアントプログラムはOK
gRPCのサーバ・クライアントプログラムを動作させてみる
まずはサーバプログラムを動かす。
cd server
go run main.go
エラーがでなければOK この状態で別のターミナルからクライアントプログラムを動かすとサーバとのやり取りが行われる。
cd client
go run main.go
# 出力結果
2017/11/04 10:56:01 Greeting: Hello world
こんな感じでclientプログラムのdefaultName変数で指定したworldを付与した文字列が返却される。
gRPCについてのまとめ
冒頭にも書きましたが、protoファイルを書くことでサーバとクライアントのプログラムへコンパイルできると言うのは言い過ぎだなー。という印象です。私が、どこかの記事を読んだときに勝手にサーバとクライアントのプログラムへ変換できるんだ!!と解釈したのかもしれないですが。。。protoファイルをサーバとクライアントのシリアライズ・デシリアライズプログラムへコンパイルできる。みたいな感じのほうがしっくり来るかな?って感じです。
とはいえ、出来上がったコードを組み込むだけで、サーバもクライアントも書けるのでゼロから作るよりは確かに楽。何よりもprotoファイルを作っておけばAPIのエンドポイントの管理がし易いんじゃないかなと思いました。
今回はサーバもクライアントもGoで作ってますが、次回はクライアントを別の言語にしてみたものも紹介できたらいいなーと思います。