5
2

More than 3 years have passed since last update.

gRPCでサーバー・クライアントを繋げてみる(iOS編)

Last updated at Posted at 2019-12-27

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

:warning: 注意点 :warning:

忘れてはいけないのが、ソースコードを生成した後に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... の順にクリックすると、以下のような画面が出てきます。
スクリーンショット 2019-12-27 21.59.39.png

この画面の 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 をクリックして依存関係の追加は完了です。

リクエストを送ってレスポンスを表示してみる

完成形

ViewController.swift
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コマンドで生成することは可能なのか?
  • 認証周りについて

上記三点について、今後調べつつ試していきたいと思っています。

5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2