6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

gRPC C++ SSL上でストリーミング通信をする

Posted at

#はじめに
linuxでのgRPCのインストール方法をまとめておきます。
今回はdockerコンテナでlinux環境を用意します。

今回コードの説明などは省きます。
参考文献を参照してください。

#インストール
Dockerfileで書きます。
cmakeやgrpcをインストールします。

Dockerfile
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ファイルも用意しておきます。

docker-compose.yml
version: '3.3'
services:
  dev:
    build: .
    volumes:
      - type: bind
        source: .
        target: /grpc_communication

コンテナに入る

bash
$ docker-compose build
$ docker-compose run --rm dev

Dockerfileのあるホストマシンのディレクトリをコンテナにマウントさせているので、ホスト上でファイルの編集を行えます。

gRPC関連の作業

.protoファイルの作成

chat.proto
syntax = "proto3";

package chat;

service Chat {
    rpc HogeChat(stream Note) returns (stream Note) {} 
}

message Note {
    string message = 1;
}

.protoファイルを用いていろいろ作成

bash
$ protoc -I ./ --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ./chat.proto
$ protoc -I ./ --cpp_out=. ./chat.proto

chat.grpc.pb.hchat.grpc.pb.ccchat.pb.hchat.pb.ccファイルができます。
今後使います。

serverの実装

chat_server.cpp
#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(&note)) {
                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の実装

chat_client.cpp
#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;
}

証明書、秘密鍵等の作成

ここはテストなのでかなり適当にしています。
ちゃんとする人はちゃんとしましょう。

bash
# 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を用意しました。

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

ではビルドします。

bash
$ 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

参考文献

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?