この記事の対象者
iPhone, iPad アプリ開発者
gRPC とは
gRPC は、Google が開発した高性能な RPC(Remote Procedure Call)1です。gRPC は、異なるプログラミング言語やシステム間で効率的に、また高速に通信できるプロトコルとなっています。
こちらのサイトhttps://grpc.io2に詳細な情報があります。
次に特に重要な特徴をまとめます。
特徴名 | 内容 |
---|---|
プロトコルバッファ | プロトコルバッファ(Protocol Buffers)を利用してインタフェース定義とデータ構造を定義します。構造化されたデータをシリアライズ、デシリアライズでき、通信路上ではバイナリ形式でデータを表現するため通信速度とパフォーマンスが向上します。 |
HTTP/2 | HTTP/23 プロトコルをベースにしており、低レイテンシー、高スループット、バイナリフレーミング、ストリーム多重化などの特徴が利用できます。これにより、効率的で高速な通信を実現します。 |
4種類の通信方式 | 4つの通信方式を利用できます。Unary RPC / Server streaming RPC / Client streaming RPC / Bidirectional streaming RPC |
コードの自動生成 | gRPC ツールキットは、プロトコルバッファ定義からクライアントとサーバーのコードを自動生成します。これにより、開発者は簡単にクライアントとサーバーの実装を開始できます。Swift gRPC では、Swift 用のコードが生成されます。 |
マルチプラットフォーム | さまざまなプログラミング言語とプラットフォームでサポートされており4、異なるシステム間で容易に通信できます。Swift gRPC は、Swift 言語に特化した実装ですが、他の言語による gRPC と通信可能です。 |
プロトコルバッファ
gRPC は構造化データをシリアライズ/デシリアライズするために Google が開発したプロトコルバッファを使用します。
インタフェース定義とデータ構造は次のような記述で行います。
データ定義
QueryRequest は id をデータとしてもつ構造で、名前のとおりリクエストに利用する想定です。
Person は、レスポンスに使う想定のデータ構造です。
= で記述されている番号は、フィールドの連番と考えてください。
ファイル名は、xxxxx.proto
とします。
message QueryRequest {
int32 id = 1;
}
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
インタフェース定義
QueryRequest を送信すると、Person が返信されるサービス(インタフェース )の定義は次のようになります。
とても簡単に記述できることが分かります。
service PersonService {
rpc QueryPerson (QueryRequest) returns (Person) {}
}
コードの自動生成
後ほど詳しく説明しますが、次のようなコマンドを実行すると、Swift のコードを自動生成してくれます。
protoc $PROTO_DIR/*.proto \
--proto_path=. \
--plugin=$BREW_BIN/protoc-gen-swift \
--swift_out=. \
--plugin=$BREW_BIN/protoc-gen-grpc-swift \
--grpc-swift_out=.
4種類の通信方式
4つの異なるタイプの RPC がサポートされています。その中で、UnaryCall はもっとも一般的なタイプで、クライアントが 1つのリクエストメッセージを送信し、サーバーが 1つのレスポンスメッセージを返すという通信方式です。
項目 | 内容 |
---|---|
Unary RPC(UnaryCall) | リクエストに対してレスポンスを返す形式の一般的な通信方式 |
Server streaming RPC | クライアントから送られた1回のリクエストに対して、サーバーからのレスポンスが複数返ってくる通信方式。レスポンスメッセージはストリームとして送信され、クライアントはサーバーからのストリームを受信しながら処理を続行できます。 |
Client streaming RPC | クライアントから複数回リクエストを送信し、サーバーがそれに対してレスポンスを1回返す通信方式。リクエストメッセージはストリームとして送信され、サーバーはすべてのリクエストメッセージを受信して処理した後にレスポンスを返します。 |
Bidirectional streaming RPC | サーバー・クライアントともに任意のタイミングでリクエスト・レスポンスを送ることができる通信方式。読み取りと書き込みのストリームが独立しており、クライアントとサーバーは順序に関係なくメッセージを送信できます。 |
マルチプラットフォーム
下図のように C++ や Ruby、Java などの多くの言語でサポートされています。
図は https://grpc.io/docs/what-is-grpc/introduction/ より
ライブラリ・ツール
gRPC を、Swift プログラミング言語で実装したものが Swift gRPC
と呼ばれています。
https://grpc.io/docs/#official-support を見ると、Official support に Swift は含まれていませんが、
Swift Protobuf
は、Apple から提供されています。
また、gRPC Swift
は、gRPC のリポジトリーで提供されています。
gRPC Swift
は、Apple の Swift Protobuf
を利用をしています。
ライブラリ・ツール | URL |
---|---|
Swift Protobuf | https://github.com/apple/swift-protobuf |
gRPC Swift | https://github.com/grpc/grpc-swift |
使ってみる
準備
proto ファイルを Swift コードにコンパイルするコマンドツールを Homebrew でインストールしておきます。
コマンド自体は、前述のライブラリ・ツールに含まれていますが、設定などが面倒な当初はコマンドラインの方が使いやすいと思います。
brew install swift-protobuf grpc-swift
また次のような Swift コードに生成するためのスクリプトを準備しておくと便利でしょう。
#!/bin/sh
# Homebrewのbinへのパス
# Apple Siliconならそのままで大丈夫なはず
BREW_BIN=/opt/homebrew/bin
protoc $1/*.proto \
--proto_path=. \
--plugin=$BREW_BIN/protoc-gen-swift \
--swift_out=. \
--plugin=$BREW_BIN/protoc-gen-grpc-swift \
--grpc-swift_out=.
protoc で、Swift へコンパイルします。
--swift_opt=Visibility=Public
や
--grpc-swift_opt=Visibility=Public
というオプションを付け加えることで、生成されるコードを public
にできます。
protoファイルを用意する
person.proto
最初に、サポートするプロトコルバッファのバージョンを明確にするために syntax
を記入します。
パッケージ名は、Swift の場合は構造体や関数名のプレフィックスとなって名前空間での衝突を防ぎます。
message
キーワードが付いているものが、データ構造の定義になります。
次の proto ファイルの場合は、QueryRequest
のデータ構造と Person
のデータ構造が定義されています。
// バージョン
syntax = "proto3";
// パッケージ名の定義
package example.grpc;
// メッセージ型(データ構造)の定義
message QueryRequest {
int32 id = 1;
}
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
person_service.proto
こちらも基本的には同じ記述が必要ですが、import
によって別の proto ファイルのデータ構造を引用できます。
service
キーワードで、UnityCall の通信を利用することを宣言しています。
syntax = "proto3";
package example.grpc;
import "person.proto";
// サービス定義(gRPCで使用)
service PersonService {
rpc QueryPerson (QueryRequest) returns (Person) {}
}
コード生成結果
前述の proto ファイルから、3種類の Swift ファイルが生成されます。
プロトコルバッファのために、QueryRequest や Person のデータ定義用とサービス定義用の Swift ファイルが2種類。
またサービスの gRPC 用に1種類のファイルが生成されます。
コード量が多いのですが、一部を記載します。
package で指定した文字列が、プレフィックスとして利用されています。
person.pb.swift
Person の構造体が生成されていることが分かります。
struct Example_Grpc_Person {
var name: String = String()
var id: Int32 = 0
var hasPonycopter_p: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
// ...
person_service.pb.swift
サービスを定義した proto をコンパイルすると、QueryRequest の構造体などのgPRCを使うための構造体の他に、decodeMessage() などの関数も用意されます。
struct Example_Grpc_QueryRequest {
var id: Int32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
extension Example_Grpc_QueryRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// ...
}
}
// ...
person_service.grpc.swift
次に、通信を実際に行うサービス部分です。
こちらには、通信に必要な関数なども用意されます。
例えば、 Example_Grpc_PersonServiceAsyncClient
は通信チャネルである GRPCChannel を与えてインスタンス化したのち、proto で宣言した queryPerson
を実行したりできます。
queryPerson() を関数を見ると、performAsyncUnaryCall() が呼び出されており、これは UnaryCall を実行する非同期関数であることが分かります。
import GRPC
import NIO
import NIOConcurrencyHelpers
import SwiftProtobuf
// ...
internal protocol Example_Grpc_PersonServiceAsyncClientProtocol: GRPCClient {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: Example_Grpc_PersonServiceClientInterceptorFactoryProtocol? { get }
func makeQueryPersonCall(
_ request: Example_Grpc_QueryRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Example_Grpc_QueryRequest, Example_Grpc_Person>
}
extension Example_Grpc_PersonServiceAsyncClientProtocol {
// ...
}
internal struct Example_Grpc_PersonServiceAsyncClient: Example_Grpc_PersonServiceAsyncClientProtocol {
internal var channel: GRPCChannel
// ...
}
// ...
extension Example_Grpc_PersonServiceAsyncClientProtocol {
func queryPerson(
_ request: Example_Grpc_QueryRequest,
callOptions: CallOptions? = nil
) async throws -> Example_Grpc_Person {
return try await self.performAsyncUnaryCall(
path: Example_Grpc_PersonServiceClientMetadata.Methods.queryPerson.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeQueryPersonInterceptors() ?? []
)
}
}
// ...
クライアント/サーバをiPhoneに実装する
Swift gRPC
は、Swift-NIO を利用します。
Swift-NIO は、Apple が開発した Swift で実装された非同期ネットワークフレームワークです。詳しい内容は
https://github.com/apple/swift-nio
を参照してください。
ここでは、Swift-NIO
のMultiThreadedEventLoopGroup
を利用して非同期動作を実行します。
クライアントのコードでは、MultiThreadedEventLoopGroup
のスレッドは当然ながら1になっています。サーバ側のコードではこれを複数に設定できます。
クライアント
クライアントでは、GRPCChannelPool 関数を使って通信路を設定します。ここでは、
- 自分自身のポート 8080 に対して通信する
- 平文(TLS を使わない)を使う
としています。
Example_Grpc_PersonServiceAsyncClient
, Example_Grpc_QueryRequest
は protoc によって自動生成されたコードです。
コード内のコメントのように、通信を実施するクライアントのインスタンス化し、リクエストを発行する形になっています。
async/await がない場合は、Swift-NIO の非同期関数を使って、複雑なコードを書かなくてはいけなかったのですが、async/await のおかげで随分シンプルになりました。
import Foundation
import GRPC
import NIO
final class PersonServiceClient {
func fetch() async throws {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
// グループがシャットダウンされていることを確認する
defer {
try! group.syncShutdownGracefully()
}
// チャネルを設定します。この設定ではTLSを使用しません。
let channel = try GRPCChannelPool.with(
target: .host("localhost", port: 8_080),
transportSecurity: .plaintext,
eventLoopGroup: group
)
// 関数を抜ける時に接続を閉じる
defer {
try! channel.close().wait()
}
// クライアントを生成する
let client = Example_Grpc_PersonServiceAsyncClient(channel: channel)
// リクエストを生成する
let request = Example_Grpc_QueryRequest.with {
$0.id = 99
}
do {
let result = try await client.queryPerson(request)
print("received: \(result)")
} catch {
print("failed: \(error)")
}
}
}
サーバ
サーバ側の実装では、XXXProvider という protocol を使って、リクエストに対するレスポンスを生成するコードを実装します。
この処理をさまざまなリクエストに対して記述して、gRPC のサーバ機能に与えてやることで容易にサーバを構成できます。
PersonServiceServer
に使用されている Server
は、gRPC に組み込まれたサーバ機能です。
Server
に対して次のような設定を行います。
- 自分自身のポート 8080 に対して通信する
- 平文(TLS を使わない)を使う
- queryPerson リクエストに対する処理(XXXProviderの実装)を組み込む
Server
を使うとわずか数行で gRPC サーバを動作させることができます。
class PersonService: Example_Grpc_PersonServiceAsyncProvider {
var interceptors: Example_Grpc_PersonServiceServerInterceptorFactoryProtocol?
// queryPersonリスエストに対する処理を実装する
func queryPerson(request: Example_Grpc_QueryRequest, context: GRPCAsyncServerCallContext) async throws -> Example_Grpc_Person {
let response = Example_Grpc_Person.with { person in
// レスポンスの内容をセットする
person.name = "Steve"
person.id = 409
person.hasPonycopter_p = true
}
return response
}
}
class PersonServiceServer {
private var server: Server!
private var group: MultiThreadedEventLoopGroup!
func start() async throws {
// スレッドを1つで実行する
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
// Serverを設定して、実行する
let server = try await Server.insecure(group: group)
.withServiceProviders([PersonService()])
.bind(host: "localhost", port: 8_080)
.get()
print("server started on port \(server.channel.localAddress!.port!)")
// サーバの停止を待つ
try await server.onClose.get()
}
func stop() {
try? server.close().wait()
try? group.syncShutdownGracefully()
}
}
終わりに
gRPC を使って容易にマルチプラットフォームのシステムを構築できる(そうな)気がしてきませんか。しかも最新の通信技術を内包しているために非常に高速に動作するはずです。なによりコードを自動生成してくれるところが嬉しい仕組みです。
今回は、UnityCall のサンプルを試してみましたが、リアクティブな動きも可能にする Bidirectional streaming RPC
も試してみたいと思います。
何かのお役に立てれば幸いです。
-
RPC(Remote Procedure Call)は、コンピュータネットワーク上で実行されるプロセス間通信の一形態で、あるコンピュータ上で実行されているプログラムが、別のコンピュータ上で実行されているプログラムのサブルーチン(手続き、関数)を呼び出すことができる仕組みです。つまり、RPC はネットワークを介してリモートで手続きを呼び出すための通信プロトコルです。 ↩
-
gRPC は、Cloud Native Computing Foundation(CNCF)のプロジェクトの一部です。CNCF は、クラウドネイティブコンピューティングを促進し、クラウドネイティブシステムの成長と発展を支援する非営利団体です。 ↩
-
HTTP/2 は、Web 通信のための主要なプロトコルである HTTP(Hypertext Transfer Protocol)の第2版です。HTTP/2 は、HTTP/1.1 の後継として、Web のパフォーマンスと効率を向上させるために開発されました。HTTP/2 は、2015年に標準化され、現在多くのブラウザとサーバーに対応しています。 ↩
-
https://grpc.io/docs/#official-support で公式サポートされている言語をみることができます。 ↩