at backyard

Color my life with the chaos of trouble.

Node.jsでgRPCに入門する

実は何度目かの入門。
普段から使わないとすぐ忘れてしまうのでHello worldなサンプルを動かすまでの過程をメモで残すことにした。
なるべくコードはシンプルになるように意識している。

目次

書いていたら長くなってしまったので目次つけます

自身の環境

  • M1 MacBook Air
  • Node.js(v14.17.1)
  • 今回は yarn を使って作業しています

とりあえずgRPCを使ってHelloWorldするだけのメモ

M1 macだと grpc-tools をインストールしようとした際に node-pre-gyp 関連でエラーになる。

よって下記のように依存関係はインストールしている。

npm_config_target_arch=x64 yarn add grpc-tools @grpc/grpc-js google-protobuf

下記の記事を参照している

qiita.com

hello.protoを作成する

hello.protoを作成

syntax = "proto3";

package hello;

service HelloWorld {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

@grpc/proto-loaderを利用すればコード生成は行わずとも実行ができる

最初色々とインターネットでgRPCについて調べていたとき、大抵はここでprotoファイルをもとにコードの生成を行ったりしているのだが、Node.jsでは @grpc/proto-loader というパッケージを利用することで、上で定義した hello.proto ファイルをもとに動的に生成できる仕組みが用意されているようだ。
(つまり @grpc/proto-loader を利用する場合、コード生成は行わなくともコードを実行することができる)

というわけで@grpc/proto-loaderを利用することで、下記のコードサンプルはそのまま動く

server側のNode.jsのサンプル (@grpc/proto-loader利用)

const PROTO_PATH = './hello.proto';

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).hello;

const sayHello = (call, callback) => {
  console.log('Server: Call sayHello RPC method');
  callback(null, { message: 'gRPC Sample: ' + call.request.name });
};

const main = () => {
  const server = new grpc.Server();
  server.addService(hello_proto.HelloWorld.service, { sayHello: sayHello });
  server.bindAsync(
    '0.0.0.0:50051',
    grpc.ServerCredentials.createInsecure(),
    () => {
      server.start();
      console.log('Server: Start');
    }
  );
};

main();

下記の protoLoader.loadSync というところで hello.proto を読んでいるのがわかる。

const PROTO_PATH = './hello.proto';

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).hello;

client側のNode.jsのサンプル(@grpc/proto-loader利用)

そして、client側のコード。このコードを実行することで上に書いたサーバプログラムにリクエストが送られ、レスポンスが返される。

const PROTO_PATH = './hello.proto';
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const hello_proto = grpc.loadPackageDefinition(packageDefinition).hello;

const main = () => {
  const client = new hello_proto.HelloWorld(
    '0.0.0.0:50051',
    grpc.credentials.createInsecure()
  );
  client.sayHello({ name: 'HELLO WORLD!' }, function (err, response) {
    console.log(response.message);
  });
};

main();

これで下記のようにプログラムをそれぞれプログラムを実行すると、 gRPC Sample: HELLO WORLD! という文字を始め、いくつかのログが出力されるのが確認できる。

node server.js
node client.js
# gRPC Sample: HELLO WORLD! はこちら側で出力される

プロトコル定義ファイルからコードを生成して利用する

次はhello.protoをもとにコードを生成して利用する方法についてメモしていく

の前に、grpcを使う上で利用することになるライブラリについて書いておく。
(ザックリ調べた内容をメモしているだけの素人メモなのでご了承ください)

grpcと@grpc/grpc-js

Node.jsからgrpcを利用する場合、grpc@grpc/grpc-js というのがある。

最初は grpcが利用されていたようで、後発が @grpc/grpc-js らしいのだが、下記のnpmのページに行ってみるとわかるように、現在 grpc は非推奨となっており、 @grpc/grpc-js への以降が推奨されている。
このサンプルでも @grpc/grpc-js を利用する。

www.npmjs.com

www.npmjs.com

protoファイルから@grpc/grpc-js向けのコードを生成する方法

コードを生成する際に --grpc_out=grpc_js:./gen/proto というオプションを付けているのだが、これをつけることで@grpc/grpc-js用のコードを生成される。
(最初このオプションを付け忘れていて、ライブラリが見つからない的なエラーになって10分ほど迷っていた)

# 生成したコードを格納するためのディレクトリ
mkdir -p gen/proto

# コード生成
yarn run grpc_tools_node_protoc -I. --js_out=import_style=commonjs,binary:./gen/proto --grpc_out=grpc_js:./gen/proto ./hello.proto

これで gen/proto 配下にコードが生成される。

次にこの生成されたコードを利用するコードをserver・clientそれぞれで書いていく。

server側のNode.jsのサンプル

これはclient側のコードにも言えることだが、先ほどhello.protoで定義した namemessagegetName()getMessage() という形で利用している点が先ほどのサンプルとは異なる。

なおコード生成の時点で hello_grpc_pb.jshello_pb.js という2種類のファイルが生成されている。
これらのコードをそれぞれ読み込んでコードを実行していくようになる。

  • hello_grpc_pb.js は gRPCに対応するserverとclientを提供するサービスらしい。
    • ( 生成されたコードを見ると service HelloWorld に関する記述あり)
  • hello_pb.jsHelloRequestHelloReply というそれぞれ定義された message に対応したコードのようだ。
    • (注意:なんとなく生成されたコードを読んだ雰囲気で書いています。)
const grpc = require('@grpc/grpc-js');
const { HelloWorldService } = require('./gen/proto/hello_grpc_pb');
const { HelloReply } = require('./gen/proto/hello_pb');

const sayHello = (call, callback) => {
  const reply = new HelloReply();
  console.log('Server: Call sayHello RPC method');
  const message = 'gRPC Sample: ' + call.request.getName();
  reply.setMessage(message);
  callback(null, reply);
};

const main = () => {
  const server = new grpc.Server();
  server.addService(HelloWorldService, { sayHello: sayHello });
  server.bindAsync(
    '0.0.0.0:50051',
    grpc.ServerCredentials.createInsecure(),
    () => {
      server.start();
      console.log('Server: Start');
    }
  );
};

main();

client側のNode.jsのサンプル

client側も同じく hello_grpc_pb.jshello_pb.js をそれぞれ読み込んでいる。

const grpc = require('@grpc/grpc-js');
const { HelloWorldClient } = require('./gen/proto/hello_grpc_pb');
const { HelloRequest } = require('./gen/proto/hello_pb');

const main = () => {
  const request = new HelloRequest();
  const client = new HelloWorldClient(
    '0.0.0.0:50051',
    grpc.credentials.createInsecure()
  );
  request.setName('HELLO WORLD!');
  client.sayHello(request, function (err, response) {
    console.log(response.getMessage());
  });
};

main();

以上、Hello Worldなサンプルでした。