基本チュートリアル

Ruby における gRPC の基本的なチュートリアル入門。

基本チュートリアル

Ruby における gRPC の基本的なチュートリアル入門。

このチュートリアルでは、Ruby プログラマー向けの gRPC の基本的な使い方を紹介します。

この例を段階的に実行することで、以下のことを学びます。

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

gRPC の 「gRPC の紹介」 を読んだことがあり、protocol buffers に慣れていることを前提としています。このチュートリアルでの例は protocol buffers 言語の proto3 バージョンを使用しています。詳細については、proto3 言語ガイド を参照してください。

gRPCを使用する理由

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

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

サンプルコードとセットアップ

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

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

次に、カレントディレクトリを examples/ruby/route_guide に変更します。

cd examples/ruby/route_guide

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

サービスを定義する

(gRPC の紹介で説明したように) 最初の手順は、protocol buffers を使用して gRPC サービスとメソッドのリクエストおよびレスポンスの型を定義することです。完全な .proto ファイルは examples/protos/route_guide.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 プラグインを使用して protocol buffer コンパイラ protoc で行います。

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

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

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

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

  • lib/route_guide.pbExamples::RouteGuide モジュールを定義します。
    • これには、リクエストおよびレスポンスメッセージの型を生成、シリアライズ、取得するためのすべての protocol buffer コードが含まれています。
  • 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 のための _call、クライアントの Point protocol buffer リクエストが渡され、Feature protocol buffer が返されます。メソッド内では、適切な情報で 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_routeEnumerable を使用しますが、ここではそれがコールオブジェクトから取得されます。これは前の例では無視していました。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::RpcServerrun を呼び出し、サービス用の RPC サーバーを作成して起動します。

クライアントを作成する

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

スタブを作成する

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

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

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

サービスメソッドを呼び出す

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

シンプルなRPC

シンプルなRPCGetFeatureの呼び出しは、ローカルメソッドの呼び出しとほとんど同じくらい簡単です。

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

ご覧のとおり、リクエスト protocol buffer オブジェクト (この場合は Point) を作成して入力し、サーバーが入力するためのレスポンス protocol buffer オブジェクトを作成します。最後に、スタブでメソッドを呼び出し、コンテキスト、リクエスト、レスポンスを渡します。メソッドが OK を返した場合、レスポンスオブジェクトからサーバーからのレスポンス情報を読み取ることができます。

ストリーミングRPC

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

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 オブジェクトが返されます。次に、操作 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}" }

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

試してみましょう!

例のディレクトリから作業します。

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