Node.jsとTypeScriptでgRPCを動かす


Node.js上で、gRPCのサーバとクライアントを動かしてみます。

また、gRPCのコード生成の際に、TypeScriptの型情報も出力し、静的な型付けも可能にします。

コード全体はこちらにあります。

TypeScriptのインストール

まずは、TypeScriptをインストールします。

yarn add -D typescript ts-node

ts-node は実行するのに楽なので入れました。普通に tsc でコンパイルしてから node で実行しても良いです。

proto ファイルを書く

proto ファイルに gRPC サービスの定義を書きます。今回は BookService というものを用意しました。

proto/book.proto
syntax = "proto3";

service BookService {
  rpc GetBook(GetBookRequest) returns (GetBookResponse);
}

message GetBookRequest {
  string id = 1;
}

message GetBookResponse {
  Book book = 1;
}

message Book {
  string title = 1;
  string author = 2;
}

proto ファイルからコード生成を行う

proto ファイルから、コード生成を行います。
生成するのは、JavaScriptファイル(.js)とTypeScriptの型定義ファイル(.d.ts)です。

ちなみに、公式のチュートリアルにあるように、Node.jsでgRPCを実装する場合には、実行時にコード生成をおこなう方法(dynamic codegen)と、
事前にprotocで事前にコード生成を行う方法(static codegen)の二種類があります。
今回は、後者の方法をとります。

Node.jsのコード生成をおこなうには、grpc-toolsというnpmパッケージを使用します。
これには、 protoc と その gRPC Node プラグインが同梱されています。

TypeScriptの型定義を生成するには、また別の protoc プラグインである、grpc_tools_node_protoc_ts をインストールします。

yarn add -D grpc-tools grpc_tools_node_protoc_ts

インストールをおこなったら、次のようなシェルスクリプトを用意します。

protoc.sh
#!/usr/bin/env bash

set -eu

export PATH="$PATH:$(yarn bin)"

PROTO_SRC=./proto
PROTO_DEST=./src/proto

mkdir -p ${PROTO_DEST}

grpc_tools_node_protoc \
  --js_out=import_style=commonjs,binary:${PROTO_DEST} \
  --grpc_out=${PROTO_DEST} \
  --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \
  -I ${PROTO_SRC} \
  ${PROTO_SRC}/*

grpc_tools_node_protoc \
  --plugin=protoc-gen-ts=$(npm bin)/protoc-gen-ts \
  --ts_out=${PROTO_DEST} \
  -I ${PROTO_SRC} \
  ${PROTO_SRC}/*

このスクリプトを実行すると、4つのファイルが生成されます。

src/proto/book_pb.d.ts
src/proto/book_grpc_pb.js
src/proto/book_grpc_pb.d.ts
src/proto/book_pb.js

これでコード生成は完了です。

gRPCサーバの実装

まずは、 grpc パッケージと google-protobuf パッケージをインストールします。

yarn add grpc google-protobuf

サーバのコードは次のようになります。

src/server.ts
import * as grpc from 'grpc';
import * as book_grpc_pb from './proto/book_grpc_pb';
import * as book_pb from './proto/book_pb';

import { bookData } from './books'

class BookService implements book_grpc_pb.IBookServiceServer {
  getBook(
    call: grpc.ServerUnaryCall<book_pb.GetBookRequest>,
    callback: grpc.sendUnaryData<book_pb.GetBookResponse>,
  ) {
    const bookId = call.request.getId();

    const response = new book_pb.GetBookResponse();
    const book = new book_pb.Book();
    book.setTitle(bookData[bookId].title);
    book.setAuthor(bookData[bookId].author);
    response.setBook(book);

    callback(null, response);
  }
}

(() => {
  const server = new grpc.Server();
  server.bind(
    `0.0.0.0:50051`,
    grpc.ServerCredentials.createInsecure(),
  );
  server.addService(
    book_grpc_pb.BookServiceService,
    new BookService(),
  );

  server.start();
})();

先の proto ファイルで定義した BookService を実装したクラスを、TypeScriptで書きます。
BookService には GetBook というメソッドが定義されていました。
なので、インターフェース IBookServiceServer を実装するには、 getBook というメソッドを実装すればよいことになります。

class BookService implements book_grpc_pb.IBookServiceServer {
  getBook(
    call: grpc.ServerUnaryCall<book_pb.GetBookRequest>,
    callback: grpc.sendUnaryData<book_pb.GetBookResponse>,
  ) {
    ...
  }
}

そして、サービスの実装を、gRPCサーバに追加し、起動します。

server.addService(
  book_grpc_pb.BookServiceService,
  new BookService(),
);
server.start();

クライアントの実装

クライアントも実装してみます。

src/client.ts
import * as grpc from 'grpc';
import * as book_grpc_pb from './proto/book_grpc_pb';
import * as book_pb from './proto/book_pb';

const client = new book_grpc_pb.BookServiceClient(
  '127.0.0.1:50051',
  grpc.credentials.createInsecure(),
);

const req = new book_pb.GetBookRequest();
req.setId('book1')

client.getBook(req, function(error, result) {
  if (error) console.log('Error: ', error);
  else console.log(result.toObject());
});

シンプルですね。

実行してみる

サーバを起動します。

yarn ts-node src/server.ts

クライアントからリクエストを送ります。

yarn ts-node src/client.ts

レスポンスが得られれば成功です。

{ book: { title: 'Book1', author: 'Author1' } }