第1回は、サービス間の通信技術として古くから使われているRPCの解説から入り、gRPCの登場、特徴、サポートされるプラットフォームやプログラミング言語、データ交換フォーマットであるProtocol Buffersのあらましについて紹介します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
Googleによって開発され、オープンソース化された通信技術である「gRPC」は、マイクロサービスアーキテクチャにおけるサービス間の通信手段としてはもとより、モバイルアプリがサービスにアクセスするためのインタフェースとしても注目されています。現在ではLinux Foundation傘下のCloud Native Computing Foundation(CNCF)のもとで開発されています。
本連載では、gRPCについて、さまざまなプログラミング言語によるアプリケーションの実装を通じて、技術的な仕様や開発手法を紹介していきます。
gRPCは、RPCという通信プロトコルです。RPCとはRemote Procedure Callの略で、日本語にすると「遠隔手続き呼び出し」となります。RPCは、他の多くのプロトコルと同様にクライアント/サーバ型のモデルを採用しています。サーバに用意された、サービスのための手続き(関数)をクライアントがパラメーターとともに呼び出し、クライアントはその結果を受け取ります(図1)。
RPC自体は、決して新しい技術ではなく、考え方自体はインターネット(TCP/IP)の登場以前からありました。例えば、Sun Microsystems(現Oracle)の開発したRPCの実装であるSunRPCです。1980年代には情報共有のためのNIS(Network Information Service)やファイル共有のためのNFS(Network File System)が、C言語のライブラリとして実装されていました。開発者はあたかも自コンピュータ内における手続き呼び出しと同じ感覚で、外部のコンピュータのサービスを利用できました。
RPCの実装は上記のSunRPCをはじめとしてさまざまあり、時代とともに変化してきました。近年では、標準的なプロトコルであるHTTPやHTTPSを通信路に使い、XML形式やJSON形式のデータをやりとりするというXML-RPCやJSON-RPCがよく使われていました。標準的な技術を使用するため導入しやすいというメリットがありますが、反面、以下のようなデメリットが指摘されていました。
このような問題点を解消するために登場したのが、gRPCです。
gRPCのベースとなる技術はGoogleにより開発されました。「Stubby」と呼ばれたその技術は、2000年代初頭からGoogle内部で運用されてきましたが、独自の仕様が多く、長らく外部での利用は想定されていませんでした。しかし、2015年以降、仕様の一部はHTTP/2によって置き換えられるなどして、GoogleはStubbyをgRPCとしてオープンソース化し、誰でも利用できるようにしています。
gRPCでは、通信路にはHTTP/2が、データのやり取りにはProtocol BuffersというGoogleの開発した技術が用いられています。Protocol Buffersは、バイナリデータを効率的に扱うことができるのが特徴で、.protoファイルというテキストファイルを定義に用いることで、異なるプログラミング言語との連携を容易にしています。また、HTTP/2を採用することで、オーバーヘッドの少ない通信や、双方向の通信が可能となっています。
以下に、gRPCの主な特徴をまとめておきます。
gRPCでは、サーバ側にはgRPCサーバが、クライアント側にはgRPCスタブ(gRPCクライアント)が存在し、プログラミング言語からはそれぞれを呼び出します。gRPCサーバとgRPCスタブの間の通信はプログラミング言語やプラットフォームに依存しないので、異なる言語やプラットフォーム間での運用が可能になっています。
gRPCではHTTP/2やProtocol Buffersを標準で使用しますが、これらは置き換えることができます。本稿では、標準であるHTTP/2とProtocol Buffersを使っていきます。なお、gRPCの利用に必要な情報は以下の公式サイトから得られます。本稿では、その中から次回以降の理解に必要となる部分を抜粋して紹介していきます。
まず、サポートされる環境(プログラミング言語、プラットフォーム、コンパイラ/SDK)を押さえておきましょう。本稿の作成時点で公式にサポートされている環境は表1の通りです。
言語 | プラットフォーム | コンパイラ/SDK |
---|---|---|
C/C++ | Linux, macOS | GCC 6.3以降、Clang 6以降 |
C/C++ | Windows 10以降 | Visual Studio 2017以降 |
C# | Linux, macOS | .NET Core、Mono 4以降 |
C# | Windows 10以降 | .NET Core、.NET 4.5以降 |
Dart | Windows, Linux, macOS | Dart 2.12以降 |
Go | Windows, Linux, macOS | Go 1.13以降 |
Java | Windows, Linux, macOS | Java 8以降(KitKat以降 for Android) |
Kotlin | Windows, Linux, macOS | Kotlin 1.3以降 |
Node.js | Windows, Linux, macOS | Node v8以降 |
Objective-C | macOS 10.10以降、iOS 9.0以降 | Xcode 12以降 |
PHP | Linux, macOS | PHP 7.0以降 |
Python | Windows, Linux, macOS | Python 3.5以降 |
Ruby | Windows, Linux, macOS | Ruby 2.3以降 |
表1 gRPCでサポートされているプラットフォームとプログラミング言語 |
ご覧の通り、現時点で広く利用されているプログラミング言語がサポートされ、基本的なプラットフォームがサポートされています。特に利用における制約はないと思われます。
本稿ではgRPCの概要を述べますが、次回からは各種プログラミング言語を用いたアプリケーション開発の手順を紹介していきます。ここではまず、それに共通する開発の大まかな流れを紹介しておきます。具体的な作業は、各回にて紹介していきます。
アプリケーション開発の大まかな流れは以下の通りです。サーバとクライアントでほぼ共通となります(図3)。
まず、プラットフォームやプログラミング言語に依存したツールやライブラリをインストールする必要があります。多くの場合、プログラミング言語に用意されたパッケージマネジャー等を用いて簡単にインストールできます。ツールの名前はgrpc-tools、ライブラリの名前はgrpcとなっていることが多くなっています。
次に、サービスを定義します。サービスとは、(g)RPCにおける機能そのもので、サービスの名称、含まれる手続き、手続きへのパラメーター、手続きからの戻り値などを定義します。また、やりとりされるデータをメッセージと呼び、パラメーターや戻り値はメッセージとして定義します。いずれも、Protocol Buffersに基づいたフォーマットで定義します。
最後に、サービス定義に基づきプログラミング言語に応じたコード(クラスなどの定義が含まれる)をツールに含まれるprotocによって生成します。このコードとライブラリを使って、アプリケーションごとの機能を実装していきます。
既述の通り、gRPCにおけるデータのやりとりは、Protocol Buffersという技術に基づきます。Protocol Buffersをプログラミング言語から使うときに、利用できるサービス(手続き)の定義と、やりとりするデータであるメッセージの定義を、テキストファイルに記述していきます。このテキストファイルは、公式ドキュメントでは.protoファイルと記載されていますが、本稿ではProtocol Buffers定義ファイル(簡略化して定義ファイル)と呼ぶことにします。
gRPCを使ってサービスを定義し、何かデータをやりとりしようと考えたら、まずはこの定義ファイルの記述から始めます。といっても難しいところは何もなく、プログラミング言語における関数プロトタイプや構造体、クラスを定義するのと同じ要領で、サービス(手続き)の書式を記述し、データのまとまりやデータそのものに名前とデータ型を指定していけばよいのです。
Protocol Buffersは、gRPCサーバとクライアントの間でのやりとりを規定しますが、基本的にはバイト列でのやりとりです。プログラミング言語で使用するオブジェクトなどの複雑に構造化されたデータを、単一のバイト列や文字列に変換することをシリアライズ(直列化)と呼びます。逆に、バイト列や文字列から構造化されたデータを取得することはデシリアライズと呼びます。gRPCでは、シリアライズとデシリアライズはライブラリによって自動化されているので、開発者がこれを意識する必要はほとんどありません。
定義ファイルを記述する上での基本的なルールは、以下の通りです。
このほか、記述順序の推奨マナーがありますが、最初はそれほど意識する必要はないでしょう。必要な場合には、都度触れていくことにします。
定義ファイルは、拡張子を.protoとして作成することになっています。ここからは、書籍情報を扱うBookサービスのためのbooks.protoファイルを作成してみましょう。
定義ファイルはテキストファイルなので、どのようなテキストエディタを使って編集しても問題ありませんが、まだまだ一般的ではないのでサポートされるテキストエディタは限定されてきます。
ですので、筆者はVSCodeを推奨します。VSCodeでは、vscode-proto3などのProtocol Buffersをサポートする拡張機能が多数リリースされていますので、それらをインストールすることで、シンタックスハイライトやコード補完、スニペットなどの機能を利用できるようになります。
Protocol Buffersには、本稿の作成時点でバージョン2とバージョン3が存在します。デフォルトはバージョン2ですが、より単純な構文で多くの機能を持ち、多くのプログラミング言語をサポートするバージョン3の使用が推奨されています。
そこで、定義ファイルを記述するにあたり、バージョン3で記述されていることを明示しておきます。これには、ファイルの冒頭でsyntaxを定義します。バージョン3なので「proto3」を、syntaxに設定します(バージョン2なら「proto2」)。以下は、Protocol Buffersのバージョンを3に指定しています。
// バージョンを3に指定 syntax = "proto3";
なお、上記の通り定義ファイルにはコメントを記述できます。コメントは、ダブルスラッシュ(//)による1行コメントと、/* 〜 */によるC言語ライクなブロックコメントが使用できます。
Protocol Buffersバージョン3におけるガイドが以下にありますから、詳細が必要な場合には参照してください。
Language Guide (proto3) | Protocol Buffers | Google Developers
Protocol Buffersでやりとりされるデータを、メッセージと呼びます。メッセージは、データそのもののフォーマットであるほか、サービスを呼び出す際のパラメーターや戻り値のフォーマットともなります。
メッセージ(MessageName)は複数のフィールド(field_name)を持ち、それぞれのフィールドは型(type)と番号(number)を持ちます。以下のような形式になります。
message MessageName { type field_name = number; : }
中括弧({ })を使った、よくある形式ですから、戸惑うこともないでしょう。この書式を用いて、書籍情報そのものを表すBookというメッセージを定義すると以下のようになります。
message Book { (1) int32 id = 1; (2) string title = 2; string author = 3; string publisher = 4; string isbn = 5; uint32 price = 6; }
(1)では、Bookメッセージを定義するブロックを開始しています。(2)以降は、個々のフィールドの定義です。「int32」が型であり、「id」がフィールド名、「1」がフィールド番号です。
メッセージ名の制約は特にありませんが、キャメルケース(単語の先頭も大文字にするアッパーキャメルケースあるいはPascalケース)が推奨されています。これは、後述する列挙体でも同様です。
フィールドの型は、表2に示すものが指定できます。デフォルト値と、C++における型名の対応も例示しました。
型名 | 概要 | デフォルト | 例:C++での型名 |
---|---|---|---|
int32 | 符号あり32bit整数 | 0 | int32 |
int64 | 符号あり64bit整数 | 0 | int64 |
uint32 | 符号なし32bit整数 | 0 | uint32 |
uint64 | 符号なし64bit整数 | 0 | uint64 |
sint32 | 符号あり32bit整数 | 0 | int32 |
sint64 | 符号あり64bit整数 | 0 | int64 |
fixed32 | 符号なし32bit整数(固定バイト幅) | 0 | uint32 |
fixed64 | 符号なし64bit整数(固定バイト幅) | 0 | uint64 |
sfixed32 | 符号あり32bit整数(固定バイト幅) | 0 | int32 |
sfixed64 | 符号あり64bit整数(固定バイト幅) | 0 | int64 |
float | 単精度浮動小数点数(32bit) | 0.0 | float |
double | 倍精度浮動小数点数(64bit) | 0.0 | double |
bool | ブール型(true/false) | false | bool |
string | UTF-8もしくは7bitASCII文字列 | 空文字列 | string |
bytes | バイト列 | 空のバイト列 | string |
表2 フィールドの型 |
表2を見ると、整数型が細かく設けられています。「int32」「uint32」といった整数はいいとして、「sint32」「fixed32」などはこれらとどう違うのでしょうか? sint32は、int32よりも負数のエンコーディング効率に優れており、sfixed32はint32よりも大きな範囲の数をエンコーディングできる、という違いがあります。詳細は、下記から確認できます。
Encoding | Protocol Buffers | Google Developers
フィールド名の制約は特にありませんが、スネークケース(基本的に小文字で、単語をアンダースコア(_)で連結する)が推奨されています。これは、後述する列挙体の列挙子でも同様です。
フィールド番号は、1〜536,870,911(2の29乗-1)の任意の数で、1つのメッセージ中で一意(ユニーク)である必要があります。連続している必要はありませんし、小さい数から使う必要もありません。一意であれば問題ありません。
なお、型の前に「repeated」キーワードを付加すると、そのフィールドは値を複数持つ、すなわち配列のように振る舞うことを指定できます。以下の例では、authorフィールドが複数のstring型の値を持つことを指定しています。
message Book { …略… repeated string author = 3; …略… }
プロトコル定義ファイルには、列挙型(enum)も記述できます。列挙型を使うことで、単なる数値型を使うのではなく、意味のある値をフィールドに持たせることができます。
列挙型の定義は、以下のようになります。
enum EnumName { field_name = number; : }
形式はメッセージとほとんど同じですが、フィールドの型を指定する必要がないことに注意してください。以下は、Bookメッセージに列挙型BookTypeのtypeフィールドを設ける例です。
enum BookType { (1) BOOK = 0; (2) PAPERBACK = 1; POCKET = 2; MOOK = 3; OTHER = 99; } message Book { …略… BookType type = 7; (3) }
(1)では、BookType列挙型を定義するブロックを開始しています。(2)以降では、個々のフィールドを定義していますが、フィールド番号に0を指定しているものがあることに注意してください。フィールド番号の制約についてはメッセージと同様ですが、列挙型では0を指定できます。0をフィールド番号に持つ列挙子は、メッセージにおけるデフォルト値となります。(3)では、BookメッセージにてBookType列挙体のフィールドを定義しています。
ここでは、BookType列挙体に、BOOK(単行本)、PAPERBACK(新書)、POCKET(文庫)、MOOK(ムック)、OTHER(その他)を列挙子として持たせ、このうちBOOKをデフォルトとしています。
メッセージのフィールドには、定義済みのメッセージも指定することができます。つまり、メッセージの入れ子が可能です。レスポンスが、データ本体以外にステータスコードなどを含むといった場合、レスポンス全体をメッセージとして定義して、その中にデータそのものを含ませるといったことができます。
以下は、検索結果を表すSearchResponseメッセージに、書籍そのもののデータであるBookメッセージをステータスコードとともに持たせる例です。
message SearchResponse { (1) int32 status_code = 1; Book book = 2; (2) }
(1)は、Bookメッセージを含むSearchResponseメッセージの定義です。Bookメッセージのフィールドを、(2)でSearchResponseメッセージ中に定義しています。
なお、メッセージと列挙体は入れ子にもできます。ここまでの例だと、Bookメッセージの内部でBookType列挙体、BookSizeメッセージを以下のように記述できます。
message Book { …略… enum BookType { BOOK = 0; …略… } BookType type = 7; message BookSize { …略… } BookSize size = 8; }
サービスの定義は、サービスの名称とその手続き、引数と戻り値を指定します。
service BookService { (1) rpc Search(SearchRequest) returns (SearchResponse); (2) }
(1)は、BookServiceという書籍情報のためのサービスを定義しています。(2)は個々の手続きの定義で、「rpc」から始めます。「Search」は検索を実行する手続きの名前であり、引数にSearchRequestメッセージを渡し、結果としてSearchResponseメッセージを受け取ることを表しています。
最後に、パッケージを紹介します。パッケージとは、メッセージなどに名前空間を付与して、名称の衝突を回避するための仕組みです。
package AtmarkIt; (1) message Book { (2) …略… } …略… message SearchResponse { …略… AtmarkIt.Book book = 2; (3) }
(1)は、AtmarkItというパッケージを宣言しています。通常、パッケージは定義ファイルの冒頭で宣言し、以降に現れるメッセージ等の定義がパッケージに含まれることを示します。つまり、(2)のメッセージ定義はAtmarkItパッケージの配下に含まれることになります。そして、そのメッセージは(3)のようにドット(.)で区切ってパッケージ名も付記することで参照することができます。
パッケージの指定は、プログラミング言語ごとに生成されるコードに名前空間やモジュール、パッケージといった形で影響を与えることがありますが、これについては個別の回で触れることにします。
今回は、連載第1回として、gRPCとそれのベースとなるRPCの概要、そしてgRPCを使う上での基本となるProtocol Buffersについて紹介しました。
次回は、.NET環境とC#でgRPCアプリケーションを作成し、実践に踏み込んでいきます。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.