grpcの環境をdocker内に作り,そのdockerの中でgrpcを使うC++プログラムをビルドし実行できるようにする方法についてまとめます.
「grpc c++ docker」で普通にググるとトップに出てくる以下の記事でもいいのですが,微妙に違う感じになったので書き残します.
上記記事(2021年11月末時点)に対し本記事は:
- Dockerfileの記述がちょっと少ない
- サンプルのコードの記述がシンプル
- 上記記事ではgrpcの双方向方式(Bidirectional streaming RPC)を使ってる(すごい!)が,こちらはそこまでやってない
- クライアントもc++で書く
- protoファイルのコンパイルをcmakeでやる
と,全体的に単純化してる感じですね.
本記事で挙げるコード類はこのレポジトリにありますのでご参考までに
前置き
上述のレポジトリ(mu-777/grpc-test)の"blog_20211128"ブランチを前提に話を進めます.
このレポジトリのフォルダ構成は以下のようになってます.ただし,本記事に不要なものはなくしてます.
<proj_path>/grpc-test ├── CMakeLists.txt ├── cmake │ └── GRPCHelper.cmake ├── docker │ └── Dockerfile.grpc ├── grpc_client.cpp ├── grpc_server.cpp └── protos └── simple.proto
(ソースをルートに直置きしてごめんなさい......サボりました......)
環境の準備
最初に言った通り,ビルド/実行環境はDockerに構築します.
短いDockerfileなので全部載せてしまいましょう.
FROM ubuntu:18.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install -y \ ssh \ build-essential \ autoconf \ libtool \ pkg-config \ libssl-dev \ git \ && apt-get clean RUN wget -q -O cmake-linux.sh https://github.com/Kitware/CMake/releases/download/v3.19.6/cmake-3.19.6-Linux-x86_64.sh \ && sh cmake-linux.sh -- --skip-license \ && rm cmake-linux.sh RUN git clone https://github.com/grpc/grpc -b v1.41.0 --recursive \ && cd grpc \ && mkdir build \ && cd build \ && cmake .. -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \ && make -j8 \ && make install
From: grpc-test/Dockerfile.grpc at blog_20211128 · mu-777/grpc-test · GitHub
キモとしては,cmakeの新しいバージョンのものを取ってきてビルドして使ってるところです. ubuntu:18.04のaptで入れたcmakeだとgrpcはビルドできるのですが,grpcをリンクするプログラムのcmakeのconfigureがコケるので最新版を自前ビルドする必要があります.
もちろんベースイメージによっては普通にパッケージマネージャで最新のcmakeをインストールできるかもしれませんが,ちょっと調査できておりません.
参考: grpc.io
このDockerfileを普通にビルドしておきます.
docker build -t mu-777/grpc-cpp -f ./Dockerfile.grpc .
protoの用意
今回は最小構成ということで,超シンプルなprotoを用意しました.(ファイル名適当すぎてごめんなさい)
syntax = "proto3"; package simple; service Add { rpc Add (AddRequest) returns (AddReply) {} } message AddRequest { string str1 = 1; string str2 = 2; } message AddReply { string str = 1; }
From: grpc-test/simple.proto at blog_20211128 · mu-777/grpc-test · GitHub
Add
関数は,AddRequest
で文字を2つ送ったら,AddReply
で1つにつなげて返してくれる関数にしようと思います.
サーバの実装
こちらもだいぶ短いので全文載せます.アドレス直書きですみません......
#include <iostream> #include <grpcpp/ext/proto_server_reflection_plugin.h> #include <grpcpp/grpcpp.h> #include "simple.pb.h" #include "simple.grpc.pb.h" namespace simple { class SimpleAddImpl final : public Add::Service { ::grpc::Status Add(::grpc::ServerContext *context, const AddRequest *req, AddReply *res) override { std::cout << "[Server] ReceivedReq: " << req->str1() << ", " << req->str2() << std::endl; res->set_str(req->str1() + req->str2()); return ::grpc::Status::OK; } }; }; void RunServer() { std::string server_address("localhost:50051"); simple::SimpleAddImpl service_simple; grpc::EnableDefaultHealthCheckService(true); grpc::reflection::InitProtoReflectionServerBuilderPlugin(); grpc::ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service_simple); std::unique_ptr<grpc::Server> server(builder.BuildAndStart()); std::cout << "Server listening on " << server_address << std::endl; server->Wait(); } int main(int argc, char **argv) { std::cout << "Hello, World! server" << std::endl; RunServer(); return 0; }
From: grpc-test/grpc_server.cpp at blog_20211128 · mu-777/grpc-test · GitHub
AddRequest
のstr1
とstr2
をAddReply
にセットするAdd
関数を実装したSimpleAddImpl
を作って,grpcサーバに設定し,サーバを実行しています.
このあたりはもうサンプルからコピペって,いろいろ自分のしたいことをやってく感じだと思うので,下記の参考や他のサンプルを見つつ,自分のしたいことの実装を探しつつやっていただければ......
参考: github.com
クライアントの実装
こちらも載せますよ.
#include <iostream> #include <grpcpp/security/credentials.h> #include <grpcpp/create_channel.h> #include "simple.pb.h" #include "simple.grpc.pb.h" int main(int argc, char **argv) { std::cout << "Hello, World! I'm simple client" << std::endl; if (argc < 2){ std::cout << "Must input 2 args" << std::endl; return 0; } auto str1 = argv[1]; auto str2 = argv[2]; auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()); auto stub = simple::Add::NewStub(channel); simple::AddRequest req; req.set_str1(str1); req.set_str2(str2); grpc::ClientContext ctx; simple::AddReply res; auto status = stub->Add(&ctx, req, &res); if (!status.ok()) { std::cout << status.error_code() << ": " << status.error_message() << std::endl; std::cout << "RPC failed" << std::endl; return 0; } std::cout << "[Client] ReceivedRes: " << res.str() << std::endl; return 0; }
From: grpc-test/grpc_client.cpp at blog_20211128 · mu-777/grpc-test · GitHub
実行時に引数を2つ取って,それをAddRequest
に入れた上でAdd
関数を呼び出す感じになってます.
ビルドまわり
今回はCMakeでやります.
上述のgrpc_server
とgrpc_client
をビルドするためのCMakeLists.txt
は以下のような感じです.
cmake_minimum_required(VERSION 3.7) project(grpc_test) set(CMAKE_CXX_STANDARD 14) include(${CMAKE_SOURCE_DIR}/cmake/GRPCHelper.cmake) find_package(Protobuf CONFIG REQUIRED) find_package(gRPC CONFIG REQUIRED) include_directories(${Protobuf_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) # 自作の関数 (From: https://github.com/mu-777/grpc-test/blob/blog_20211128/cmake/GRPCHelper.cmake) pb_grpc_generate_cpp(PROTO_SRCS PROTO_HDRS GRPC_SRCS GRPC_HDRS ${CMAKE_SOURCE_DIR}/protos/simple.proto) add_library(grpc_proto ${PROTO_SRCS} ${PROTO_HDRS} ${GRPC_SRCS} ${GRPC_HDRS}) target_link_libraries(grpc_proto gRPC::grpc++ gRPC::grpc++_reflection protobuf::libprotobuf) foreach (_target grpc_server grpc_client) add_executable(${_target} "${_target}.cpp") target_link_libraries(${_target} grpc_proto) install(TARGETS ${_target} RUNTIME DESTINATION bin) endforeach ()
From: grpc-test/CMakeLists.txt at blog_20211128 · mu-777/grpc-test · GitHub
find_package(Protobuf CONFIG REQUIRED)
とfind_package(gRPC CONFIG REQUIRED)
は,この順でないと失敗するのでお気をつけください......(うーん......cmakeってそういうもの??自分の環境がおかしいのかな......)
pb_grpc_generate_cpp
は自作の関数で,この引数で指定したprotoファイル(複数指定OK)をコンパイルし,<proto名>.pb.h/cc
と<proto名>.grpc.pb.h/cc
を作り,パスを返してくれます.(grpc-test/cmake/GRPCHelper.cmake
に定義があります)
それらをまとめて実行ファイル(grpc_server, grpc_client)へリンクさせ,ビルドします.
参考: github.com
ビルド & 実行
準備はできたので,実際にビルドして実行しましょう~~
最初に作ったDockerイメージをコンテナにして中に入ります.このとき,プロジェクトをマウントしておきます.
$ docker run -it --rm -v <proj_path>/grpc-test:<proj_path>/grpc-test --name grpc_cpp mu-777/grpc-cpp bash
Docker内でビルドしていきますよ.
# cd <proj_path>/grpc-test # mkdir build && cd build && cmake .. && make -j8 -- The C compiler identification is GNU 7.5.0 -- The CXX compiler identification is GNU 7.5.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /usr/bin/cc - skipped ~~ (snip) ~~ -- Configuring done -- Generating done -- Build files have been written to: <proj_path>/grpc-test/build [ 12%] Generating simple.pb.cc, simple.pb.h, simple.grpc.pb.cc, simple.grpc.pb.h Scanning dependencies of target grpc_proto [ 37%] Building CXX object CMakeFiles/grpc_proto.dir/simple.pb.cc.o [ 37%] Building CXX object CMakeFiles/grpc_proto.dir/simple.grpc.pb.cc.o [ 50%] Linking CXX static library libgrpc_proto.a [ 50%] Built target grpc_proto Scanning dependencies of target grpc_server Scanning dependencies of target grpc_client [ 75%] Building CXX object CMakeFiles/grpc_client.dir/grpc_client.cpp.o [ 75%] Building CXX object CMakeFiles/grpc_server.dir/grpc_server.cpp.o [ 87%] Linking CXX executable grpc_client [100%] Linking CXX executable grpc_server [100%] Built target grpc_client [100%] Built target grpc_server
という感じでビルドできるかと思います.
実行には,新しく2つターミナルを開きまして,
$ docker exec -it grpc_cpp <proj_path>/grpc-test/build/grpc_server Hello, World! server Server listening on localhost:50051
としてサーバを立ち上げたあと,
もう1つのターミナルで,クライアント実行すると以下のようにレスポンスがあるかとお思います.
$ docker exec -it grpc_cpp <proj_path>/grpc-test/build/grpc_client mu 777 Hello, World! I'm simple client [Client] ReceivedRes: mu777
ちゃんと2つの文字列("mu"と"777")がつながって返ってきました🎉🎉
サーバ側もリクエストがあったタイミングで以下のような標準出力があるかと思います!
$ docker exec -it grpc_cpp <proj_path>/grpc-test/build/grpc_server Hello, World! server Server listening on localhost:50051 [Server] ReceivedReq: mu, 777
ということで,dockerにgrpc環境を作り,そこでビルドしたプログラムをそのdockerの環境で実行し,動作することを確認しました!