はじめに
GRPCを扱おう事になって色々調べてもあまり文献がない。
そもそもGRPCの通信ができない、ヘッダーの設定方法や鍵認証通信方法など・・・とにかく色々ハマったのでこれから触る人のために一通りの事を書いておく。
GRPCとは
Googleが提供するPRC。
リモートプロシージャコール。
早い話がネットワーク越しのコンピューターへのアクセスをまるで自分のコンピュータのメソッドを叩いているように通信を行うもの。
APIという言葉に置き換えても話は十分通じます。
選定
grpc-swift
初めはgrpc-swiftで構築しようとした。
構築も楽そうだし、初心者にも敷居が低いと思ったのだが・・・。
やめた。
その理由は鍵を使った認証が出来なかったからだ。(検証日:2019年3月頃)
コードは正しくて、証明書も正しいのにエラーが発生するという状況が1日続いたのでやめた。
ここでエラーが発生する時点で、他でもエラーが発生するかもしれないと思って断念。
その時には気がつかなかったけれど、今振り返ってみるとProto
ファイルの管理も自前で行う必要がありそうだと思ったので断念。
Google本家
本家のGrpcを敬遠したのは二つの理由がある。
- Swift版がない
- 構築が面倒臭そう
grpc-swift
で構築する場合はあっという間だった。
鍵認証がなくてライトなプロジェクトなら間違いなくgrpc-swift
をオススメする。
これは難しそうだ・・・。
と感じる敷居の高さがあった。
ただ、構築が終われば管理は大変ではありません。
僕にとっての敷居が高かった理由はPodspec
を作ったことがない為でした。
構築方法
構築するためには自前でPodを作る必要があった。
そのために使用するのがpodspec
。
これをプロジェクトに配備します。
公式ページにあるものを配備します。
基本的にはルートに配備した方が楽ですが、別に階層の中に配備しても大丈夫です。
ここではファイル名をtest.podspec
にしていますが、何でも良いです。
ただし、ファイル名は後でプロジェクト内のimport文
に記載するのでプロジェクト名+GRPC.podspec
などにしておくとわかりやすいと思います。
以下に使う時に困りそうな箇所をコメントで記載しておきます。
記載がない場所は変更の必要がない箇所 or 僕が把握してない箇所になります。
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/
配下に配置してしてください。
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
のコードを追ったりしてしまった・・・。
(答えはすぐそこにあったのに気がつかなかった・・・)
最後に
自分がハマった事を文章にする事大切。
誰かの役に立ちますように(願望
※上記のコードは全て未検証です。間違ってたら適時修正お願いします。