基本チュートリアル

RubyによるgRPCの基本的なチュートリアル入門です。

基本チュートリアル

RubyによるgRPCの基本的なチュートリアル入門です。

このチュートリアルは、gRPCを使った作業について、Rubyプログラマー向けの基本的な入門を提供します。

この例を通して、次の方法を学習します。

  • .protoファイルでサービスを定義する。
  • プロトコルバッファコンパイラを使用して、サーバーとクライアントのコードを生成する。
  • Ruby gRPC APIを使用して、サービスの単純なクライアントとサーバーを作成する。

gRPCのはじめにを読了済みであり、プロトコルバッファに精通していることを前提としています。このチュートリアルの例では、プロトコルバッファ言語のproto3バージョンを使用しています。proto3言語ガイドで詳細を確認できます。

gRPCを使う理由

私たちの例は、クライアントがルート上のフィーチャに関する情報を取得し、ルートのサマリーを作成し、サーバーや他のクライアントと交通情報などのルート情報を交換できる、シンプルなルートマッピングアプリケーションです。

gRPCを使用すると、サービスを一度`.proto`ファイルで定義し、gRPCでサポートされている任意の言語でクライアントとサーバーを生成できます。これらは、大規模データセンター内のサーバーからタブレットまで、さまざまな環境で実行できます。異なる言語や環境間の通信の複雑さはすべてgRPCによって処理されます。効率的なシリアライゼーション、シンプルなIDL、簡単なインターフェースの更新など、プロトコルバッファを使用するすべての利点も得られます。

コード例と設定

チュートリアルのコード例は、grpc/grpc/examples/ruby/route_guideにあります。例をダウンロードするには、次のコマンドを実行して`grpc`リポジトリをクローンします。

$ git clone -b v1.62.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc
$ cd grpc

次に、現在のディレクトリを`examples/ruby/route_guide`に変更します。

$ cd examples/ruby/route_guide

サーバーとクライアントのインターフェースコードを生成するために必要なツールもインストールする必要があります。まだインストールしていない場合は、クイックスタートの手順に従ってください。

サービスの定義

最初のステップは(gRPCのはじめにでわかるように)、プロトコルバッファを使用して、gRPC *サービス*とメソッドの*リクエスト*および*レスポンス*タイプを定義することです。`examples/protos/route_guide.proto`で完全な`.proto`ファイルを確認できます。

サービスを定義するには、`.proto`ファイルで名前付き`service`を指定します。

service RouteGuide {
   ...
}

次に、サービス定義内に`rpc`メソッドを定義し、そのリクエストとレスポンスタイプを指定します。gRPCでは、4種類のサービスメソッドを定義できます。これらはすべて`RouteGuide`サービスで使用されています。

  • クライアントがスタブを使用してサーバーにリクエストを送信し、通常の関数呼び出しのようにレスポンスが返されるのを待つ、*単純なRPC*。

    // Obtains the feature at a given position.
    rpc GetFeature(Point) returns (Feature) {}
    
  • クライアントがサーバーにリクエストを送信し、一連のメッセージを読み取るストリームを取得する、*サーバーサイドストリーミングRPC*。クライアントは、メッセージがなくなるまで返されたストリームから読み取ります。例に見られるように、レスポンスタイプの前に`stream`キーワードを配置することで、サーバーサイドストリーミングメソッドを指定します。

    // Obtains the Features available within the given Rectangle.  Results are
    // streamed rather than returned at once (e.g. in a response message with a
    // repeated field), as the rectangle may cover a large area and contain a
    // huge number of features.
    rpc ListFeatures(Rectangle) returns (stream Feature) {}
    
  • クライアントがメッセージのシーケンスを書き込み、提供されたストリームを使用してサーバーに送信する、*クライアントサイドストリーミングRPC*。クライアントがメッセージの書き込みを完了すると、サーバーがすべてを読み取り、レスポンスを返すのを待ちます。リクエストタイプの前に`stream`キーワードを配置することで、クライアントサイドストリーミングメソッドを指定します。

    // Accepts a stream of Points on a route being traversed, returning a
    // RouteSummary when traversal is completed.
    rpc RecordRoute(stream Point) returns (RouteSummary) {}
    
  • 両方とも読み書きストリームを使用してメッセージのシーケンスを送信する、*双方向ストリーミングRPC*。2つのストリームは独立して動作するため、クライアントとサーバーは好きな順番で読み書きできます。たとえば、サーバーは、レスポンスを書き込む前にすべてのクライアントメッセージの受信を待つことも、メッセージを読み取ってからメッセージを書き込むことも、読み取りと書き込みのその他の組み合わせを行うこともできます。各ストリームのメッセージの順序は保持されます。リクエストとレスポンスの両方に`stream`キーワードを配置することで、このタイプのメソッドを指定します。

    // Accepts a stream of RouteNotes sent while a route is being traversed,
    // while receiving other RouteNotes (e.g. from other users).
    rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
    

私たちの`.proto`ファイルには、サービスメソッドで使用されるすべてのリクエストとレスポンスタイプのプロトコルバッファメッセージタイプ定義も含まれています。たとえば、`Point`メッセージタイプを次に示します。

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

クライアントとサーバーコードの生成

次に、`.proto`サービス定義からgRPCクライアントとサーバーのインターフェースを生成する必要があります。これには、特別なgRPC Rubyプラグインを使用してプロトコルバッファコンパイラ`protoc`を使用します。

自分で実行する場合は、gRPCprotocがインストールされていることを確認してください。

完了したら、次のコマンドを使用してRubyコードを生成できます。

$ grpc_tools_ruby_protoc -I ../../protos --ruby_out=../lib --grpc_out=../lib ../../protos/route_guide.proto

このコマンドを実行すると、libディレクトリに次のファイルが再生成されます。

  • `lib/route_guide.pb`は、`Examples::RouteGuide`モジュールを定義します。
    • これには、リクエストとレスポンスのメッセージタイプを生成、シリアライズ、取得するためのすべてのプロトコルバッファコードが含まれています。
  • `lib/route_guide_services.pb`は、スタブとサービスクラスを使用して`Examples::RouteGuide`を拡張します。
    • RouteGuideサービスの実装を定義するときに基底クラスとして使用する`Service`クラス。
    • リモートRouteGuideインスタンスにアクセスするために使用できる`Stub`クラス。

サーバーの作成

最初に、`RouteGuide`サーバーを作成する方法を見てみましょう。gRPCクライアントの作成に関心がある場合のみ、このセクションをスキップしてクライアントの作成に直接進むことができます(ただし、興味深いものかもしれません!)。

`RouteGuide`サービスを機能させるには、2つの部分があります。

  • サービス定義から生成されたサービスインターフェースの実装:サービスの実際の「作業」を実行します。
  • クライアントからのリクエストをリッスンし、サービスレスポンスを返すgRPCサーバーを実行します。

私たちの例である`RouteGuide`サーバーは、examples/ruby/route_guide/route_guide_server.rbにあります。どのように機能するか詳しく見てみましょう。

RouteGuideの実装

ご覧のとおり、私たちのサーバーには、生成された`RouteGuide::Service`を拡張する`ServerImpl`クラスがあります。

# ServerImpl provides an implementation of the RouteGuide service.
class ServerImpl < RouteGuide::Service

`ServerImpl`は、すべてのサービスメソッドを実装します。最初に、最も単純なタイプである`GetFeature`を見てみましょう。これは、クライアントから`Point`を取得し、対応するフィーチャ情報をデータベースから`Feature`で返すだけです。

def get_feature(point, _call)
  name = @feature_db[{
    'longitude' => point.longitude,
    'latitude' => point.latitude }] || ''
  Feature.new(location: point, name: name)
end

メソッドには、RPCの_コール_、クライアントの`Point`プロトコルバッファリクエストが渡され、`Feature`プロトコルバッファを返します。メソッドでは、適切な情報を使用して`Feature`を作成し、それを`return`します。

次に、もう少し複雑なストリーミングRPCを見てみましょう。`ListFeatures`はサーバーサイドストリーミングRPCであるため、クライアントに複数の`Feature`を送信する必要があります。

# in ServerImpl

  def list_features(rectangle, _call)
    RectangleEnum.new(@feature_db, rectangle).each
  end

ご覧のとおり、ここでのリクエストオブジェクトは、クライアントが`Feature`を見つけるための`Rectangle`ですが、単純なレスポンスを返す代わりに、レスポンスを生成するEnumeratorを返す必要があります。メソッドでは、ヘルパークラス`RectangleEnum`を使用して、Enumeratorの実装として機能します。

同様に、クライアントサイドストリーミングメソッド`record_route`はEnumerableを使用しますが、ここでは、以前の例では無視していたcallオブジェクトから取得します。`call.each_remote_read`は、クライアントによって順番に送信された各メッセージを生成します。

call.each_remote_read do |point|
  ...
end

最後に、双方向ストリーミングRPC `route_chat`を見てみましょう。

def route_chat(notes)
  RouteChatEnumerator.new(notes, @received_notes).each_item
end

ここでは、メソッドはEnumerableを受け取りますが、レスポンスを生成するEnumeratorも返します。どちら側も常に書き込まれた順序で相手のメッセージを取得しますが、クライアントとサーバーの両方で任意の順序で読み書きできます。ストリームは完全に独立して動作します。

サーバーの起動

すべてのメソッドを実装したら、クライアントが実際にサービスを使用できるように、gRPCサーバーを起動する必要もあります。次のスニペットは、`RouteGuide`サービスでこれを行う方法を示しています。

port = '0.0.0.0:50051'
s = GRPC::RpcServer.new
s.add_http2_port(port, :this_port_is_insecure)
GRPC.logger.info("... running insecurely on #{port}")
s.handle(ServerImpl.new(feature_db))
# Runs the server with SIGHUP, SIGINT and SIGQUIT signal handlers to
#   gracefully shutdown.
# User could also choose to run server via call to run_till_terminated
s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])

ご覧のとおり、`GRPC::RpcServer`を使用してサーバーを構築して起動します。これを行うには、

  1. サービス実装クラス`ServerImpl`のインスタンスを作成します。
  2. ビルダーの`add_http2_port`メソッドを使用して、クライアントリクエストをリッスンするために使用するアドレスとポートを指定します。
  3. `GRPC::RpcServer`にサービス実装を登録します。
  4. `GRPC::RpcServer`で`run`を呼び出して、サービスのRPCサーバーを作成して起動します。

クライアントの作成

このセクションでは、RouteGuideサービス用のRubyクライアントの作成方法について説明します。完全なクライアントコード例は、examples/ruby/route_guide/route_guide_client.rbで確認できます。

スタブの作成

サービスメソッドを呼び出すには、まずスタブを作成する必要があります。

.protoファイルから生成されたRouteGuideモジュールのStubクラスを使用します。

stub = RouteGuide::Stub.new('localhost:50051')

サービスメソッドの呼び出し

それでは、サービスメソッドの呼び出し方法を見てみましょう。gRPC Rubyは各メソッドのブロッキング/同期バージョンのみを提供することに注意してください。つまり、RPC呼び出しはサーバーからの応答を待ち、応答を返すか、例外を発生させます。

単純なRPC

単純なRPCであるGetFeatureの呼び出しは、ローカルメソッドの呼び出しとほぼ同じくらい簡単です。

GET_FEATURE_POINTS = [
  Point.new(latitude:  409_146_138, longitude: -746_188_906),
  Point.new(latitude:  0, longitude: 0)
]
..
  GET_FEATURE_POINTS.each do |pt|
    resp = stub.get_feature(pt)
	...
    p "- found '#{resp.name}' at #{pt.inspect}"
  end

ご覧のように、リクエストプロトコルバッファオブジェクト(この場合はPoint)を作成して設定し、サーバーが記入するレスポンスプロトコルバッファオブジェクトを作成します。最後に、コンテキスト、リクエスト、レスポンスを渡して、スタブでメソッドを呼び出します。メソッドがOKを返す場合、レスポンスオブジェクトからサーバーからのレスポンス情報を読み取ることができます。

ストリーミングRPC

次に、ストリーミングメソッドを見てみましょう。サーバーの作成を既に読んでいる場合は、この一部は非常に馴染みがあるかもしれません。ストリーミングRPCは、両側で同様の方法で実装されます。ここでは、FeaturesEnumerableを返すサーバーサイドストリーミングメソッドlist_featuresを呼び出します。

resps = stub.list_features(LIST_FEATURES_RECT)
resps.each do |r|
  p "- found '#{r.name}' at #{r.location.inspect}"
end

RPCストリームのノンブロッキングな使用は、複数のスレッドとreturn_op: trueフラグを使用して実現できます。return_op: trueフラグを渡すと、RPCの実行は延期され、Operationオブジェクトが返されます。その後、別のスレッドでoperationのexecute関数を呼び出すことでRPCを実行できます。メインスレッドは、statuscancelled?cancelなどのコンテキストメソッドとゲッターを使用してRPCを管理できます。これは、メインスレッドを許容できない時間ブロックする可能性のある永続的または長時間実行されるRPCセッションに役立ちます。

op = stub.list_features(LIST_FEATURES_RECT, return_op: true)
Thread.new do 
  resps = op.execute
  resps.each do |r|
    p "- found '#{r.name}' at #{r.location.inspect}"
  end
rescue GRPC::Cancelled => e
  p "operation cancel called - #{e}"
end

# controls for the operation
op.status
op.cancelled?
op.cancel # attempts to cancel the RPC with a GRPC::Cancelled status; there's a fundamental race condition where cancelling the RPC can race against RPC termination for a different reason - invoking `cancel` doesn't necessarily guarantee a `Cancelled` status

クライアントサイドストリーミングメソッドrecord_routeは似ていますが、ここではサーバーにEnumerableを渡します。

...
reqs = RandomRoute.new(features, points_on_route)
resp = stub.record_route(reqs.each)
...

最後に、双方向ストリーミングRPCであるroute_chatを見てみましょう。この場合、メソッドにEnumerableを渡し、Enumerableを返します。

sleeping_enumerator = SleepingEnumerator.new(ROUTE_CHAT_NOTES, 1)
stub.route_chat(sleeping_enumerator.each_item) { |r| p "received #{r.inspect}" }

この例ではうまく示されていませんが、各列挙可能オブジェクトは互いに独立しています。クライアントとサーバーの両方で任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみましょう!

examplesディレクトリで作業します

$ cd examples/ruby

クライアントとサーバーをビルドします

$ gem install bundler && bundle install

サーバーを実行します

$ bundle exec route_guide/route_guide_server.rb ../python/route_guide/route_guide_db.json

別のターミナルから、クライアントを実行します

$ bundle exec route_guide/route_guide_client.rb ../python/route_guide/route_guide_db.json