Aizu Advent Calendar の22日目の記事です。
大遅刻しました。申し訳ないです。。
gRPCでサーバー・クライアントを繋げてみたもののiOS編です。
gRPCとは
こちらの記事でわかりやすく簡単に紹介されています。
gRPC自体は .proto
ファイルを使用し、それを元に生成したコードを使用してクライアントから簡単にサーバー側のデータを取得することができるものです。
上記の点から、特定のエンドポイントに対して必要な値を載せてリクエストを飛ばして○○というデータが返ってくるというAPI定義書を作成することなく使用する側はサーバーからデータを取得することができます。
また、HTTP/2を使用、データ取得関数使用時にシリアライズを行うため、高速に通信することができます。
実装編
proto
ファイルを定義する
クライアントとサーバーにとっての共通のインターフェースである proto
ファイルを定義します。
syntax = 'proto3';
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
この proto
ファイルの内容自体は gRPCのガイドにあるものにいくつか追加して書き入れているものがあります。
以下の2つです。
syntax = 'proto3'
使用するプロトコルバッファーのバージョンを指定しています。指定していない場合、proto
ファイルからソースコードを生成する際にエラーが吐かれます。
package
この追加している部分はクライアント側ではあまり意味がありません。サーバー側での話になるので割愛します。
また、この proto
ファイルの内容自体はすごく簡単なものとなっていて、message HelloRequest
で指定した値を使用してリクエストを送ると message HelloReply
で指定した値が返ってくるようになっているというものです。
この流れを service Greeter
で定義しています。
今回この proto
ファイルはクライアント側で定義して持つのではなく、サーバー側で持ち、サーバー側のリポジトリをサブモジュールとして登録して proto
ファイルを参照するようにしてみています。
ソースコードを生成する
今回コマンドラインで proto
ファイルからソースコードを生成するようにしているのですが、少しばかり長いコマンドとなっています。
それを毎回簡単に叩くために今回は Makefile を作成しました。
また、 proto
ファイルから生成する際に protoc-gen-grpc-swift
というコマンドと protoc-gen-swift
というコマンドが必要なので、今回は先ほどの proto
ファイルと同じく grpc-swiftリポジトリを サブモジュールとして登録し、そのディレクトリへパスを Makefile 内で通してコマンドを使用できるようにしました。
export PATH += :$(PWD)/grpc-swift
hello:
protoc hello.proto\
--proto_path=gPRC-server-java/server/src/main/proto/ \
--grpc-swift_out=./gRPC-client\
--swift_out=./gRPC-client
注意点
忘れてはいけないのが、ソースコードを生成した後にXcode内で生成したファイルを参照することです。
参照しなくてはビルド時にコンパイルの対象とならないため、エラーが発生してしまいます。
やり方
追加対象のGroupを右クリック > Add Files to "追加Group名"... > 生成されたファイル2つを選択 > Add
protoc
コマンドについて
protoc
コマンド自体はこういった形式で使用します。
protoc protoファイル名 ...各種オプション
protoc
コマンドをインストールする方法については、MacOS環境であれば
brew install protobuf
とすればコマンドが入ってくれます。
オプション類について
protoc
コマンドについて であった "各種オプション"について説明します。
--proto_path
指定した protoファイル名 のprotoファイルが存在するディレクトリを指定します。
今回の例の場合、以下のようなファイルツリーのため、gPRC-server-java/server/src/main/proto/
を指定します。
gPRC-server-java
├── db
└── server
├── build
├── gradle
│ └── wrapper
└── src
├── main
│ ├── java
│ ├── proto
│ │ ├── hello.proto
│ └── resources
└── test
--grpc-swift_out
protoファイル名.grpc.swift
ファイルの出力先を指定するオプションです。
このファイルがないと Service
が使用できず、サーバー側からデータが取得できません。
今回は ViewController.swift
などと同じ階層に出力されるようにしています。
このオプションがない場合、単純にモデル定義をしているだけのため、コンパイルエラーとなったりはしません。
--swift_out
protoファイル名.pb.swift
ファイルの出力先を指定するオプションです。
このファイルがないと proto
ファイル内で定義した message
が使用できません。
また、 --grpc-swift_out
のみ指定していた場合に protoファイル名.grpc.swift
ファイル内でコンパイルエラーとなります。
gRPCのswiftライブラリをプロジェクトの依存関係に追加する
今回は Swift Package Manager を使用してライブラリをプロジェクトに追加します。
Xcode11 以上が前提の内容となっていますので、10.x以下の方はアップデートするか、Cocoapodsを使用するなどしてください。
トップバーにある File
> Swift Packages
> Add Package Dependency...
の順にクリックすると、以下のような画面が出てきます。
この画面の Enter package repository URL
と書いてある場所にgRPCのSwiftライブラリである grpc-swift
のリポジトリのURL (https://github.com/grpc/grpc-swift) を入力します。
すると、リポジトリのURLがラベルとして埋め込まれ、ルールを選ぶことのできる画面に進むと思います。
ほとんどの場合はそのまま Next
を選択しても大丈夫なのですが、今回は公式のREADMEで master
ブランチは非推奨になっていると書いてあり、 nio
ブランチを使用することが推奨されていたので nio
ブランチを指定します。
Branch
を選択し、入力欄に nio
と入力して Next
をクリックします。
最後にそのまま Finish
をクリックして依存関係の追加は完了です。
リクエストを送ってレスポンスを表示してみる
完成形
import UIKit
import GRPC
import NIO
class ViewController: UIViewController {
let client: Greeter_GreeterServiceClient = {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let configuration = ClientConnection.Configuration(
target: .hostAndPort("localhost", 6565),
eventLoopGroup: group)
let connection = ClientConnection(configuration: configuration)
return Greeter_GreeterServiceClient(connection: connection)
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let request = Greeter_HelloRequest.with {
$0.name = ""
}
do {
let response = try client.sayHello(request).response.wait()
print(response)
} catch let e {
print(e.localizedDescription)
}
}
}
これだけだと味気ないので実装の手順を説明を交えながらしていきます。
1. clientを作成する
let client: Greeter_GreeterServiceClient = {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let configuration = ClientConnection.Configuration(
target: .hostAndPort("localhost", 6565),
eventLoopGroup: group)
let connection = ClientConnection(configuration: configuration)
return Greeter_GreeterServiceClient(connection: connection)
}()
この部分です。
リクエストを送るための関数が定義されているクラス(※1)を初期化するために connection
を引数として渡す必要があるため、定義します。
今回、connectionを生成するにあたっての値として、
- host が
localhost
, portが6565(gRPCサーバーのデフォルトポート) - スレッド数が1の
EventLoopGroup
(※2)
を指定します。
※1 ファイル内で定義したpackage名・service名が使用されたクラスは package名_service名Client
という形式の名前で生成されています。package が指定されていなかった場合 service名_Client
という名前になっています。
※2 EventLoopGroup
については、 swift-nio というライブラリで定義されている protocol で、イベントストリームについて扱うものとなっています。こちらについてはあまり深く調べられていないので割愛します。
2. リクエストの message
を作成する
let request = Greeter_HelloRequest.with {
$0.name = ""
}
リクエストの message
にはファクトリメソッドが存在していて、一回イニシャライズして値を入れていくということをする必要がなく、便利なのでそれを使用します。
今回用意するサーバーでは name
の値に関わらず Hello
を返すようにしているので空文字を入れています。
3. リクエストを送り、レスポンスを標準出力に表示する
今回のアプリでは、アプリ自体の見た目には表示しません。(手抜きです。すみません。)
アプリ自体の見た目に表示すること自体は調べたらいくつも出てきたりなどしますので、そちらを参照してください。
do {
let response = try client.sayHello(request).response.wait()
print(response)
} catch let e {
print(e.localizedDescription)
}
先ほど作成した client
を使用し、リクエストを送信します。
client
には proto
ファイル内の service
で定義した rpc
の関数(以後リクエストの関数)が定義されていて、それを使用するとリクエストを送ることができます。
今回の例だと、sayHello(_ HelloReply)
が定義されています。
リクエストの関数の返り値の中に response
というプロパティがあり、このプロパティ自体はまた先程も出てきたのですが、swift-nio というライブラリで定義されている EventLoopFuture
という型です。
このプロパティをそのまま使用しようとしても値が取り出せないので、取り出すために wait
関数を使用します。また、この関数は throws
で定義されているため、do ~ catch
で囲み、try
をつけて実行します。
最後に、wait
関数の返り値をそのまま出力して、完了です。
2019-12-27T22:31:33+0900 info: gRPC connection to [IPv6]::1/::1:6565 on SelectableEventLoop { selector = Selector { descriptor = 4 }, thread = NIOThread(name = NIO-ELT-0-#0) } ready
gRPC_client.Greeter_HelloReply:
message: "Hello"
感想など
実装自体とても簡単で、共通のインターフェースを提供して型安全に通信ができるのはすごくいいなと感じました。
予定ではもっと前に投稿しようかと思っていたのですが、gRPCとは関係のないところでサーバー側でうまくいかない箇所があり、このタイミングでの投稿となりました。
サーバー側については書いていませんが、記事に書いていることをそのままやってみると HelloWorld はできるようにしたつもりです。試してみていただけるとありがたいです。
今回は試せてないですが、
- Date型など複数のフォーマットのあるものはどのように扱うのか?
- クライアントのコードだけをprotocコマンドで生成することは可能なのか?
- 認証周りについて
上記三点について、今後調べつつ試していきたいと思っています。