Node.jsとTypeScriptでgRPCを動かす

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

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

コード全体は[こちら](https://github.com/daisaru11/node-typescript-grpc-example)にあります。

### TypeScriptのインストール

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

```
yarn add -D typescript ts-node
```

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

### proto ファイルを書く

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

```proto: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)です。

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

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

TypeScriptの型定義を生成するには、また別の protoc プラグインである、[grpc_tools_node_protoc_ts](https://www.npmjs.com/package/grpc_tools_node_protoc_ts) をインストールします。

```
yarn add -D grpc-tools grpc_tools_node_protoc_ts
```

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

```bash: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](https://www.npmjs.com/package/grpc) パッケージと [google-protobuf](https://www.npmjs.com/package/google-protobuf) パッケージをインストールします。

```
yarn add grpc google-protobuf
```

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

```typescript: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` というメソッドを実装すればよいことになります。

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

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

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

### クライアントの実装

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

```typescript: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' } }
```