0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-07-03

はじめに

最終目標

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

今回の目標

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

参考

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

環境

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);
    }
}

他のモジュールから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の通信が実現できました。

関連記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?