読者です 読者をやめる 読者になる 読者になる

gRPCでサーバーとAndroid/iOSを通信する - Re.Ra.Ku アドベントカレンダー day 22

Re.Ra.Ku アドベントカレンダー 22日目です。

こんにちは。安部です。

ずっと気になっていたgRPCをせっかくの機会なので試してみました。

基本はgRPCの公式のサンプルとほぼ変わっていませんが、サーバー側とAndroid/iOSを連携するために必要なものをまとめてみました。

gRPCとは

grpc / grpc.io

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のドキュメントを見てください。

メッセージとしてHelloRequestHelloReplyを定義して、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/mainprotoディレクトリを作ってそこに、先程作成した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ファイルによってインターフェースもしっかり定義されるので、それ自体が仕様になるのも良かったです。

敷居は高い感じですが、導入事例も国内外で結構あるようなので今後選択肢に入ってくるのではないでしょうか。