Swiftでasync/awaitを使ってgRPCサーバをたてる
はじめに
macOS上でSwiftを用いてgRPCサーバを動作させ、かつSwift5.5で導入されたasync/awaitで記述します。
async/awaitを用いるにはiOS15やmacOS Montereyが必要ですが、iOSシミュレーターを用いればBig Surでもasync/awaitなSwiftコードを動かすことができます。
今回はmacOS上で動かすので、macOSをMontereyに上げる必要があります。どうしても上げたくない方はDcokerでLinuxイメージを使って動かすか、バックデプロイを待つなどする必要があります。
gRPCの実装にはgrpc-swiftを使います。
async/awaitのサポートは現時点ではExperimentalであるため、注意が必要です。今後インターフェイスなどに破壊的変更が入る可能性があります。
環境
- macOS Monterey 12.0.1
- Xcode 13.1
- Swift 5.5.1
protocとgrpc用のprotocプラグインを用意する
.protoファイルからコード生成を行うにはprotoc
コマンドが必要です。
またSwift用のProtocol Buffersモデルのコードと、gRPCのサーバ・クライアントコードを生成するためにそれぞれプラグインが必要になります。
今回protoc
はbrewでインストールしました
$ brew install protobuf
Swift用プラグインの2種は自分でビルドして用意します。
冒頭で述べたようにgrpc-swiftのasync/awaitサポートは現時点ではまだExperimentalであり、mainブランチではサポートされていません。
よって専用ブランチに切り替えてからビルドを行います。
$ git clone https://github.com/grpc/grpc-swift.git
$ cd grpc-swift
$ git switch 1.4.1-async-await
$ make plugins
これにより、 リポジトリルートに protoc-gen-swift
protoc-gen-grpc-swift
が生成されます。
(実態としてはSwiftPMでビルドされたファイルを.build/release
からコピーしてきているだけです。)
この2つの実行可能ファイルに対してパスを通します。
SwiftPMプロジェクトの作成
パッケージの初期化
$ mkdir GrpcEcho
$ cd GrpcEcho
$ swift package init --type executable
Package.swiftの記述
これまで同様、async/awaitサポートはExperimentalであるため専用ブランチを指定します。
またasync/awaitを用いるためにplatformsに.macOS(.v12)
の指定が必須です
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "GrpcEcho",
platforms: [.macOS(.v12)],
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", .branch("1.4.1-async-await")),
],
targets: [
.executableTarget(
name: "GrpcEcho",
dependencies: [.product(name: "GRPC", package: "grpc-swift")]
),
]
)
.protoファイルの作成
練習用サーバのための.proto
ファイルを記述します。
今回は適当にパッケージのルートディレクトリに置いておきます。
syntax = "proto3";
package echo;
service Echo {
rpc Hello(HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
.protoファイルのビルド
protoc
を用いてコード生成をします。
async/await対応のサーバ用コードを生成するためにはExperimentalAsyncServer
オプションを有効にする必要があります。
EventLoopをそのまま使う普通のサーバ用コードは不要なので、そちらは無効にしました。クライアント用コードも今回は実装しないので無効にしました。各自の目的にそって適宜設定します。
オプションについてはplugin.mdに記載されています。
$ mkdir -p Sources/GrpcEcho/Gen
$ protoc echo.proto --swift_out=Sources/GrpcEcho/Gen --grpc-swift_out=Sources/GrpcEcho/Gen --grpc-swift_opt=Server=false,Client=false,ExperimentalAsyncServer=true
生成物としてecho.grpc.swift
とecho.pb.swift
がSources/GrpcEcho/Gen
に出力されます。生成物は普通のSwiftコードです。
サーバ用コードの記述
各ServiceごとにProviderクラスを定義し、そのクラスをサーバに登録します。
今回はEcho
サービスとHello
メソッドを定義したので、そのインターフェイスを元に自動生成されたプロトコルであるEcho_EchoAsyncProvider
に対して具体的な実装を定義します。
import GRPC
import NIO
class EchoProvider: Echo_EchoAsyncProvider {
func hello(request: Echo_HelloRequest, context: GRPCAsyncServerCallContext) async throws -> Echo_HelloResponse {
return Echo_HelloResponse.with {
$0.message = "Hello, \(request.name)"
}
}
}
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
let server = try Server.insecure(group: group)
.withServiceProviders([
EchoProvider(),
])
.bind(host: "localhost", port: 8080)
.wait()
print("started server: \(server.channel.localAddress!)")
try server.onClose.wait()
実行すると、サーバが起動してリクエストを待機した状態になります。
動作確認
適当なgRPCクライアントで先程立ち上げたサーバに対してリクエストを送ってみます。
今回はgrpcurlを用いました。
$ brew install grpcurl
実際にリクエストを投げてみます。
$ grpcurl -plaintext -proto echo.proto -d '{"name": "Iceman"}' localhost:8080 echo.Echo.Hello
{
"message": "Hello, Iceman"
}
無事に動きました。
-plaintext
がない場合、 Unable to determine http version, closing
というエラーログが吐かれつつ無のレスポンスが返る( Failed to dial target host "localhost:8080": EOF
と言われる)ので注意してください。
デフォルト設定ではサーバログは虚無に捨てられるので、適宜設定する必要があります。
おわりに
SwiftでgRPCサーバを動かすことができました。またSwift最新機能であるasync/awaitも用いることができました(今回のサンプルコードではとくに恩恵がありませんが)。
注意点として、Swiftのstructured concurrencyに対応しているといっても実態はSwiftNIOベースのEventLoopで動いているという点があります。
現時点のEventLoopはactorのcustom executorには対応しておらず、これらに特に連携がありません。例えばawaitした箇所によってはEventLoopの外側で処理が実行されることがあります。EventLoopで保護されたリソースをasyncコンテキストの中で触れるときは注意が必要です。
またawaitを使ってFutureの完了を待つなどのタイミングでは本質的に不要なスレッドのスイッチが起こる可能性があります。基本的に問題にならないでしょうが、パフォーマンス的には不利となります。パフォーマンスにシビアなAPIにおいては用いないほうが賢明だと思います。
今回の作業コードはこちらです https://github.com/sidepelican/GrpcEcho
Discussion