grpc-swiftの使い方ノート
基本的にはアプリにgRPCを組み込むことを考えるとクライアントコードがメインになる。
protoファイルからswiftコードの生成
-
%.pb.swift
- messageの型定義ファイル -
%.grpc.swift
- 関数などのインターフェイスの定義ファイル
プラグインはhomebrewでインストールできる
- protoc-gen-swift
- protoc-gen-grpc-swift
$ brew install swift-protobuf grpc-swift
PROTOC_GEN_SWIFT=/opt/homebrew/bin/protoc-gen-swift
PROTOC_GEN_GRPC_SWIFT=/opt/homebrew/bin/protoc-gen-grpc-swift
%.pb.swift: %.proto
protoc $< \
--proto_path=$(dir $<) \
--plugin=${PROTOC_GEN_SWIFT} \
--swift_opt=Visibility=Public \
--swift_out=$(dir $<)
%.grpc.swift: %.proto
protoc $< \
--proto_path=$(dir $<) \
--plugin=${PROTOC_GEN_GRPC_SWIFT} \
--grpc-swift_opt=Visibility=Public \
--grpc-swift_out=$(dir $<)
適当に切り出したmakefile
# protoc plugins.
PROTOC_GEN_SWIFT=/opt/homebrew/bin/protoc-gen-swift
PROTOC_GEN_GRPC_SWIFT=/opt/homebrew/bin/protoc-gen-grpc-swift
%.pb.swift: %.proto
protoc $< \
--proto_path=$(dir $<) \
--plugin=${PROTOC_GEN_SWIFT} \
--swift_opt=Visibility=Public\
--swift_out=$(dir $<)
%.grpc.swift: %.proto
protoc $< \
--proto_path=$(dir $<) \
--plugin=${PROTOC_GEN_GRPC_SWIFT} \
--grpc-swift_opt=Visibility=Public,Server=false,Client=true,TestClient=true \
--grpc-swift_out=$(dir $<)
SERVICE_PROTO=Model/service.proto
SERVICE_PB=$(SERVICE_PROTO:.proto=.pb.swift)
SERVICE_GRPC=$(SERVICE_PROTO:.proto=.grpc.swift)
.PHONY:
generate-service: ${SERVICE_PB} ${SERVICE_GRPC}
[grpc-swift]の面白いところ,AsyncSequenceがしっかりと活用できる
// Interface exported by the server.
service RouteGuide {
// A simple RPC.
rpc GetFeature(Point) returns (Feature) {}
// A server-to-client streaming RPC.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// A client-to-server streaming RPC.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// A Bidirectional streaming RPC.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
- Unary RPCは普通の関数呼び出しになる。
- server2client streaming RPCは返り値がAsyncSequenceになる。
- client2server streaming RPCは
requestStream
がCombineのSubjectのようにsend(_:)
とfinish()
を持つ。 - Bi-directional streaming RPCはs2c, c2sの両方を組み合わせた性質になる
呼び出し方法
unary streaming
let feature = try await self.routeGuide.getFeature(point)]
server2client streaming
AsyncSequenceが活用されている
for try await feature in self.routeGuide.listFeatures(rectangle) {
print("Result #\(resultCount): \(feature)")
resultCount += 1
}
client2server streaming
let recordRoute = self.routeGuide.makeRecordRouteCall()
for i in 1 ... featuresToVisit {
if let feature = features.randomElement() {
try await recordRoute.requestStream.send(feature.location)
try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9)))
}
}
try await recordRoute.requestStream.finish()
let summary = try await recordRoute.response
bi-directional streaming
Structured Concurrencyが活用されている
withThrowingTaskGroup(of:task:)
の処理で並行処理をしている。(of: Void.self)
にはgroupに追加したタスクが返却できる返り値の型を表現させる。group
はAsyncSequence
プロトコルに準拠しているのでfor await v in group {}
として処理ができる。それぞれの返り値を処理する必要がない(VoidならwaitForAll()
のように待つだけで良い)
try await withThrowingTaskGroup(of: Void.self) { group in
let routeChat = self.routeGuide.makeRouteChatCall()
// Add a task to send each message adding a small sleep between each.
group.addTask {
for note in notes {
try await routeChat.requestStream.send(note)
try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9)))
}
try await routeChat.requestStream.finish()
}
// Add a task to print each message received on the response stream.
group.addTask {
for try await note in routeChat.responseStream {
print("Received message '\(note.message)' at \(note.location)")
}
}
try await group.waitForAll()
}
全体の呼び出し元
GRPCChannelPool
で接続先を指定している
// Load the features.
let features = try loadFeatures()
let group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
defer {
try? group.syncShutdownGracefully()
}
let channel = try GRPCChannelPool.with(
target: .host("localhost", port: self.port),
transportSecurity: .plaintext,
eventLoopGroup: group
)
defer {
try? channel.close().wait()
}
let routeGuide = Routeguide_RouteGuideAsyncClient(channel: channel)
let example = RouteGuideExample(routeGuide: routeGuide, features: features)
await example.run()
色々メソッドがあり,getFeature
のように呼び出してrecordRoute
やrouteChat
のrequestStreamを扱えるような仕組みがある。サンプルコードはmakeXXXCall()
を利用している。Sequence,AsyncSequenceに準拠したものをRequestStreamに流すことができるコードが生成されている。
public func recordRoute<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) async throws -> Routeguide_RouteSummary where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Routeguide_Point
Connection周りはClientConnection
を使うといいっぽい。GRPCChannelPool
との違いはよくわからない
実際にアプリを作る上でのサンプル実装はこれ
// Make EventLoopGroup for the specific platform (NIOTSEventLoopGroup for iOS)
// see https://github.com/grpc/grpc-swift/blob/main/docs/apple-platforms.md for more details
let group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
// Setup a logger for debugging.
var logger = Logger(label: "gRPC", factory: StreamLogHandler.standardOutput(label:))
logger.logLevel = .debug
// Create a connection secured with TLS to Google's speech service running on our `EventLoopGroup`
let channel = ClientConnection
.usingPlatformAppropriateTLS(for: group)
.withBackgroundActivityLogger(logger)
.connect(host: "speech.googleapis.com", port: 443)
// Specify call options to be used for gRPC calls
let callOptions = CallOptions(customMetadata: [
"x-goog-api-key": Constants.apiKey,
], logger: logger)
// Now we have a client!
self.client = Google_Cloud_Speech_V1_SpeechClient(
channel: channel,
defaultCallOptions: callOptions
)
TestClientの追加のやり方 (参考にした記事記事)
ClientConnectionを利用するものとResponseをenqueueしたtestClientを使い分ければいいっぽい
container.register(Pb_HelloServiceClientProtocol.self) { _ in
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
#if MOCK
let testClient = Pb_HelloServiceTestClient()
testClient.enqueueHelloResponse(Pb_HelloResponse.with {
$0.hello = "mock message"
})
return testClient
#else
let channel = ClientConnection
.insecure(group: group)
.connect(host: "REQUEST_HOST_NAME", port: 5300)
return Pb_HelloServiceClient(
channel: channel
)
#endif
実際にアプリ上で動かしてみる
import SwiftUI
import GRPC
struct ContentView: View {
let client: Routeguide_RouteGuideAsyncClient
@State var feature: Routeguide_Feature?
@State var count: Int = 0
init() {
let group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
let channel = ClientConnection
// .usingPlatformAppropriateTLS(for: group)
.insecure(group: group)
.connect(host: "localhost", port: 1234)
self.client = Routeguide_RouteGuideAsyncClient(
channel: channel
)
}
var body: some View {
VStack {
Button {
Task {
do {
let rectangle: Routeguide_Rectangle = .with {
$0.lo = .with {
$0.latitude = numericCast(400_000_000)
$0.longitude = numericCast(-750_000_000)
}
$0.hi = .with {
$0.latitude = numericCast(420_000_000)
$0.longitude = numericCast(-730_000_000)
}
}
for try await feature in self.client.listFeatures(rectangle) {
self.feature = feature
self.count += 1
try await Task.sleep(nanoseconds: UInt64(1e8))
}
} catch {
dump(error)
}
}
} label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
if let feature {
Text("\(feature.location.longitude), \(feature.location.latitude), \(count)")
} else {
Text("No")
}
}
}
}
Discussion