基本チュートリアル

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

基本チュートリアル

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

このチュートリアルは、gRPC を扱うための基本的な Node.js プログラマー向けの入門を提供します。

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

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

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

gRPCを使用する理由

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

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

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

チュートリアルのサンプルコードは、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 Buffers コンパイラ protoc を使用して静的に生成されたコードを使用します。例はすべて同じように動作し、どちらのサーバーもどちらのクライアントとも使用できます。ディレクトリ名が示唆するように、このドキュメントでは動的に生成されたコードを使用しますが、静的コードの例も自由に参照してください。

例をダウンロードするには、次のコマンドを実行して 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ファイルには、サービスメソッドで使用されるすべてリクエストとレスポンスの型に対応するプロトコルバッファメッセージ型定義も含まれています。たとえば、ここに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();

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

route_guide_server.js の関数は、すべてのサービスメソッドを実装しています。最も簡単なタイプから始めましょう。getFeature は、クライアントから Point を受け取り、データベースから対応する特徴情報(Feature)を返します。

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);
});

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

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() メソッドを使用して、クライアントリクエストをリッスンするために使用するアドレスとポートを指定します。
  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() を使用して終了したら、gRPC に書き込みが終了したことを通知するために、ストリームで end() を呼び出す必要があります。ステータスが 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