これは何
サーバが Go 、クライアントが Android/Kotlin という構成で 双方からリアムタイムにメッセージを送り合う仕組みとして、gRPC Bidirectional Streaming を使ったサンプルを作るのに必要な設定やコードをまとめたものです。
いろいろ調べながらやっていると新旧どれが正しい情報かわからず結構手間がかかったり、基本的な所でつまづいていることに気づきにくかったりしたため、この記事もしばらくの間しか使えないと思いつつ、記事にまとめておくことにしました。
Go も Android/Kotlin も普段使いしているわけではないため、色々と内容に不備があると思います。何かあれば指摘いただけるとありがたいです!
仕様
- 文字列ひとつだけのメッセージ
Greeting
をサーバとクライアント間で双方向で送り合う - サーバは数秒ごとに連番付きのメッセージをクライアントに送り続ける
- サーバはクライアントからメッセージを受信したら標準出力に表示
- クライアントはボタンをクリックすると TextEdit の内容をサーバに送信する
- クライアントはサーバからメッセージを受信したら TextView に表示
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 を起動して無限ループします。それぞれで送信と受信を行いつつ、どちらかでエラーが出たらその接続を終了しています。
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
を出してしまいハマります
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
続いて build.gradle と app/build.gradle に protoc 関連の依存関係設定を追加します。
protobuf-gradle-plugin のドキュメントが一番頼りになりました。 1
なお、各種ライブラリのバージョンは 全て、常に何が最新版なのかを調べてそれを設定 したほうが良さそうです。
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
...
mavenCentral()
}
dependencies {
...
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
}
}
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 の具体的な実装内容は以下のようになりました。
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 を有効にしていないといけません
<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>
実行
サーバのログ
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が張りっぱなしになるが、通信が切れた・黙ってしまったときの復旧を含めた構成を検討したい
- この構成でサーバがどれくらいの同時接続に耐えられるか
-
注意点として README が長く いろいろな記述サンプルがありますが、Androidの場合は最後の方の "Starting from Protobuf 3.8.0, lite code generation is built into protoc's "java" output. Example:" のところを見なければならないということを理解するまでにだいぶつまづきました ↩