ひつじTips

技術系いろいろつまみ食います。

grpc(C++)をDockerで使う最小構成

f:id:mu-777:20211128044923p:plain
grpc x c++ x docker

grpcの環境をdocker内に作り,そのdockerの中でgrpcを使うC++プログラムをビルドし実行できるようにする方法についてまとめます.

「grpc c++ docker」で普通にググるとトップに出てくる以下の記事でもいいのですが,微妙に違う感じになったので書き残します.

zenn.dev

上記記事(2021年11月末時点)に対し本記事は:

  • Dockerfileの記述がちょっと少ない
  • サンプルのコードの記述がシンプル
    • 上記記事ではgrpcの双方向方式(Bidirectional streaming RPC)を使ってる(すごい!)が,こちらはそこまでやってない
  • クライアントもc++で書く
  • protoファイルのコンパイルをcmakeでやる

と,全体的に単純化してる感じですね.

本記事で挙げるコード類はこのレポジトリにありますのでご参考までに

github.com

前置き

上述のレポジトリ(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

AddRequeststr1str2AddReplyにセットする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_servergrpc_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の環境で実行し,動作することを確認しました!