基本チュートリアル

NodeにおけるgRPCの基本チュートリアルです。

基本チュートリアル

NodeにおけるgRPCの基本チュートリアルです。

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

この例を順を追って見ていくことで、以下の方法を学ぶことができます。

  • .protoファイルでサービスを定義する。
  • Node.js gRPC APIを使用して、サービスのシンプルなクライアントとサーバーを作成する。

gRPCの概要を読み、protocol buffersに精通していることを前提としています。このチュートリアルの例では、proto3バージョンのprotocol buffers言語を使用しています。詳しくは、proto3言語ガイドをご覧ください。

なぜgRPCを使うのか?

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

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

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

このチュートリアルのサンプルコードは、grpc/grpc-node/examples/routeguide/dynamic_codegenにあります。リポジトリを見るとわかるように、grpc/grpc-node/examples/routeguide/static_codegenにも非常によく似た例があります。ルートガイドの例は2つのバージョンがあります。これは、Node.jsでprotocol buffersを扱うために必要なコードを生成する2つの方法があるためです。1つの方法は、Protobuf.jsを使用して実行時にコードを動的に生成する方法で、もう1つは、protocol bufferコンパイラprotocを使用して静的に生成されたコードを使用する方法です。この2つの例は同じように動作し、どちらのサーバーもどちらのクライアントとも使用できます。ディレクトリ名からわかるように、このドキュメントでは動的に生成されたコードを使用したバージョンを使用しますが、静的コードの例も自由にご覧ください。

この例をダウンロードするには、次のコマンドを実行してgrpcリポジトリをクローンします。

$ git clone -b @grpc/grpc-js@1.9.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc-node
$ cd grpc

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

$ cd examples

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

サービスの定義

最初の手順(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ファイルには、サービスメソッドで使用されるすべてのリクエストとレスポンスタイプのprotocol bufferメッセージタイプの定義も含まれています。たとえば、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ファイルからサービス記述子を読み込む

Node.jsライブラリは、実行時にロードされた.protoファイルからサービス記述子とクライアントスタブ定義を動的に生成します。

.protoファイルを読み込むには、gRPC protoローダーライブラリをrequireし、そのloadSync()メソッドを使用してから、出力をgRPCライブラリのloadPackageDefinitionメソッドに渡します。

var PROTO_PATH = __dirname + '/../../protos/route_guide.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
// Suggested options for similarity to existing grpc.load behavior
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
// The protoDescriptor object has the full package hierarchy
var routeguide = protoDescriptor.routeguide;

これが完了すると、スタブコンストラクターはrouteguide名前空間(protoDescriptor.routeguide.RouteGuide)にあり、サービス記述子(サーバーの作成に使用される)はスタブのプロパティ(protoDescriptor.routeguide.RouteGuide.service)です。

サーバーの作成

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

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

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

サンプルのRouteGuideサーバーは、examples/routeguide/dynamic_codegen/route_guide_server.jsにあります。その仕組みを詳しく見てみましょう。

RouteGuideの実装

ご覧のとおり、サーバーには、RouteGuide.service記述子オブジェクトから生成されたServerコンストラクターがあります。

var Server = new grpc.Server();

この場合、デフォルトのgRPCサーバーの動作を提供するRouteGuideの*非同期*バージョンを実装しています。

route_guide_server.jsの関数は、すべてのサービスメソッドを実装しています。最初に、クライアントからPointを取得し、データベースから対応する地物情報をFeatureで返すだけの、最も単純なタイプのgetFeatureを見てみましょう。

function checkFeature(point) {
  var feature;
  // Check if there is already a feature object for the given point
  for (var i = 0; i < feature_list.length; i++) {
    feature = feature_list[i];
    if (feature.location.latitude === point.latitude &&
        feature.location.longitude === point.longitude) {
      return feature;
    }
  }
  var name = '';
  feature = {
    name: name,
    location: point
  };
  return feature;
}
function getFeature(call, callback) {
  callback(null, checkFeature(call.request));
}

このメソッドには、RPCの呼び出しオブジェクトが渡されます。このオブジェクトには、プロパティとしてPointパラメーターと、返されたFeatureを渡すことができるコールバックがあります。メソッド本体では、指定されたポイントに対応するFeatureを入力し、エラーがないことを示すために最初のパラメーターをnullにしてコールバックに渡します。

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

function listFeatures(call) {
  var lo = call.request.lo;
  var hi = call.request.hi;
  var left = _.min([lo.longitude, hi.longitude]);
  var right = _.max([lo.longitude, hi.longitude]);
  var top = _.max([lo.latitude, hi.latitude]);
  var bottom = _.min([lo.latitude, hi.latitude]);
  // For each feature, check if it is in the given bounding box
  _.each(feature_list, function(feature) {
    if (feature.name === '') {
      return;
    }
    if (feature.location.longitude >= left &&
        feature.location.longitude <= right &&
        feature.location.latitude >= bottom &&
        feature.location.latitude <= top) {
      call.write(feature);
    }
  });
  call.end();
}

ご覧のとおり、メソッドパラメーターで呼び出しオブジェクトとコールバックを取得する代わりに、今回はWritableインターフェースを実装するcallオブジェクトを取得します。メソッドでは、返す必要のある数のFeatureオブジェクトを作成し、write()メソッドを使用してcallに書き込みます。最後に、call.end()を呼び出して、すべてのメッセージを送信したことを示します。

クライアントサイドストリーミングメソッドRecordRouteを見ると、今回はcallパラメーターがReaderインターフェースを実装していることを除けば、単項呼び出しと非常によく似ていることがわかります。call'data'イベントは、新しいデータがあるたびに発生し、'end'イベントはすべてのデータが読み取られたときに発生します。単項の場合と同様に、コールバックを呼び出すことで応答します。

call.on('data', function(point) {
  // Process user data
});
call.on('end', function() {
  callback(null, result);
});

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

function routeChat(call) {
  call.on('data', function(note) {
    var key = pointKey(note.location);
    /* For each note sent, respond with all previous notes that correspond to
     * the same point */
    if (route_notes.hasOwnProperty(key)) {
      _.each(route_notes[key], function(note) {
        call.write(note);
      });
    } else {
      route_notes[key] = [];
    }
    // Then add the new note to the list
    route_notes[key].push(JSON.parse(JSON.stringify(note)));
  });
  call.on('end', function() {
    call.end();
  });
}

今回は、メッセージの読み取りと書き込みに使用できる、`Duplex` を実装する `call` を取得します。ここでの読み取りと書き込みの構文は、クライアントストリーミングメソッドとサーバーストリーミングメソッドの場合とまったく同じです。各側は常に相手のメッセージを書き込まれた順序で取得しますが、クライアントとサーバーはどちらも任意の順序で読み書きできます。ストリームは完全に独立して動作します。

サーバーの起動

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

function getServer() {
  var server = new grpc.Server();
  server.addService(routeguide.RouteGuide.service, {
    getFeature: getFeature,
    listFeatures: listFeatures,
    recordRoute: recordRoute,
    routeChat: routeChat
  });
  return server;
}
var routeServer = getServer();
routeServer.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  routeServer.start();
});

ご覧のとおり、サーバーは次の手順で構築および起動します。

  1. `RouteGuide` サービス記述子から `Server` コンストラクターを作成します。
  2. サービスメソッドを実装します。
  3. メソッド実装を使用して `Server` コンストラクターを呼び出すことにより、サーバーのインスタンスを作成します。
  4. インスタンスの `bind()` メソッドを使用して、クライアント要求を listen するために使用するアドレスとポートを指定します。
  5. インスタンスで `start()` を呼び出して、RPC サーバーを起動します。

クライアントの作成

このセクションでは、`RouteGuide` サービスの Node.js クライアントの作成について説明します。完全なクライアントコードの例は、examples/routeguide/dynamic_codegen/route_guide_client.js にあります。

スタブの作成

サービスメソッドを呼び出すには、まず *スタブ* を作成する必要があります。これを行うには、サーバーのアドレスとポートを指定して、RouteGuide スタブコンストラクターを呼び出すだけです。

new routeguide.RouteGuide('localhost:50051', grpc.credentials.createInsecure());

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

次に、サービスメソッドの呼び出し方法を見てみましょう。これらのメソッドはすべて非同期であることに注意してください。イベントまたはコールバックを使用して結果を取得します。

単純RPC

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

var point = {latitude: 409146138, longitude: -746188906};
stub.getFeature(point, function(err, feature) {
  if (err) {
    // process error
  } else {
    // process feature
  }
});

ご覧のとおり、リクエストオブジェクトを作成して設定します。最後に、スタブでメソッドを呼び出し、リクエストとコールバックを渡します。エラーがない場合は、レスポンスオブジェクトからサーバーからのレスポンス情報を読み取ることができます。

console.log('Found feature called "' + feature.name + '" at ' +
    feature.location.latitude/COORD_FACTOR + ', ' +
    feature.location.longitude/COORD_FACTOR);
ストリーミングRPC

次に、ストリーミングメソッドを見てみましょう。サーバーの作成 を既に読んでいる場合、これは非常によく似ているかもしれません。ストリーミング RPC は両側で同様の方法で実装されます。ここでは、地理的な `Feature` のストリームを返すサーバーサイドストリーミングメソッド `ListFeatures` を呼び出します。

var call = client.listFeatures(rectangle);
  call.on('data', function(feature) {
      console.log('Found feature called "' + feature.name + '" at ' +
          feature.location.latitude/COORD_FACTOR + ', ' +
          feature.location.longitude/COORD_FACTOR);
  });
  call.on('end', function() {
    // The server has finished sending
  });
  call.on('error', function(e) {
    // An error has occurred and the stream has been closed.
  });
  call.on('status', function(status) {
    // process status
  });

メソッドにリクエストとコールバックを渡す代わりに、リクエストを渡して `Readable` ストリームオブジェクトを取得します。クライアントは `Readable` の `'data'` イベントを使用して、サーバーのレスポンスを読み取ることができます。このイベントは、メッセージがなくなるまで、各 `Feature` メッセージオブジェクトで発生します。 `'data'` コールバックのエラーは、ストリームのクローズを引き起こしません。 `'error'` イベントは、エラーが発生し、ストリームが閉じられたことを示します。 `'end'` イベントは、サーバーが送信を完了し、エラーが発生しなかったことを示します。 `'error'` または `'end'` のいずれか 1 つだけが発行されます。最後に、サーバーがステータスを送信すると、`'status'` イベントが発生します。

クライアント側ストリーミングメソッド `RecordRoute` も同様ですが、メソッドにコールバックを渡して `Writable` を取得する点が異なります。

var call = client.recordRoute(function(error, stats) {
  if (error) {
    callback(error);
  }
  console.log('Finished trip with', stats.point_count, 'points');
  console.log('Passed', stats.feature_count, 'features');
  console.log('Travelled', stats.distance, 'meters');
  console.log('It took', stats.elapsed_time, 'seconds');
});
function pointSender(lat, lng) {
  return function(callback) {
    console.log('Visiting point ' + lat/COORD_FACTOR + ', ' +
        lng/COORD_FACTOR);
    call.write({
      latitude: lat,
      longitude: lng
    });
    _.delay(callback, _.random(500, 1500));
  };
}
var point_senders = [];
for (var i = 0; i < num_points; i++) {
  var rand_point = feature_list[_.random(0, feature_list.length - 1)];
  point_senders[i] = pointSender(rand_point.location.latitude,
                                 rand_point.location.longitude);
}
async.series(point_senders, function() {
  call.end();
});

`write()` を使用してクライアントのリクエストをストリームに書き込んだら、ストリームで `end()` を呼び出して、書き込みが完了したことを gRPC に通知する必要があります。ステータスが `OK` の場合、`stats` オブジェクトにはサーバーのレスポンスが設定されます。

最後に、双方向ストリーミング RPC `routeChat()` を見てみましょう。この場合、メソッドにコンテキストを渡すだけで、`Duplex` ストリームオブジェクトが返されます。これを使用して、メッセージの書き込みと読み取りの両方を行うことができます。

var call = client.routeChat();

ここでの読み取りと書き込みの構文は、クライアントストリーミングメソッドとサーバーストリーミングメソッドの場合とまったく同じです。各側は常に相手のメッセージを書き込まれた順序で取得しますが、クライアントとサーバーはどちらも任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみよう!

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

$ npm install

サーバーを実行します。

$ node ./routeguide/dynamic_codegen/route_guide_server.js --db_path=./routeguide/dynamic_codegen/route_guide_db.json

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

$ node ./routeguide/dynamic_codegen/route_guide_client.js --db_path=./routeguide/dynamic_codegen/route_guide_db.json
最終更新日 2023 年 9 月 13 日: URL の修正 (#1193) (ba9c6d8)