gRPC × Rubyのチュートリアルをカスタムしてやってみた
gRPC公式のRuby版チュートリアルを参考に、手元で試してみた記録。
gRPCで開発をするときの全体感みたいなものをつかめたらいいなぁくらいのところからスタート。
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.protolib 以下にコードが生成された。
$ 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 respDarktree::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) という例外が発生した。
それぞれ実行時に例外が発生してくれるので、 テストを書いておけば 実装とインターフェースがずれてしまった場合に気がつけそうだ。

h3pei
フリーランスのソフトウェアエンジニア。Ruby / Rails アプリケーションの開発が得意領域。設計・実装・運用まで含めてプロダクト開発が好きです。
MENTAでアプリケーション開発や学習の支援・伴走もしています。