Node.js gRPC フレームワーク mali.js の紹介


概要

Koa ライクな API を提供する Node.js 製の gRPC フレームワーク、mali.js を試してみる機会があったので、簡単に使い方を紹介します。(公式サイトには Koa ライクとありますが、Express ライクと受け取って良いでしょう)
Node.jsのgrpc実装 grpc パッケージのAPIをラップして簡単にしている印象があるので、手元でgRPCのmockサーバーが必要な際などに役立ちそうだと感じました。

※ 使ってみたという内容記事なので、実装上の tips や実運用でのノウハウなどに関しては触れません。

サーバー実装

gRPC のサーバー/クライアントを実装する際には、まずはじめに .proto でサービス/メッセージタイプの定義を行う必要があります。また定義された.proto ファイルをローディングする方法には以下の二種類に分けられます。

  • Dynamic codegen
    • 実行時に .proto ファイルを解析してコード生成を行う
  • Static codegen
    • 事前に .proto ファイルを解析してコード生成を行う( protoc などのコンパイラを使用する)

また、今回は以下のような .proto 定義を使って紹介を進めます。

syntax = "proto3";
package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHi (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

こちらは、Dynamic codegen を使って mali.js で gRPC サーバーを実装した例です。

import { resolve } from "path";
import Mali from "mali";

const PROTO_PATH = resolve(__dirname, "./protos/helloworld.proto");

function sayHello(ctx: any): void {
  ctx.res = { message: `Hello ${ctx.req.name}` };
}

function sayHi(ctx: any): void {
  ctx.res = { message: `Hi ${ctx.req.name}` };
}

export function main(): void {
  const app = new Mali(PROTO_PATH, "Greeter");
  app.use({ sayHello, sayHi });
  app.start("localhost:50051");
}

main();

こちらは、Static codegen の例です。
まず、前提として以下の手順で、 ./static ディレクトリに service class のコード生成を行っておきます。

yarn add -D grpc-tools grpc_tools_node_protoc_ts

grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./static \
--grpc_out=./static \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
protos/helloworld.proto

grpc_tools_node_protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=./static \
protos/helloworld.proto

上記の手順で生成された、service 定義を読み込み全体のコードは以下のようになります。

import Mali from "mali";

import * as services from "./static/protos/helloworld_grpc_pb";
import * as messages from "./static/protos/helloworld_pb";

function sayHello(ctx: any): void {
  const reply = new messages.HelloReply();
  reply.setMessage(`Hello ${ctx.req.name}`);
  ctx.res = reply;
}

function sayHi(ctx: any): void {
  const reply = new messages.HelloReply();
  reply.setMessage(`Hi ${ctx.req.name}`);
  ctx.res = reply;
}

export function main(): void {
  const app = new Mali(services, "GreeterService");
  app.use({ sayHello, sayHi });
  app.start("localhost:50051");
}

main();

例えば、Node.js の grpc 標準実装である、 grpc を使って同様のサーバーを実装しようとした場合、最低限のコードは以下のようになります。こちらは、Dynamic codegen の例です。

import { resolve } from "path";
import grpc from "grpc";
import protoLoader from "@grpc/proto-loader";

const PROTO_PATH = resolve(__dirname, "./protos/helloworld.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function sayHello(call: any, callback: any): void {
  callback(null, { message: `Hello ${call.request.name}` });
}

function sayHi(call: any, callback: any): void {
  callback(null, { message: `Hi ${call.request.name}` });
}

function main(): void {
  const server = new grpc.Server();
  server.addService(hello_proto.Greeter.service, {
    sayHello: sayHello,
    sayHi: sayHi
  });
  server.bind("0.0.0.0:50051", grpc.ServerCredentials.createInsecure());
  server.start();
}

main();

mali.js の Dynamic codegen の例と比較すると多少冗長なのが見て取れるかと思います。
mali.js では、Mali のコンストラクタで .proto ファイルのパスを渡すだけで gRPC サーバーの基本的な設定を行うことができます。
また、server に対して service の method を追加する方法も、koa で middleware を登録するように app.use() の API を使って行うのも特徴的です。

参考