gRPCの概要を理解し `proto` ファイルから `protoc` でコードを生成してみる

はじめに

ここ2ヶ月、時間を見つけてはGo言語を学んでいきました。
素人ながら簡単なロジックならなんとか組めるようになってきました。( 組み込みライブラリの使い方はまだまだですが。 )

そして、ようやくここまで来ました。
Go言語を学習し始めたのも、 gRPC を習得したかったからと言っても過言では有りません。

まずは概要を学んだので、その内容をエントリにまとめておきます。
また、 proto ファイルという定義ファイルを作成し、 protoc というコマンドを使って定義ファイルからクライアントコード、サーバコードを生成してみました。

gRPC って?

gRPCの特徴を列挙してみます。

  • Remote Procedure Call ( RPC ) のひとつ
  • 近年開発されたオープンソースの フレームワーク
  • 多くの環境で実行可能
  • 負荷分散、トレーシング、ヘルスチェックなどの機能にも対応可能な仕組み
  • 通信効率が良い

その他に以下のような特徴もあります。 ( 公式ページを参照したものなので、抽象度が高くあまりピンと来ません。 )

サービス定義が簡単

Protocol Buffers を使ってサービスを定義できます。
通信はシリアライズされたバイナリデータに変換されるため、転送効率が良いです。

すぐに始められて、かつスケーラビリティに優れている

ランタイムと開発環境を 1 行でインストールできます。
それにも関わらず、1 秒間に 100 万回の RPC リクエストを処理可能なスケーラビリティを持っています。

さまざまな言語と実行環境に対応

さまざまな言語と実行環境に対応しています。
適切な「クライアント」と「サーバ」のスタブコードを自動生成できます。

双方向ストリーミングと統合認証

「双方向ストリーミング」と「統合認証」の機能が組み込まれています。


gRPC は "Protocol Buffers" をどのように利用している?

もう少しだけ具体的な話に掘り下げてみたいと思います。

gRPC は、 Protocol Buffers という技術を利用してます。

Protocol Buffers は、 gRPC の中で大きく 2 つの目的で利用されています。

  1. 「インターフェイス定義言語」 ( Interface Definition Language , IDL )
  2. クライアントとサーバの メッセージの「フォーマット」

いきなり新しい単語が出てきて理解が追いついていかなくなってきました。

「Protocol Buffers ?なにそれ?」と思っているとすれば、今回のエントリ、少しは役に立つかもしれません。

gRPC は「リモート」にあるメソッドを提供し、呼び出す仕組み

gRPC を利用すると、クライアントアプリケーションはサーバアプリケーションのメソッドを直接呼び出すことができます。
( もちろんクライアントアプリはサーバアプリケーションと物理的に別である場合もありますし、同居している場合も考えられます。 )

いずれにしても、アプリケーションとサービスの分離を目指したフレームワークであるという背景があります。

RPC という名前を持つ仕組みから分かる通り、 gRPC は 「サービスを定義する」「パラメータを渡し、実行結果を返すリモート呼び出し可能なメソッドを仕様化する」という思想に基づいています。

"サーバサイド"では、クライアントからの呼び出しに対するインターフェイスを「実装」し、「稼働」させます。
"クライアントサイド"では、"サーバサイド"で提供される想定のメソッドのスタブコードが内包されます。スタブコードはクライアントアプリケーションの言語に合わせて、 IDL から生成できます。

+-C++ Service-+                 +-Ruby Client----------+
|+-----------+|                 |+---------+           |
||gRPC Server||---------------->||gRCP Stub|           |
||           ||<----------------|+---------+           |
||           ||                 +----------------------+
||           ||-----------+
|+-----------+|<---------+|     +-Android Java Client--+
+-------------+          ||     |+---------+           |
                         |+---->||gRCP Stub|           |
                         +------|+---------+           |
                                +----------------------+

gRPC はできるだけ実行環境に依存しない

gRPC の "クライアント" と "サービス" はいろんな 環境 で実行することが可能です。
例えば、 "クライアント" は 自分の作業用 PC で、 "サービス" は Google Cloud Platform ( GCP ) のサーバ、という構成でも正常に動作します。

環境 だけでなく、 いろんな プログラミング言語 で実装することができます。
それらの実装プログラミング言語が gRPC に対応しており、コードを「自動生成」できさえすればよいのです。
例えば、gRPC サーバを Java で実装し、 クライアントを Go、Python、Ruby など別の言語で実装しても OK です。

ちなみに、最新の Google API は Rest API インターフェイスだけではなく、 gRPC のインターフェイスも提供されています。gRPC クライアントを今まで以上にかんたんに作成することができるようになっています。( "自動生成されたクライアントコード" が提供されているということですね。 )

ともあれ Protocol Buffers に触れてみる

デフォルトの挙動として、gRPC は Protocol Buffers を利用します。

Protocol Buffers は十分に 枯れた Google のオープンソースの技術です。構造化されたデータをバイナリ形式にシリアライズして転送するために利用されます。( バイナリ形式にシリアライズする方法ではなく、JSON 形式で転送させることもできるようです。 )

簡単ではありますが、 Protocol Buffers に触れてみたいと思います。

メッセージ(データ構造)を定義する

Protocol Buffers を利用する最初のステップは、 proto ファイル ( 拡張子は .proto とします ) にシリアライズしたいデータ構造を定義することです。

proto ファイルに定義されたデータを メッセージ と呼びます。
メッセージは内部に「名前」と「値」のセットを複数持っており、これを フィールド と呼びます。

以下はサンプルコードになります。

syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

一行目は proto ファイルの構文バージョンです。一旦は「おまじない」だと思ってスキップします。

このファイルを使って、定義ファイルから自分の利用したい言語用のデータ操作用クラスを生成します。
ソースコードの生成には Protocol Buffers のコンパイルツールである protoc コマンドを利用します。

クラスには name()set_name() といった、メッセージのフィールドにアクセスするためのシンプルな関数が含まれます。 ( 生成されるメソッド名は、選択したプログラム言語によって変わります。 )
バイナリデータにシリアライズ、デシリアライズするためのメソッドも含まれています。

クライアントやサーバの実装言語として "C++" を選択した場合、先程の proto 定義コードから Person というクラスを持つ .cpp コードが生成されます。

このコードを使って Person メッセージに対してデータを入力したり、データを出力したり、転送のためにシリアライズしたりします。

サービスを定義する

メッセージ の定義が終わりましたが、これだけでは意味がありません。
「手紙」は書いたが誰にも送られず、誰も受け取れないのと同じです。

そこで、メッセージの送信、受信を担当するものとして サービス を定義します。
サービスは メソッド です。 パラメータを受け取り、戻り値を返します
パラメータや戻り値の「型」として先程定義したメッセージを指定します。

サンプルコードを見ていきましょう。

syntax = "proto3";

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

何度か触れましたが、gRPC では protoc というツールを使って proto ファイルから ソースコードを生成します。
クライアントサイド、サーバサイドのそれぞれのソースコードが生成可能です。

protoc を使って、先程のサンプルコード( main.proto として保存してあります。)から PHP のソースコードを生成してみます。

# 出力先ディレクトリを作成
$ mkdir -p dist/

# protoc実行
$ protoc --php_out=./dist main.proto

ソースコードを確認してみます。

$ ls -la dist/
total 8
drwxr-xr-x 5 root root  160 May 19 00:23 .
drwxr-xr-x 5 root root  160 May 19 00:23 ..
drwxr-xr-x 3 root root   96 May 19 00:23 GPBMetadata
-rw-r--r-- 1 root root 1282 May 19 00:23 HelloReply.php
-rw-r--r-- 1 root root 1261 May 19 00:23 HelloRequest.php
$ cat dist/HelloRequest.php
<?php
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: main.proto

use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;

/**
 * The request message containing the user's name.
 *
 * Generated from protobuf message <code>HelloRequest</code>
 */
class HelloRequest extends \Google\Protobuf\Internal\Message
{
    /**
     * Generated from protobuf field <code>string name = 1;</code>
     */
    protected $name = '';

    /**
     * Constructor.
     *
     * @param array $data {
     *     Optional. Data for populating the Message object.
     *
     *     @type string $name
     * }
     */
    public function __construct($data = NULL) {
        \GPBMetadata\Main::initOnce();
        parent::__construct($data);
    }

    /**
     * Generated from protobuf field <code>string name = 1;</code>
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Generated from protobuf field <code>string name = 1;</code>
     * @param string $var
     * @return $this
     */
    public function setName($var)
    {
        GPBUtil::checkString($var, True);
        $this->name = $var;

        return $this;
    }

}

"Protocol Buffers" のバージョン

バージョンについてさらっと流してしまいましたが、 "Protocol Buffers" の構文にはバージョンがあります。

2021-05-19 現在、 version2 ( proto2 ) と version3 ( proto3 ) が利用できます。

サンプル proto コードの一行目にある syntax = "proto3"; を記述しなければ proto2 として扱われるということです。

gRPC 公式ページでは、 proto3 の利用が推奨されています。 proto2proto3 の違いは以下のとおりです。

  • 構文がシンプルになった
  • いくつかの新機能追加
  • コード生成対象のプログラム言語追加

ひとこと

少しだけ gRPCProtocol Buffers について知ることができました。

Java の EJB 、 Oracle の ESBSOAPCobra など過去にあった分散技術が頭に浮かんできます。

Posted by genzouw