gRPC × Rubyのチュートリアルをカスタムしてやってみた
gRPC公式のRuby版チュートリアルを参考に、手元で試してみた記録。
gRPCで開発をするときの全体感みたいなものをつかめたらいいなぁくらいのところからスタート。
https://grpc.io/docs/tutorials/basic/ruby.html
Protocol Buffersでサービスの定義を書く
まず一番最初にやることは、Protocl Buffersを用いてサービスの定義を書くことだ。チュートリアルのコードをそのまま使うのはおもしろくないので、自分で書いてみることにしよう。
ここでは、Darktree
という拙作のフラッシュカードアプリケーションを想定し、Card
というmessageをやりとりする定義を書く。
// proto/card.proto
syntax = "proto3";
message Card {
int64 card_id = 1;
string front = 2;
string back = 3;
enum Status {
OK = 0;
NG = 1;
}
Status status = 4;
}
message GetCardRequest {
int64 card_id = 1;
}
message GetCardResponse {
Card card = 1;
}
service Darktree {
rpc GetCard(GetCardRequest) returns (GetCardResponse) {
}
}
Card
の属性のイメージは次の通り。
card_id
: 連番の数値。Railsアプリケーションのid
をイメージfront
: フラッシュカードの表側に書かれるデータ(例:富士山の標高は?
)back
: フラッシュカードの裏側に書かれるデータ(例:3776m
)status
: フラッシュカードの学習状況。OK
は習得済み、NG
は未習得を表す
そして Card
を取得する GetCard
というrpcを定義。
引数は GetCardRequest
として中身はcard_idのみ、返り値は GetCardResponse
としてCardメッセージ1件を含むものとした。
Protocol BuffersからRubyコードを生成する
Protocl Buffersで作成したサービス定義から、実際のRubyコードを生成する。
grpc_tools_ruby_protoc
というコマンドが必要となるので、bundlerでこれをインストールする。
bundle init
echo 'gem "grpc"' >> Gemfile
echo 'gem "grpc-tools"' >> Gemfile
bundle install --path vendor/bundle
また、生成するコードを配置するディレクトリを事前に作っておく。 ここでは lib
という名前のディレクトリにしておこう。
mkdir lib
そして、生成コマンドを実行する。
bundle exec grpc_tools_ruby_protoc --ruby_out=./lib --grpc_out=./lib ./proto/card.proto
lib 以下にコードが生成された。
$ tree lib
lib
└── proto
├── card_pb.rb
└── card_services_pb.rb
1 directory, 2 files
ちなみにここまでで、作業ディレクトリ全体は次の状態になっている。
$ tree -L 3
.
├── Gemfile
├── Gemfile.lock
├── lib
│ └── proto
│ ├── card_pb.rb
│ └── card_services_pb.rb
├── proto
│ └── card.proto
└── vendor
└── bundle
└── ruby (省略)
サーバの実装を書く
コードは生成され、そのインターフェースはProtocol Buffersで書いた通りだが、実際のサーバの実装は存在しない。
なので、次はインターフェースを満たすサーバの実装を行う必要がある。server.rb
という名前のファイルを新しく作り、そこに書いていこう。
# server.rb
$LOAD_PATH << File.expand_path("./lib")
require 'proto/card_services_pb'
class ServerImpl < Darktree::Service
def get_card(req, _call)
card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'OK')
GetCardResponse.new(card: card)
end
end
server = GRPC::RpcServer.new
server.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)
server.handle(ServerImpl.new)
server.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
基本的にチュートリアルのコードをベースに書いているので詳細は省略するが、get_card
というメソッドを定義して、インターフェース通りに GetCardResponse
オブジェクトを返すようにしている。ファイルの下部は、サーバを起動して待ち受けるためのコードとなっている。
サーバは次のようにして起動し待受け状態にして、次に進もう。
bundle exec ruby server.rb
クライアントからサーバへrpcリクエストを送る
gRPCサーバを起動するところまで終わったので、あとはクライアントからrpcリクエストを投げるだけとなった。
client.rb
という名前の新しいファイルを作り、そこに次のようなコードを書く。
# client.rb
$LOAD_PATH << File.expand_path("./lib")
require 'proto/card_services_pb'
client = Darktree::Stub.new('localhost:50051', :this_channel_is_insecure)
resp = client.get_card(GetCardRequest.new(card_id: 1))
pp resp
Darktree::Stub.new
でクライアントオブジェクトを作り、#get_card
メソッドをコールし、結果を pp
するだけのコードだ。実行してみよう。
$ bundle exec ruby client.rb
<GetCardResponse: card: <Card: card_id: 1, front: "富士山の標高は?", back: "3776m", status: :OK>>
期待通り、GetCardReponse
オブジェクトを取得できた!
※ 作業ディレクトリは最終的に次の状態になった。
$ tree -L 3
.
├── Gemfile
├── Gemfile.lock
├── client.rb
├── lib
│ └── proto
│ ├── card_pb.rb
│ └── card_services_pb.rb
├── proto
│ └── card.proto
├── server.rb
└── vendor
└── bundle
└── ruby
6 directories, 7 files
番外編:インターフェース定義に反するレスポンスを返すとどうなる?
おまけとして、定義したインターフェースに反するレスポンスをサーバ側が返した場合にどのようなことが起きるか試してみた。
server.rb
をいじって、レスポンスに hoge
という属性を追加してみる。
# 省略
class ServerImpl < Darktree::Service
def get_card(req, _call)
card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'OK')
GetCardResponse.new(card: card, hoge: 'hoge') # ★★ `hoge` という属性を勝手に追加 ★★
end
end
# 省略
サーバを起動し..
bundle exec ruby server.rb
クライアントコードを実行する
$ bundle exec ruby client.rb
Traceback (most recent call last):
# 省略
/xxx/vendor/bundle/ruby/2.6.0/gems/grpc-1.19.0-universal-darwin/src/ruby/lib/grpc/generic/active_call.rb:31:in `check_status': 2:ArgumentError: Unknown field name 'hoge' in initialization map entry. (GRPC::Unknown)
ArgumentError: Unknown field name 'hoge' in initialization map entry. (GRPC::Unknown)
という例外が発生した。
続いて、Card
の status
を OKでもNGでもない値にしてクライアントを実行してみる。
# 省略
class ServerImpl < Darktree::Service
def get_card(req, _call)
card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'HOGE') # ★★ statusをHOGEにする ★★
GetCardResponse.new(card: card)
end
end
# 省略
$ bundle exec ruby client.rb
Traceback (most recent call last):
# 省略
/xxx/vendor/bundle/ruby/2.6.0/gems/grpc-1.19.0-universal-darwin/src/ruby/lib/grpc/generic/active_call.rb:31:in `check_status': 2:RangeError: Unknown symbol value for enum field. (GRPC::Unknown)
RangeError: Unknown symbol value for enum field. (GRPC::Unknown)
という例外が発生した。
それぞれ実行時に例外が発生してくれるので、 テストを書いておけば 実装とインターフェースがずれてしまった場合に気がつけそうだ。