gRPCでサーバーとAndroid/iOSを通信する - Re.Ra.Ku アドベントカレンダー day 22
Re.Ra.Ku アドベントカレンダー 22日目です。
こんにちは。安部です。
ずっと気になっていたgRPCをせっかくの機会なので試してみました。
基本はgRPCの公式のサンプルとほぼ変わっていませんが、サーバー側とAndroid/iOSを連携するために必要なものをまとめてみました。
gRPCとは
A high performance, open-source universal RPC framework
Googleが作ったRPCフレームワークです。
gRPCでは様々な言語をサポートしていて、Protocol Buffersを使ってます。
Protocol Buffersのインターフェース定義からgRPCのコード生成できるようになっています。
gRPCインストール
Protocol Buffersの.proto
ファイルからコードを生成するためのビルドツールをインストールします。
Macを使っているので、今回はHomebrewを使ってインストールします。
$ brew tap grpc/grpc $ brew install --with-plugins grpc
grpc/homebrew-grpc: gRPC formulae repo for Homebrew
protoファイル
Protocol Buffersのインターフェースの定義ファイルになります。インタフェース定義言語(IDL)を使って記述していきます。これを元にソースコードを生成します。
サーバーとクライアントの両方で同じファイルを使用するのでjavaやobjcの設定もあります。
詳しい仕様はProtocol Buffersのドキュメントを見てください。
メッセージとしてHelloRequest
とHelloReply
を定義して、Greeter
というサービスでRPCのSayHello
メソッドを定義している感じです。
helloworld.proto
で保存します。
syntax = "proto3"; option java_multiple_files = true; option java_package = "com.star_zero.example.grpcandroid.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
サーバー側設定
今回はRubyでやります。ちょっと調べた感じGoで書いてるのが多い気がしましたが、私はGoがまだ分からないので…
セットアップ
$ bundle init
Gemfileを次のようにします。
source "https://rubygems.org" gem "grpc"
bundle install
$ bundle install --path vendor/bundle
protoファイルからソースコード生成
protoc
というコマンドを使ってソースコードを生成します。
lib
ディレクトリに出力するので事前にディレクトリを作成しています。helloworld.proto
は一つ上の階層のprotos
ディレクトリに配置しています。パスは適宜変更指定ください。
$ mkdir lib $ protoc -I ../protos --ruby_out=lib --grpc_out=lib --plugin=protoc-gen-grpc=`which grpc_ruby_plugin` ../protos/helloworld.proto
これを実行するとソースコードが2つ生成されていると思います。
- lib/helloworld_pb.rb
- lib/helloworld_services_pb.rb
サーバー側コード
かなり分かってないこと多いですが、先程のprotoファイルから生成されたコードを使ってRPCサーバーを起動しています。
この例ではもらったリクエストパラメータにHelloという文字列をつけて返却しています。
root = File.dirname(__FILE__) $LOAD_PATH.unshift File.join(root, 'lib') require 'rubygems' require 'bundler/setup' require 'grpc' require 'helloworld_services_pb' class GreeterServer < Helloworld::Greeter::Service def say_hello(hello_req, _unused_call) Helloworld::HelloReply.new(message: "Hello #{hello_req.name}") end end def main s = GRPC::RpcServer.new s.add_http2_port('0.0.0.0:50051', :this_port_is_insecure) s.handle(GreeterServer) s.run_till_terminated end main
実行
$ ruby server.rb
これでサーバー起動します。
Androidクライアント
適当にプロジェクトを作ります。
build.gradle
プロジェクト直下のbuild.gradleにprotocol bufferのプラグインを追加します。
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.2.2' classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.0" } }
app直下のbuild.gradleにgRPC関連の設定と、protoからコードを生成するための設定を追記します。
apply plugin: 'com.google.protobuf' // ... android { // Warning:Conflict with dependency 'com.google.code.findbugs:jsr305'. // が出たときはこれを追加します configurations.all { resolutionStrategy.force 'com.google.code.findbugs:jsr305:2.0.1' } } dependencies { // ... compile 'com.squareup.okhttp:okhttp:2.7.5' compile 'io.grpc:grpc-okhttp:1.0.2' compile 'io.grpc:grpc-protobuf-lite:1.0.2' compile 'io.grpc:grpc-stub:1.0.2' compile 'javax.annotation:javax.annotation-api:1.2' } protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.0.0' } plugins { javalite { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" } grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.0.2' } } generateProtoTasks { all().each { task -> task.plugins { javalite {} grpc { option 'lite' } } } } }
protoファイル
app/src/main
にproto
ディレクトリを作ってそこに、先程作成したhelloworld.proto
を配置します。
少し確認した感じですと、proto
っていうディレクトリ名が重要でこれを間違えているとうまくビルドできませんでした。
ビルド
ビルドをすると、app/build/generated/source/proto
内にソースコードが生成されていると思います。
ソースコード
サーバーとの通信するには、protoファイルから生成されたものを使用して通信します。
newBlockingStub
でstubを作ると、処理をブロックして通信が終わるのを待ちます。
// ホストとポートを指定(エミュレータからlocalhostアクセスのために10.0.2.2にしてます) ManagedChannel channel = ManagedChannelBuilder.forAddress("10.0.2.2", 50051) .usePlaintext(true) .build(); GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel); // リクエスト生成してパラメータを設定 HelloRequest message = HelloRequest.newBuilder().setName("Android").build(); // 実行して結果を受け取る HelloReply reply = stub.sayHello(message); return reply.getMessage();
非同期でやる場合は次のようにnewStub
でstubを作って、メソッドを呼び出すときにObserverを渡してあげる感じになります。RxJavaっぽい。
GreeterGrpc.GreeterStub stub = GreeterGrpc.newStub(channel); HelloRequest message = HelloRequest.newBuilder().setName("Android").build(); stub.sayHello(message, new StreamObserver<HelloReply>() { @Override public void onNext(HelloReply reply) { Log.d(TAG, "Message = " + reply.getMessage()); } @Override public void onError(Throwable t) { } @Override public void onCompleted() { } });
iOS
こちらも適当にプロジェクトを作ります。今回はSwiftでやっています。
CocoaPods
CocoaPodsを使いますが、結構特殊な感じになってます。
まずはpodspecを作ります。通常はライブラリ作るときに使うんですけど、protoファイルから生成したコードをライブラリとして設定するためのものになります。
authorsやディレクトリ等の設定は環境に合わせてください。authorsとかはとりあえずサンプルの設定のままやってます。
s.prepare_command
のとこでprotoファイルからソースコードを生成しています。
HelloWorld.podspec
として保存します。
Pod::Spec.new do |s| s.name = "HelloWorld" s.version = "0.0.1" s.license = "New BSD" s.authors = { 'gRPC contributors' => 'grpc-io@googlegroups.com' } s.homepage = "http://www.grpc.io/" s.summary = "HelloWorld example" s.source = { :git => 'https://github.com/grpc/grpc.git' } s.ios.deployment_target = "10.1" # protoファイルのディレクトリ src = "../protos" # gRPCのプラグイン s.dependency "!ProtoCompiler-gRPCPlugin", "~> 1.0" pods_root = 'Pods' protoc_dir = "#{pods_root}/!ProtoCompiler" protoc = "#{protoc_dir}/protoc" plugin = "#{pods_root}/!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin" dir = "#{pods_root}/#{s.name}" # protoファイルからソースコード生成処理 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}/helloworld.proto CMD # 生成されたコードからsubspec設定 s.subspec "Messages" do |ms| ms.source_files = "#{dir}/*.pbobjc.{h,m}", "#{dir}/**/*.pbobjc.{h,m}" ms.header_mappings_dir = dir ms.requires_arc = false ms.dependency "Protobuf" end # 生成されたコードからsubspec設定 s.subspec "Services" do |ss| ss.source_files = "#{dir}/*.pbrpc.{h,m}", "#{dir}/**/*.pbrpc.{h,m}" ss.header_mappings_dir = dir ss.requires_arc = true ss.dependency "gRPC-ProtoRPC" ss.dependency "#{s.name}/Messages" end s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1', 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', } end
Podfile
は以下のように自分のディレクトリをライブラリとして読み込むように設定します。
target 'GrpcIos' do use_frameworks! # HelloWorld.podspec pod 'HelloWorld', :path => '.' end
ここまで出来たらインストールします。
$ pod install
ソースコード
gRPCのコードはObjective-Cになっていますので、Bridging-Headerに使用するものをimportします。
#import <GRPCClient/GRPCCall+ChannelArg.h> #import <GRPCClient/GRPCCall+Tests.h> #import <HelloWorld/Helloworld.pbrpc.h>
Swift側のコードimportします。
import HelloWorld
最後に通信する処理を記述します。
let hostAddress = "localhost:50051" GRPCCall.useInsecureConnections(forHost: hostAddress) GRPCCall.setUserAgentPrefix("HelloWorld/1.0", forHost: hostAddress) let client = HLWGreeter(host: hostAddress) let request = HLWHelloRequest() request.name = "iOS" client.sayHello(with: request, handler: { (reply: HLWHelloReply?, error: Error?) -> Void in if let reply = reply { print("message = " + reply.message) } })
まとめ
少し触ってみた感じ使いこなせるとだいぶ強力だなと思いました。型もあって、メソッド呼び出しと変わらないので、JSONより断然扱いやすかったです。
protoファイルによってインターフェースもしっかり定義されるので、それ自体が仕様になるのも良かったです。
敷居は高い感じですが、導入事例も国内外で結構あるようなので今後選択肢に入ってくるのではないでしょうか。