はじめに
最終目標
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をコピーしてきます。
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()
はサーバ用です。
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リクエストを契機に実行させるようにしています。
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の設定を記述します。
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モジュールをインポート
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の設定を記述します。
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
に下記を追加
{
"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の通信が実現できました。
関連記事