LoginSignup
2
2

More than 3 years have passed since last update.

SwiftでGRPCを動かす方法と基本的な挙動について

Posted at

はじめに

GRPCを扱おう事になって色々調べてもあまり文献がない。
そもそもGRPCの通信ができない、ヘッダーの設定方法や鍵認証通信方法など・・・とにかく色々ハマったのでこれから触る人のために一通りの事を書いておく。

GRPCとは

Googleが提供するPRC。
リモートプロシージャコール。
早い話がネットワーク越しのコンピューターへのアクセスをまるで自分のコンピュータのメソッドを叩いているように通信を行うもの。
APIという言葉に置き換えても話は十分通じます。

選定

grpc-swift

初めはgrpc-swiftで構築しようとした。
構築も楽そうだし、初心者にも敷居が低いと思ったのだが・・・。
やめた。

その理由は鍵を使った認証が出来なかったからだ。(検証日:2019年3月頃)
コードは正しくて、証明書も正しいのにエラーが発生するという状況が1日続いたのでやめた。
ここでエラーが発生する時点で、他でもエラーが発生するかもしれないと思って断念。

その時には気がつかなかったけれど、今振り返ってみるとProtoファイルの管理も自前で行う必要がありそうだと思ったので断念。

Google本家

本家のGrpcを敬遠したのは二つの理由がある。

  1. Swift版がない
  2. 構築が面倒臭そう

grpc-swiftで構築する場合はあっという間だった。
鍵認証がなくてライトなプロジェクトなら間違いなくgrpc-swiftをオススメする。
これは難しそうだ・・・。

と感じる敷居の高さがあった。
ただ、構築が終われば管理は大変ではありません。

僕にとっての敷居が高かった理由はPodspecを作ったことがない為でした。

構築方法

構築するためには自前でPodを作る必要があった。
そのために使用するのがpodspec

これをプロジェクトに配備します。
公式ページにあるものを配備します。
基本的にはルートに配備した方が楽ですが、別に階層の中に配備しても大丈夫です。

ここではファイル名をtest.podspecにしていますが、何でも良いです。
ただし、ファイル名は後でプロジェクト内のimport文に記載するのでプロジェクト名+GRPC.podspecなどにしておくとわかりやすいと思います。

以下に使う時に困りそうな箇所をコメントで記載しておきます。
記載がない場所は変更の必要がない箇所 or 僕が把握してない箇所になります。

test.podspec
Pod::Spec.new do |s|
  s.name     = '<Podspec file name>'
  s.version  = '0.0.1' # 後述しますが、protoファイルを変更するたびにこのヴァージョンを上げてください。
  s.license  = '...' # Apache License, Version 2.0とか社内のルールに従ってください。通常は社外に公開しないと思うので何でも良いです。
  s.authors  = { '<your name>' => '<your email>' } # 何でも良いです。 ex) 'gRPC' => 'hoge@gmail.com'
  s.homepage = '...' # 何でも良いです。 ex) https://hoge.com
  s.summary = '...' # 何でも良いです。 ex) 社名
  s.source = { :git => 'https://github.com/...' } # 社内のリポジトリを使わない場合はGoogleのGithubを指定します。 ex) https://github.com/grpc/grpc.git

  s.ios.deployment_target = '7.1'  # 自分のアプリバージョンに合わせてください。
  s.osx.deployment_target = '10.9'

  # Base directory where the .proto files are.
  src = '.' # 後述するprotoファイルの置き場所です。ルート直下におくことはないはずです。 ex) proto

  # We'll use protoc with the gRPC plugin.
  s.dependency '!ProtoCompiler-gRPCPlugin', '~> 1.0'

  # Pods directory corresponding to this app's Podfile, relative to the location of this podspec.
  pods_root = '<path to your Podfile>/Pods' # Podsファイルの場所。通常は`pods_root = 'Pods'`になるはず。

  # Path where Cocoapods downloads protoc and the gRPC plugin.
  protoc_dir = "#{pods_root}/!ProtoCompiler"
  protoc = "#{protoc_dir}/protoc"
  plugin = "#{pods_root}/!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin"

  # Directory where you want the generated files to be placed. This is an example.
  dir = "#{pods_root}/#{s.name}"

  # Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients.
  # You can run this command manually if you later change your protos and need to regenerate.
  # Alternatively, you can advance the version of this podspec and run `pod update`.
  # 階層化したい場合は以下のコードを変更する必要があります。(後述
  s.prepare_command = <<-CMD
    mkdir -p #{dir}
    #{protoc} \
        --plugin=protoc-gen-grpc=#{plugin} \
        --objc_out=#{dir} \
        --grpc_out=#{dir} \
        -I #{src} \
        -I #{protoc_dir} \
        #{src}/*.proto
  CMD

  # The --objc_out plugin generates a pair of .pbobjc.h/.pbobjc.m files for each .proto file.
  s.subspec 'Messages' do |ms|
    ms.source_files = "#{dir}/*.pbobjc.{h,m}"
    ms.header_mappings_dir = dir
    ms.requires_arc = false
    # The generated files depend on the protobuf runtime.
    ms.dependency 'Protobuf'
  end

  # The --objcgrpc_out plugin generates a pair of .pbrpc.h/.pbrpc.m files for each .proto file with
  # a service defined.
  s.subspec 'Services' do |ss|
    ss.source_files = "#{dir}/*.pbrpc.{h,m}"
    ss.header_mappings_dir = dir
    ss.requires_arc = true
    # The generated files depend on the gRPC runtime, and on the files generated by `--objc_out`.
    ss.dependency 'gRPC-ProtoRPC'
    ss.dependency "#{s.name}/Messages"
  end

  s.pod_target_xcconfig = {
    # This is needed by all pods that depend on Protobuf:
    'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1',
    # This is needed by all pods that depend on gRPC-RxLibrary:
    'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES',
  }
end

これをプロジェクトに配置してPodfileに以下を記載すれば準備は完了です。

# podspecのファイル名とパスを記載する事。
pod 'test', :path => '.'

あとは以下のコマンドを叩けば完成ですが、今はまだ叩かないでおきましょう。

$> pod install

GRPCの仕組み

GRPCはどの環境からでもリモートの資産(サーバー)にアクセスできるようにすることが狙いです。
そのためにprotoという設計図のようなものを用意してあります。

例えば、クライアントがRubyの場合はprotoファイルからRubyのコードを自動的に作成してくれます。
今回はSwift(Objective-c)なのでprotoファイルからSwift(Objective-c)のコードを作ってもらう必要があります。

その仕組みが上記で作ったpodfileになります。

上記の例ではprotoフォルダに*.protoファイルを配置する事を想定していますので、お手元にある*.protoファイルをprotoフォルダに全てコピーしてください。

そして以下を実行しましょう。

$> pod install

.protoファイルはプロジェクトに入れる必要がある?

.protoファイルはSwift(Objective-c)のプログラムができてしまえば不要です。
なので、XCodeのプロジェクトに含める必要はありません。

しかし、gitなどのバージョン管理に含めておかないと複数人で開発する時にはpodコマンドで失敗してしまいます。
なので、バージョン管理には含めておくと良いでしょう。

階層化について

もしprotoファイルを階層化構造で管理したい場合は上記のpodspecを一部変更する必要があります。
以下に例を記載しておきます。

  s.prepare_command = <<-CMD
    mkdir -p #{dir}
    #{protoc} \
        --plugin=protoc-gen-grpc=#{plugin} \
        --objc_out=#{dir} \
        --grpc_out=#{dir} \
        -I #{src} \
        -I #{protoc_dir} \
        #{src}*.proto #{src}*/*.proto # ここを変更

    # この行を追加
    sed -i '' 's/\\(.import "\\).*\\/\\(.*"\\)/\\1\\2/' #{dir}/*/*.h #{dir}/*/*.m
  CMD

これは2階層にした場合の例です。
sed -iをしているのはpodによってできたobjective-cのファイルがコンパイル時に全てルート直下に移動するためimportのパスがずれてしまうための施策です。
おまじないのようなものです。気になる人はその行をコメントアウトして実行してみてください。

protoからコードの作成

以下のようなファイルをproto/配下に配置してしてください。

test.proto
syntax = "proto3";

package sample;
option objc_class_prefix = "GRPC";

service SampleService {
  rpc Test (SampleRequest) returns (SampleResponse);
}

message SampleRequest {
  string text = 1;
}

message SampleResponse {
  string result = 1;
}

その後podspecファイルのバージョンを上げてください。

s.version  = '0.0.2' # protoファイルの中身が変わるたびにバージョンを上げてください。

そしてpod installを行なってください。

接頭区について

protoファイルの中に

option objc_class_prefix = "GRPC";

と書いています。
これを書いておくとprotoからできたSwiftのクラスを使う時に接頭区をつけてくれます。
今回はGRPCをつけているので、Protoから作成されたクラスには全てGRPCがつくためわかりやすくなります。
(クラス名が競合することもなくなると思います。)

Swiftコードのサンプル

では実際にコードを実行します。
これらのコードはpod installを行なった時点で作られています。(実態はobjective-cですがSwift用のクラスも作られています。)
サーバーは建っているものとします。

import test // podspecのファイル名

class Test() {
  func run() {
    // サービスにアクセスするクラスを準備します。
    let client = GRPCSampleService(host: "test.server:443")
    // アクセスする際に使うクラスを生成します。
    let request = GRPCSampleRequest()
    request.text = "サーバーへ送りたい文字"
    // 実際にアクセスします。
    client.test(with: request) { (response, error) in
      // レスポンスが帰ってきます。エラーハンドリングなどは必要に応じて行なってください。
      if let response = response {
        print(response.result)
      }
    }
  }
}

これで一通りの挙動は完成です。
あとは簡単にトピックを紹介します。

エラーハンドリング

書くまでもないことですが、困る人もいるかもしれないのでサンプルを書いておきます。

if let error = error as NSError? {
  let alert = UIAlertController(title: "通信エラー", message: "通信に失敗しました、\n(\(error.localizedDescription))", preferredStyle: .alert)
}
let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: .default) { (_) in
  // OKを押された場合の処理
}
alert.addAction(defaultAction)
viewController?.present(alert, animated: true)

鍵認証

鍵はGRPCの通信全てに行うことができます。
(リクエスト別に鍵を使う必要がなかったので、その辺は調査していません。)

// 鍵ファイルを読み込む(ca.crtファイルの場合)
if let certUrl = Bundle.main.url(forResource: "ca", withExtension: "crt") {
  let certificate = try String(contentsOf: certUrl, encoding: .utf8)
  try! GRPCCall.setTLSPEMRootCerts(certificate, forHost: ConnectionString)
}

GRPCリクエストにヘッダーをつける。

この記事を書こうと思ったのは、この処理の文献がなかったから。
なのに気がつけば他の内容の方がはるかに多いという・・・。

まぁ、他ではまっている人の助けになれば良いな。

client.test(with: request) { (response, error) in
  // レスポンスが帰ってきます。エラーハンドリングなどは必要に応じて行なってください。
  if let response = response {
    print(response.result)
  }
}

上記ではこのように呼び出していたが、これを以下のように変更する。

let protoCall = client.rpcToTest(with: request) { (response, error) in
  // レスポンスが帰ってきます。エラーハンドリングなどは必要に応じて行なってください。
  if let response = response {
    print(response.result)
  }
}
// ここにヘッダーリクエストを登録。他にもレスポンスなどの登録もできる?(未確認だがメソッドはあった)
protoCall.requestHeaders.addEntries(from: ["paramName":"value"])
// 実行します。
protoCall.start()

pod installを行うとrpcToが付いたメソッドも用意されていて、これを使うことでGRPCCallの部分へアクセスすることができるようになる。
これがわからなくて半日ほどgoogleを調べたりobjective-cのコードを追ったりしてしまった・・・。
(答えはすぐそこにあったのに気がつかなかった・・・)

最後に

自分がハマった事を文章にする事大切。
誰かの役に立ちますように(願望

※上記のコードは全て未検証です。間違ってたら適時修正お願いします。

2
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
2
2