はじめに
最終目標
NestJSプログラムとC++プログラムにそれぞれgRPCモジュールを実装して相互通信させることを最終目標とし、目標達成に至るまでのプロセスを段階的に記します。
C++側は公式サンプルにあるCMakeによるコンパイルではなく、Makefileでコンパイルできるようにします。
今回の目標
C++でgRPCのクライアントプログラムを作成します。
公式のサンプルにはCMakeでコンパイルする方法しかありませんが、ここではMakefileでコンパイルする方法を試します。
環境
NestJSプログラム、C++プログラム共に同一のAlmaLinux8.8上で動作するものとします。
ファイル構成
cpp_server
フォルダは前回作成したgrpc_api
と同階層に配置します。
各ファイルについては後ほど説明します。
./grpc_api
./cpp_client
├ main.cpp
├ client.cpp
├ client.h
├ libs.mk
└ Makefile
C++でgRPCクライアントプログラムを実装
下記の通りCppClient
クラスにGetUserName()
とSetUser()
を実装します。
#include <memory>
#include <string>
#include <grpcpp/grpcpp.h>
#include <user.grpc.pb.h>
using namespace grpc;
class CppClient {
public:
CppClient(std::shared_ptr<Channel> channel);
std::string GetUserName(int id);
void SetUser(int id, std::string name);
private:
std::unique_ptr<user::UsersService::Stub> stub_;
};
#include <iostream>
#include <memory>
#include <string>
#include <grpcpp/grpcpp.h>
#include <user.grpc.pb.h>
#include "client.h"
using namespace grpc;
using namespace user;
CppClient::CppClient(std::shared_ptr<Channel> channel)
: stub_(UsersService::NewStub(channel)) {}
std::string CppClient::GetUserName(int id) {
UserById request;
request.set_id(id);
User reply;
ClientContext context;
Status status = stub_->GetUser(&context, request, &reply);
if (status.ok()) {
return reply.name();
} else {
std::cout << status.error_code() << ": " << status.error_message()
<< std::endl;
return "RPC failed";
}
}
void CppClient::SetUser(int id, std::string name) {
User request;
request.set_id(id);
request.set_name(name);
Result reply;
ClientContext context;
Status status = stub_->SetUser(&context, request, &reply);
if (status.ok()) {
std::cout << "Result: " << reply.message() << std::endl;
} else {
std::cout << status.error_code() << ": " << status.error_message()
<< std::endl;
}
}
起動引数に--id
のみを指定するとGetUserName()
を実行し、--name
も指定するとSetUser()
を実行します。
#include <iostream>
#include <string>
#include <absl/flags/flag.h>
#include <absl/flags/parse.h>
#include <grpcpp/grpcpp.h>
#include "client.h"
ABSL_FLAG(std::string, host, "localhost:5001", "Server address");
ABSL_FLAG(int, id, 0, "User by ID");
ABSL_FLAG(std::string, name, "", "User Name");
int main(int argc, char** argv) {
absl::ParseCommandLine(argc, argv);
std::string host = absl::GetFlag(FLAGS_host);
int id = absl::GetFlag(FLAGS_id);
std::string name = absl::GetFlag(FLAGS_name);
CppClient client(
grpc::CreateChannel(host, grpc::InsecureChannelCredentials()));
if(name == "") {
std::string name = client.GetUserName(id);
std::cout << "received: " << name << std::endl;
}
else{
client.SetUser(id, name);
}
return 0;
}
Makefileの作成
インクルードパス
gRPCはホームディレクトリの.local
にインストールされているので、そのinclude
フォルダと、前回作成したProtocol Buffersのライブラリのヘッダファイルがあるgrpc_api/build
をインクルードパスに指定します。
INCLUDES := -I../.local/include -I../grpc_api/build
ライブラリパス&ライブラリリンク
ライブラリパスはProtocol Buffersのライブラリがあるgrpc_api/build
、gRPCのインストールフォルダにあるlib
とlib64
を指定します。
リンクするライブラリにはProtocol Buffersのuser_grpc_proto
と、gRPCのライブラリ群を指定します。
gRPCのライブラリ群はgRPCのサンプルプログラムをコンパイルした時に生成されるlink.txt
というファイルから抽出してlibs.mk
として別ファイルにしました。
LIBDIR += -L../grpc_api/build -L../.local/lib -L../.local/lib64
LIBS += -luser_grpc_proto -labsl_flags_parse -lgrpc++_reflection -lgrpc++ -lprotobuf -lgrpc -lupb_json_lib -lupb_textformat_lib -lupb_message_lib -lupb_base_lib -lupb_mem_lib -lutf8_range_lib -lre2 -lz -lcares -lgpr -labsl_random_distributions -labsl_random_seed_sequences -labsl_random_internal_pool_urbg -labsl_random_internal_randen -labsl_random_internal_randen_hwaes -labsl_random_internal_randen_hwaes_impl -labsl_random_internal_randen_slow -labsl_random_internal_platform -labsl_random_internal_seed_material -labsl_random_seed_gen_exception -lssl -lcrypto -laddress_sorting -ldl -lm -lrt -pthread -labsl_log_internal_check_op -labsl_leak_check -labsl_die_if_null -labsl_log_internal_conditions -labsl_log_internal_message -labsl_log_internal_nullguard -labsl_examine_stack -labsl_log_internal_format -labsl_log_internal_proto -labsl_log_internal_log_sink_set -labsl_log_sink -labsl_log_entry -labsl_log_initialize -labsl_log_globals -labsl_vlog_config_internal -labsl_log_internal_fnmatch -labsl_log_internal_globals -labsl_statusor -labsl_status -labsl_strerror -lutf8_validity -labsl_flags_usage -labsl_flags_usage_internal -labsl_flags_internal -labsl_flags_marshalling -labsl_flags_reflection -labsl_flags_config -labsl_cord -labsl_cordz_info -labsl_cord_internal -labsl_cordz_functions -labsl_cordz_handle -labsl_crc_cord_state -labsl_crc32c -labsl_str_format_internal -labsl_crc_internal -labsl_crc_cpu_detect -labsl_raw_hash_set -labsl_hash -labsl_bad_variant_access -labsl_city -labsl_low_level_hash -labsl_hashtablez_sampler -labsl_exponential_biased -labsl_flags_private_handle_accessor -labsl_flags_commandlineflag -labsl_bad_optional_access -labsl_flags_commandlineflag_internal -labsl_flags_program_name -labsl_synchronization -labsl_graphcycles_internal -labsl_kernel_timeout_internal -labsl_time -labsl_civil_time -labsl_time_zone -labsl_stacktrace -labsl_symbolize -labsl_strings -labsl_strings_internal -labsl_string_view -labsl_int128 -labsl_throw_delegate -labsl_malloc_internal -labsl_debugging_internal -labsl_demangle_internal -labsl_base -lpthread -labsl_raw_logging_internal -labsl_log_severity -labsl_spinlock_wait -lrt
Makefile
Makefile全体は下記の通りです。
上のlibs.mk
をインクルードしています。
PROGRAM = cpp_client
INCLUDES := -I../.local/include -I../grpc_api/build
LIBDIR :=
LIBS :=
-include libs.mk
RM := rm -rf
OBJS := main.o client.o
CC := g++
CFLAGS := -Wall -O2
.SUFFIXES: .cpp .o
.PHONY: all
all: depend $(PROGRAM)
$(PROGRAM): $(OBJS)
$(CC) -o $(PROGRAM) $^ $(LIBDIR) $(LIBS)
.cpp.o:
$(CC) $(INCLUDES) $(CFLAGS) -c $<
.PHONY: clean
clean:
$(RM) $(PROGRAM) $(OBJS) depend.inc
.PHONY: depend
depend: $(OBJS:.o=.cpp)
-@ $(RM) depend.inc
-@ for i in $^; do cpp -MM $$i | sed "s/\ [_a-zA-Z0-9][_a-zA-Z0-9]*\.cpp//g" >> depend.inc; done
-include depend.inc
コンパイル
make
を実行してcpp_client
が生成されれば成功です。
$ make
動作確認
前回作成したサーバプログラムと通信を行います。
サーバプログラムの起動
$ cd cpp_server
$ ./cpp_server
Server listening on 0.0.0.0:5001
クライアントプログラムの実行(GetUserName)
下記を実行してid=3
のname
を取得します。
$ ./cpp_client --id=3
received: User3
サーバプログラムの出力結果
Received GetUser(id=3)
Reply User (name=User3)
クライアントプログラムの実行(SetUser)
下記を実行してid=123
のname=User123
を登録します。
$ ./cpp_client --id=123 --name=User123
Result: Success
サーバプログラムの出力結果
Request SetUser(id=123,name=User123)
さらに下記を実行してid=123
のname
を取得して正しく登録されたことを確認します。
$ ./cpp_client --id=123
received: User123
サーバプログラムの出力結果
Received GetUser(id=123)
Reply User (name=User123)
これでC++側の実装は完了です。
次回はNestJSのサーバ/クライアントプログラムを実装します。
関連記事