NestJSとC++間でgRPC（⑤ NestJSでgRPCサーバ/クライアントを作成）

# はじめに

### 最終目標
NestJSプログラムとC++プログラムにそれぞれgRPCモジュールを実装して相互通信させることを最終目標とし、目標達成に至るまでのプロセスを段階的に記します。
C++側は公式サンプルにあるCMakeによるコンパイルではなく、Makefileでコンパイルできるようにします。

### 今回の目標
最終回です。
NestJSでgRPCのクライアントプログラムおよびサーバプログラムを作成します。
また前回までで作成したC++サーバ/クライアントと通信テストを行います。

### 参考
こちらを参考にさせて頂きました。

https://zenn.dev/daimyo404/articles/710c61fd6aa877

https://zenn.dev/optimisuke/articles/b44f51311c1789

### 環境
NestJSプログラム、C++プログラム共に同一のAlmaLinux8.8上で動作するものとします。

# 環境準備
### protobufをインストール
```
# dnf install protobuf
```
### プロジェクト生成
下記を実行してgRPCサーバ/クライアントのプロジェクトを生成します。
(クライアント機能も実装しますがserverという名前にしておきます。名前はご自由に。)
```
$ nest new nest_server
$ cd nest_server
```
### 必要なパッケージのインストール
```
$ npm install ts-proto
$ npm install @grpc/grpc-js
$ npm install @nestjs/microservices
```
# Protocol Buffersの定義とコードの生成
### protoファイル
```protos```フォルダを作成し、②で作成したuser.protoをコピーしてきます。
```protos/user.proto
syntax = "proto3";

package user;

service UsersService {
  rpc GetUser (UserById) returns (User) {}
  rpc SetUser (User) returns (Result) {}
}

message UserById {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
}

message Result {
  string message = 1;
}
```
### Protocol Buffers のコードを生成
```nest_server```フォルダで下記コマンドを実行します。
```
protoc --ts_proto_opt=nestJs=true --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. ./protos/user.proto
```
実行すると```proto```フォルダに```user.ts```が生成されます。
# Userモジュール作成
下記コマンドを実行してサービス、コントローラ、モジュールを生成します。
```
$ nest g s user
$ nest g co user
$ nest g mo user
```
実行すると下記ファイルが生成されます。
```
./nest_server
  └ src
      └ user
          ├ user.controller.spec.ts
          ├ user.controller.ts
          ├ user.module.ts
          ├ user.service.spec.ts
          └ user.service.ts
```
### Userサービス
下記の通り実装します。
```sendGetUser()```と```sendSetUser()```はクライアント用、```getUser()```と```setUser()```はサーバ用です。

```user.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { Result, USERS_SERVICE_NAME, USER_PACKAGE_NAME, User, UserById, UsersServiceClient } from 'src/protos/user';

@Injectable()
export class UserService {
    private users: UsersServiceClient;
    private userList: User[] = [];

    constructor(@Inject(USER_PACKAGE_NAME) private client: ClientGrpc) {}
    onModuleInit() {
        this.users = this.client.getService<UsersServiceClient>(USERS_SERVICE_NAME);
    }

    sendGetUser(id: number): Observable<User> {
        const userById: UserById = {id: id};
        return this.users.getUser(userById);
    }

    sendSetUser(id: number, name: string): Observable<Result> {
        const user: User = {id: id, name: name};
        return this.users.setUser(user);
    }

    getUser(id: number): User {
        const users = this.userList.filter((user) => user.id === id);
        return users.length > 0 ? users[0] : ({} as User)
    }
    setUser(user: User) {
        this.userList.push(user);
    }
}
```
:::note warn
他のモジュールから```UserService```のクライアントを利用する場合、直接```sendGetUser()```、```sendSetUser()```を呼び出しても正しく送信されません。
戻り値(応答)が```Observale```なので```await lastValueFrom()```で戻り値を```Promise```に変換して最後の値を取るようにする必要があります。
:::

### Userコントローラ
下記の通り実装します。
```sendGetUser()```と```sendSetUser()```はクライアント用、```getUser()```と```setUser()```はサーバ用です。
クライアント処理はHTTPクライアントからのGETリクエストを契機に実行させるようにしています。

```user.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { Result, USERS_SERVICE_NAME, User, UserById } from 'src/protos/user';
import { UserService } from './user.service';
import { Observable } from 'rxjs';

@Controller('user')
export class UserController {
    constructor(private readonly userService: UserService){}
    @GrpcMethod(USERS_SERVICE_NAME, 'getUser')
    getUser(userById: UserById): User {
        console.log(`Received GetUser(id=${userById.id})`);
        const user = this.userService.getUser(userById.id);
        console.log(`Reply user=${JSON.stringify(user)})`);
        return user;
    }

    @GrpcMethod(USERS_SERVICE_NAME, 'setUser')
    setUser(user: User): Result {
        console.log(`Received SetUser(user=${JSON.stringify(user)})`);
        this.userService.setUser(user);
        const result : Result = {message: "Success"};
        console.log(`Reply result=${JSON.stringify(result)})`);
        return result;
    }
    
    @Get('/get')
    sendGetUser(@Query('id') id: number): Observable<User> {
        console.log(`Send GetUser(id=${id})`);
        return this.userService.sendGetUser(id);
    }

    @Get('/set')
    sendSetUser(@Query('id') id: number, @Query('name') name: string): Observable<Result> {
        console.log(`Send SetUser(id=${id}, name=${name})`);
        return this.userService.sendSetUser(id, name);
    }
}
```
### Userモジュール
クライアント側のgRPCの設定を記述します。
```user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { USER_PACKAGE_NAME } from 'src/protos/user';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';

@Module({
    imports: [
        ClientsModule.register([
            {
                name: USER_PACKAGE_NAME,
                transport: Transport.GRPC,
                options: {
                    url: 'localhost:5002',
                    package: USER_PACKAGE_NAME,
                    protoPath: join(__dirname, '../protos/user.proto'),
                },
            },
        ]),
    ],
    controllers: [UserController],
    providers: [UserService],
})
export class UserModule {}
```
### app.module.ts に Userモジュールをインポート
```app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```
### main.ts を編集
サーバ側のgRPCの設定を記述します。
```main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GrpcOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000, '0.0.0.0');

  app.connectMicroservice<GrpcOptions>({
    transport: Transport.GRPC,
    options: {
      url: 'localhost:5001',
      package: 'user',
      protoPath: join(__dirname, 'protos/user.proto'),
    },
  });
  await app.startAllMicroservices();
}
bootstrap();
```
### nest-cli.jsonを編集
デフォルトだとprotoファイルをdistの下にコピーしてくれないので```nest-cli.json```に下記を追加
```nest-cli.json
{
  "compilerOptions": {
    "assets": ["**/*.proto"]
  }
}
```
# 動作確認
### NestJSプログラムを起動
下記を実行して作成したNestJSプログラムを起動します。
```
$ npm run start:dev
```
### C++クライアント ⇒ NestJSサーバ
④で作成したC++クライアントとNestJSサーバの通信テストを行います。
下記を実行します。
```
$ cd ~/cpp_client
$ ./cpp_client --id=1 --name=User1
Result: Success
$ ./cpp_client --id=1
received: User1
```
### NestJSクライアント ⇒ C++サーバ
③で作成したC++サーバとNestJSクライアントの通信テストを行います。
下記を実行しC++サーバを起動します。
ポートは重複しないように```5002```とします。
```
$ cd ~/cpp_server
$ ./server --port=5002
Server listening on 0.0.0.0:5002
```
ブラウザのアドレスバーから```http://localhost:3000/set?id=2&name=User2```にアクセスします。
C++サーバ側のコンソールに下記が出力されれば成功です。
```
Request SetUser(id=2,name=User2)
```
次にブラウザのアドレスバーから```http://localhost:3000/get?id=2```にアクセスします。
C++サーバ側のコンソールに下記が出力されれば成功です。
```
Received GetUser(id=2)
Reply User (name=User2)
```
以上でNestJSプログラムとC++プログラム間でのgRPCの通信が実現できました。

# 関連記事

https://qiita.com/att-unno/items/cf9deb4b507cb5b6bd6d?1

https://qiita.com/att-unno/items/81f6760d3f447e7c3741?1

https://qiita.com/att-unno/items/55970c25da1d8be0503f?1

https://qiita.com/att-unno/items/17a239e661ff09badd99?1

https://qiita.com/att-unno/items/551c8554b5b521091611?1