0
2

More than 3 years have passed since last update.

Android/Kotlin + Go で gRPC Bidirectional Streaming するサンプルをゼロから構築

Last updated at Posted at 2020-03-21

これは何

サーバが Go 、クライアントが Android/Kotlin という構成で 双方からリアムタイムにメッセージを送り合う仕組みとして、gRPC Bidirectional Streaming を使ったサンプルを作るのに必要な設定やコードをまとめたものです。

いろいろ調べながらやっていると新旧どれが正しい情報かわからず結構手間がかかったり、基本的な所でつまづいていることに気づきにくかったりしたため、この記事もしばらくの間しか使えないと思いつつ、記事にまとめておくことにしました。

Go も Android/Kotlin も普段使いしているわけではないため、色々と内容に不備があると思います。何かあれば指摘いただけるとありがたいです!

仕様

  • 文字列ひとつだけのメッセージ Greeting をサーバとクライアント間で双方向で送り合う
  • サーバは数秒ごとに連番付きのメッセージをクライアントに送り続ける
  • サーバはクライアントからメッセージを受信したら標準出力に表示
  • クライアントはボタンをクリックすると TextEdit の内容をサーバに送信する
  • クライアントはサーバからメッセージを受信したら TextView に表示

proto ファイルはこういう内容にしました

proto/sample.proto
syntax = "proto3";

package main;

service Greet {
  rpc Say(stream Greeting) returns (stream Greeting) {}
}

message Greeting {
  string body = 1;
}

サーバ(Golang)

MacOSX Mojave で開発しました。

まずは最新版の protoc バイナリをインストールします

$ brew update     
$ brew upgrade    
$ brew upgrade protobuf 

$ protoc --version
libprotoc 3.6.0

Go 用のプラグインを追加して PATH に追加

$ go get -u github.com/golang/protobuf/proto
$ go get -u github.com/golang/protobuf/protoc-gen-go
PATH=$PATH:$HOME/go/bin

前述の proto ファイル proto/sample.proto をコンパイルして src/sample.pb.go を生成します。今回はサンプルなので main package 内に作ってしまいます

$ protoc --go_out=plugins=grpc:src -Iproto sample.proto

サーバの実装は以下のようにしました。

gRPC のサーバに必要なインターフェースを実装した func (s *myGreetService) Say(stream Greet_SayServer) error が本体です。

そこから 2つの goroutine を起動して無限ループします。それぞれで送信と受信を行いつつ、どちらかでエラーが出たらその接続を終了しています。

src/main.go
package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "time"

    "google.golang.org/grpc"
)

type myGreetService struct{}

func send(stream Greet_SayServer, ch chan error) {
    i := 0
    for {
        message := "message #" + fmt.Sprintf("%d", i)
        log.Println("sending: " + message)
        err := stream.Send(&Greeting{Body: message})
        if err != nil {
            log.Println(err)
            ch <- err
            return
        }
        time.Sleep(1 * time.Second)
        i++
    }
}

func receive(stream Greet_SayServer, ch chan error) {
    for {
        in, err := stream.Recv()
        if err == io.EOF || err != nil {
            log.Println(err)
            ch <- err
            return
        }
        log.Println("received: " + in.GetBody())
    }
}

func (s *myGreetService) Say(stream Greet_SayServer) error {
    log.Println("Stream started.")

    ch := make(chan error)

    go send(stream, ch)
    go receive(stream, ch)
    err := <-ch

    log.Println("Stream closed.")
    return err
}

func main() {
    listenPort, err := net.Listen("tcp", ":5000")
    if err != nil {
        log.Fatalln(err)
    }
    server := grpc.NewServer()
    sampleService := &myGreetService{}
    RegisterGreetServer(server, sampleService)
    server.Serve(listenPort)
}

クライアント(Android/Kotlin)

環境設定

Android Studio 3.5 で環境を作るのに少し苦労しました

まずは Empty Activity のテンプレートから新規プロジェクトを作成。

最初に Gradle のバージョンを最新に変更します。これがないと 後のプラグインが java.lang.NoClassDefFoundError: org/gradle/api/attributes/LibraryElemen を出してしまいハマります

/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip

続いて build.gradle と app/build.gradle に protoc 関連の依存関係設定を追加します。

protobuf-gradle-plugin のドキュメントが一番頼りになりました。 1

なお、各種ライブラリのバージョンは 全て、常に何が最新版なのかを調べてそれを設定 したほうが良さそうです。

/build.gradle
buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        ...
        mavenCentral()
    }
    dependencies {
        ...
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
    }
}
/app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' 
apply plugin: 'com.google.protobuf'   // <- これは 順番としてここにないといけない

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.11.0'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.28.0'
        }
    }

    generateProtoTasks {
        all().each {
            it.builtins {
                java { option 'lite' }
            }
            it.plugins {
                grpc { option 'lite' }
            }
        }
    }
}

dependencies {
    ...
    implementation 'javax.annotation:javax.annotation-api:1.3.2' // 生成されるスタブのためのにこれも必要
    implementation "io.grpc:grpc-okhttp:1.28.0"
    implementation "io.grpc:grpc-protobuf-lite:1.28.0"
    implementation "io.grpc:grpc-stub:1.28.0"
}

クライアントアプリ

proto ファイルは src/java/main/proto/sapmle.proto に配置しました。

Java の パッケージ名を追加しています

...
option java_package = "com.github.reki2000.grpcclient";
...

Activity の具体的な実装内容は以下のようになりました。

MainActivity.kt
package com.github.reki2000.grpcclient

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import io.grpc.ManagedChannelBuilder
import io.grpc.stub.StreamObserver

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit private var stream : StreamObserver<Sample.Greeting>
    lateinit private var handler : Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        handler = Handler()

        Log.i("app","started")
        initGRPC()
        button.setOnClickListener({ onButtonClick() })
    }

    fun initGRPC() {
        try {
            val channel = ManagedChannelBuilder.forAddress("10.0.2.2", 5000).usePlaintext().build()
            val stub = GreetGrpc.newStub(channel)
            val responseObserver = object : StreamObserver<Sample.Greeting> {
                override fun onNext(greet: Sample.Greeting) {
                    onServerMessageReceived(greet)
                }
                override fun onError(t: Throwable) {
                    Log.e("app", "error", t)
                }
                override fun onCompleted() {
                    Log.i("app", "connection closed")
                }
            }
            stream = stub.say(responseObserver)
        } catch (e: Exception) {
            Log.e("app", "grpc connection error",e)
        }
    }

    // サーバからのメッセージを受信したら 画面表示用スレッド経由で textView に表示する
    fun onServerMessageReceived(greet: Sample.Greeting) {
        handler.post({showMessage("Received: ${greet.body}")})
    }

    // ボタンがクリックされたら textView に表示するとともに サーバにメッセージを送信する
    fun onButtonClick() {
        val text = editText.text.toString()

        try {
            val request = Sample.Greeting.newBuilder().setBody(text).build()
            stream.onNext(request)
        } catch (e: Exception) {
            Log.e("app", "grpc error",e)
        }
        showMessage("Sent: ${text}")
    }

    fun showMessage(text: String) {
        textView.text = "${textView.text.toString()}${System.lineSeparator()}${text}"
        Log.i("app", text)
    }
}


なお、アプリの manifest で HTTP を有効にしていないといけません

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.reki2000.grpcclient" >
    ...
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>

実行

クライアントの画面
image.png

サーバのログ

2020/03/21 13:51:20 Stream started.
2020/03/21 13:51:20 sending: message #0
2020/03/21 13:51:21 sending: message #1
2020/03/21 13:51:22 sending: message #2
2020/03/21 13:51:23 sending: message #3
2020/03/21 13:51:24 sending: message #4
2020/03/21 13:51:25 sending: message #5
2020/03/21 13:51:26 sending: message #6
2020/03/21 13:51:26 received: message
2020/03/21 13:51:27 sending: message #7

だいぶ苦労しましたが、うまく双方向で通信できています!

その他気になっていること

  • エンドポイントが一つだけなので いろいろなメッセージをやり取りする場合も一つのオブジェクトがいろいろな処理のための情報を含むような形にしなければならない
  • TCPが張りっぱなしになるが、通信が切れた・黙ってしまったときの復旧を含めた構成を検討したい
  • この構成でサーバがどれくらいの同時接続に耐えられるか

  1. 注意点として README が長く いろいろな記述サンプルがありますが、Androidの場合は最後の方の "Starting from Protobuf 3.8.0, lite code generation is built into protoc's "java" output. Example:" のところを見なければならないということを理解するまでにだいぶつまづきました 

0
2
0

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