#はじめに
linuxでのgRPCのインストール方法をまとめておきます。
今回はdockerコンテナでlinux環境を用意します。
今回コードの説明などは省きます。
参考文献を参照してください。
#インストール
Dockerfile
で書きます。
cmakeやgrpcをインストールします。
FROM ubuntu:18.04
ENV CMAKE_INSTALL_DIR /local
ENV PATH $PATH:/grpc/cmake/build
ENV PATH $PATH:/grpc/cmake/build/third_party/protobuf
ENV PATH $PATH:$CMAKE_INSTALL_DIR/bin
RUN apt-get update && \
apt-get install -y \
git \
wget \
clang \
ca-certificates \
build-essential \
libssl-dev \
make \
autoconf \
automake \
pkg-config \
libtool \
golang \
curl && \
# Install cmake
wget -q -O cmake-linux.sh https://github.com/Kitware/CMake/releases/download/v3.17.0/cmake-3.17.0-Linux-x86_64.sh && \
mkdir -p $CMAKE_INSTALL_DIR && \
sh cmake-linux.sh -- --skip-license --prefix=$CMAKE_INSTALL_DIR && \
rm cmake-linux.sh && \
# Install grpc
cd / && git clone -b v1.28.1 https://github.com/grpc/grpc && \
cd /grpc && \
git submodule update --init && \
mkdir -p cmake/build && \
cd cmake/build && \
cmake ../.. && \
make && \
make install && \
ldconfig
WORKDIR /grpc-communication
docker-compose
ファイルも用意しておきます。
version: '3.3'
services:
dev:
build: .
volumes:
- type: bind
source: .
target: /grpc_communication
コンテナに入る
$ docker-compose build
$ docker-compose run --rm dev
Dockerfile
のあるホストマシンのディレクトリをコンテナにマウントさせているので、ホスト上でファイルの編集を行えます。
gRPC関連の作業
.proto
ファイルの作成
syntax = "proto3";
package chat;
service Chat {
rpc HogeChat(stream Note) returns (stream Note) {}
}
message Note {
string message = 1;
}
.proto
ファイルを用いていろいろ作成
$ protoc -I ./ --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ./chat.proto
$ protoc -I ./ --cpp_out=. ./chat.proto
chat.grpc.pb.h
、chat.grpc.pb.cc
、chat.pb.h
、chat.pb.cc
ファイルができます。
今後使います。
serverの実装
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <grpc/grpc.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>
#include <grpcpp/security/server_credentials.h>
#include "chat.grpc.pb.h"
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::ServerReaderWriter;
using grpc::Status;
using chat::Note;
using chat::Chat;
Note MakeNote(const std::string& message) {
Note n;
n.set_message(message);
return n;
}
void readFile(std::string filename, std::string &content) {
std::ifstream file(filename, std::ios::in);
while(file.is_open()) {
std::stringstream ss;
ss << file.rdbuf ();
file.close();
content = ss.str ();
}
}
// 証明書を読む
std::string readFile(std::string filename) {
std::string file_content;
std::string str_line;
std::ifstream file(filename, std::ios::in);
while(getline(file, str_line)) {
file_content += str_line;
}
file.close();
return file_content;
}
class ChatImpl final : public Chat::Service {
public:
Status HogeChat(ServerContext* context,
ServerReaderWriter<Note, Note>* stream) override {
Note note;
std::vector<Note> notes{
MakeNote("Hey, Client!"),
MakeNote("Hey, Apple!"),
MakeNote("Hey, Banana!"),
MakeNote("Hey, Melon!")};
while (stream->Read(¬e)) {
std::unique_lock<std::mutex> lock(mu_);
std::cout << "Got message " << note.message() << std::endl;
}
for (const Note& n : notes) {
std::cout << "Sending message " << n.message() << std::endl;
stream->Write(n);
}
return Status::OK;
}
private:
std::mutex mu_;
};
void RunServer() {
std::string server_address("0.0.0.0:50051");
ChatImpl service;
ServerBuilder builder;
// SSLの設定
std::string ca_crt_content;
std::string server_crt_content;
std::string server_key_content;
readFile("ca.crt", ca_crt_content);
readFile("server.crt", server_crt_content);
readFile("server.key", server_key_content);
grpc::SslServerCredentialsOptions sslOpts;
grpc::SslServerCredentialsOptions::PemKeyCertPair keycert = { server_key_content, server_crt_content };
grpc::SslServerCredentialsOptions sslOps;
sslOps.pem_root_certs = ca_crt_content;
sslOps.pem_key_cert_pairs.push_back(keycert);
builder.AddListeningPort(server_address, grpc::SslServerCredentials(sslOps));
builder.RegisterService(&service);
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait();
}
int main(int argc, char** argv) {
RunServer();
return 0;
}
clientの実装
#include <iostream>
#include <fstream>
#include <sstream>
#include <thread>
#include <grpc/grpc.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include "chat.grpc.pb.h"
using grpc::Channel;
using grpc::ClientContext;
using grpc::ClientReaderWriter;
using grpc::Status;
using chat::Note;
using chat::Chat;
Note MakeNote(const std::string& message) {
Note n;
n.set_message(message);
return n;
}
// 証明書を読む
void readFile(std::string filename, std::string &content) {
std::ifstream file(filename, std::ios::in);
while(file.is_open()) {
std::stringstream ss;
ss << file.rdbuf ();
file.close();
content = ss.str ();
}
}
class ChatClient {
public:
ChatClient(std::shared_ptr<Channel> channel): stub_(Chat::NewStub(channel)) {}
void HogeChat() {
ClientContext context;
std::shared_ptr<ClientReaderWriter<Note, Note> > stream(
stub_->HogeChat(&context));
std::thread writer([stream]() {
std::vector<Note> notes{
MakeNote("Hello, Server!"),
MakeNote("Hello, Apple!"),
MakeNote("Hello, Banana!"),
MakeNote("Hello, Melon!")};
for (const Note& note : notes) {
std::cout << "Sending message " << note.message() << std::endl;
stream->Write(note);
}
stream->WritesDone();
});
Note server_note;
while (stream->Read(&server_note)) {
std::cout << "Got message " << server_note.message() << std::endl;
}
writer.join();
Status status = stream->Finish();
if (!status.ok()) {
std::cout << "RouteChat rpc failed." << std::endl;
}
}
private:
std::unique_ptr<Chat::Stub> stub_;
};
int main(int argc, char** argv) {
// sslの設定
grpc::SslCredentialsOptions sslOpts;
std::string ca_crt_content;
std::string client_crt_content;
std::string client_key_content;
readFile("ca.crt", ca_crt_content);
readFile("client.crt", client_crt_content);
readFile("client.key", client_key_content);
sslOpts.pem_cert_chain = client_crt_content;
sslOpts.pem_private_key = client_key_content;
sslOpts.pem_root_certs = ca_crt_content;
auto channel_creds = grpc::SslCredentials(sslOpts);
ChatClient chat(grpc::CreateChannel("localhost:50051", channel_creds));
std::cout << "-------------- Chat --------------" << std::endl;
chat.HogeChat();
return 0;
}
証明書、秘密鍵等の作成
ここはテストなのでかなり適当にしています。
ちゃんとする人はちゃんとしましょう。
# Generate valid CA
$ openssl genrsa -passout pass:1234 -des3 -out ca.key 4096
$ openssl req -passin pass:1234 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Test/CN=Root CA"
# Generate valid Server Key/Cert
$ openssl genrsa -passout pass:1234 -des3 -out server.key 4096
$ openssl req -passin pass:1234 -new -key server.key -out server.csr -subj "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Server/CN=localhost"
$ openssl x509 -req -passin pass:1234 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
# Remove passphrase from the Server Key
$ openssl rsa -passin pass:1234 -in server.key -out server.key
# Generate valid Client Key/Cert
$ openssl genrsa -passout pass:1234 -des3 -out client.key 4096
$ openssl req -passin pass:1234 -new -key client.key -out client.csr -subj "/C=JP/ST=Tokyo/L=Tokyo/O=Test/OU=Client/CN=localhost"
$ openssl x509 -passin pass:1234 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
# Remove passphrase from Client Key
$ openssl rsa -passin pass:1234 -in client.key -out client.key
実行する
まず、ビルドします。
Makefile
を用意しました。
HOST_SYSTEM = $(shell uname | cut -f 1 -d_)
SYSTEM ?= $(HOST_SYSTEM)
CXX = g++
CPPFLAGS += `pkg-config --cflags protobuf grpc`
CXXFLAGS += -std=c++11
ifeq ($(SYSTEM),Darwin)
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
-pthread\
-lgrpc++_reflection\
-ldl
else
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
-pthread\
-Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed\
-ldl
endif
PROTOC = protoc
GRPC_CPP_PLUGIN = grpc_cpp_plugin
GRPC_CPP_PLUGIN_PATH ?= `which $(GRPC_CPP_PLUGIN)`
PROTOS_PATH = ./
vpath %.proto $(PROTOS_PATH)
all: chat_client chat_server
chat_client: chat.pb.o chat.grpc.pb.o chat_client.o
$(CXX) $^ $(LDFLAGS) -o $@
chat_server: chat.pb.o chat.grpc.pb.o chat_server.o
$(CXX) $^ $(LDFLAGS) -o $@
%.grpc.pb.cc: %.proto
$(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<
%.pb.cc: %.proto
$(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $<
clean:
rm -f *.o *.pb.cc *.pb.h chat_client chat_server
ではビルドします。
$ make
次に、実行してみましょう。
$ ./chat_server
$ ./chat_client
$ ./chat_server
Server listening on 0.0.0.0:50051
Got message Hello, Server!
Got message Hello, Apple!
Got message Hello, Banana!
Got message Hello, Melon!
Sending message Hey, Client!
Sending message Hey, Apple!
Sending message Hey, Banana!
Sending message Hey, Melon!
$ ./chat_client
-------------- Chat --------------
Sending message Hello, Server!
Sending message Hello, Apple!
Sending message Hello, Banana!
Sending message Hello, Melon!
Got message Hey, Client!
Got message Hey, Apple!
Got message Hey, Banana!
Got message Hey, Melon!
エラー対処
証明書を作成している段階でエラーが発生しました。
random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/root/.rnd
以下の方法で対処しました。
適切な対処方法なのかどうかは未調査です。
/etc/ssl/openssl.cnf
の以下の行を削除
RANDFILE = $ENV::HOME/.rnd
終わりに
クライアント、サーバそれぞれが複数のメッセージを受信、送信することができました。
ちなみに、tcpdumpを使ってパケットキャプチャを行うと実際に通信が暗号化されていることが確認できます。
以下のコマンドを実行した後で、./chat_server
、./chat_client
を実行します。
$ apt-get install tcpdump
$ tcpdump -i lo -X dst port 50051 or src port 50051
参考文献
- A basic tutorial introduction to gRPC in C++.
- gRPC Authentication
- gRPCのSSL上での通信関連のissue