Google App Engine でgRPCを使うためのgRPC Web #golang #gcpja

アドベントカレンダー1番手です。
僕から見た今年のGoogle Cloud PlatformはMachine LearningとFirebaseかなーという印象でした。
データベース周りではSpannerも熱いですね。高くて使えてませんが:sob: 
ちなみに僕の好きなGoogle App Engineの今年ははChannel APIがシャットダウンされたこととGo 1.8が使えるようになりました。
だけど、はやくGo 1.9来て！！！！！

去年は[GAEの構成](https://qiita.com/koki_cheese/items/216fe73caf958db34aa2)について書きましたが今年はAPIをどうするか？という事を書きます。

# gRPCとは

[gRPC](https://grpc.io/)は、Googleが開発したオープンソースのRPCフレームワークです。HTTP/2の上で [Protocol Buffers](https://developers.google.com/protocol-buffers/)でシリアライズされたデータを高速に送ることができます。

gRPCはProcol BuffersのIDLを使ってサービスとメソッドを定義します。
そのIDLからgRPCはクライアントやサーバで実装に使うためのインターフェイスの自動生成をしてくれます。

IDLはサービスを定義するための専用の構文なのでプログラミング言語でclassやstructを書くように書けるのでJSONやYAMLで書くような辛さはないところがよいです。(エディタの支援が受けやすいという意味で)

gRPCは有名な言語をいくつもサポートしています。

- C++
- Java
- Python
- Go
- Ruby
- C#
- Node.js
- Android Java
- Objective-C
- PHP

# gRPC Webとは

クライアントも自動生成してくれるし小さいデータをやりとりできてとっても素晴らしいフレームワークなのですが一つ弱点があります。

それはブラウザで使えないことです。

Node.jsはサポートされているのでJavaScriptで使えますがブラウザでは使えません。
しかしその弱点を補うために開発されているのが[gRPC Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md)です。

現在は仕様だけで実装は[grpc/grpc](https://github.com/grpc/grpc)に取り込まれていません、現在はまだprivateで開発中です。

ただ仕様があれば実装も誰かが作っているもので、それが[improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web)です。

# Google App EngineでgRPC Webを使う

Google App Engineを知らない人のために簡単に説明するとWebサービスを作るためのGoogle Cloud PlatformのPaaSです。

Herokuと比べると制約が多いですがその制約の上で作ることでスケールするサービスが出来上がります。

豊富な無料枠があるので放置しててもお金がかからないため、よく遊び場として使っています。

大抵のWebサービスを作るならGAEで全てができると思っているのですがAPIサーバを作る時REST APIを作ったりするのがめんどくさいしgRPCを使いたいのですが悲しいことにgRPCをApp Engineでは使えません。

なのでgRPC Webを使います。

ここから紹介するコードは全て[GitHub](https://github.com/k2wanko/gae-grpc-web)にあるのでそちらを参考にしてみてください。

デモ: https://gae-grpc-web.appspot.com/

## サービスの定義

まずはIDLでサービスを定義します。

```protobuf
syntax = "proto3";

package net.k2lab.test.grpc.testing.echo;
option go_package = "echo";

message EchoRequest {
  string message = 1;
}

message Echo {
  string id = 1;
  string message = 2;
  int64 created = 3;
}

message EchoResponse {
  Echo echo = 1;
}

message EchoHistoryRequest {
  int32 limit = 1;
}

service EchoService {
  rpc Echo(EchoRequest) returns (EchoResponse);
  rpc EchoHistory(EchoHistoryRequest) returns (stream EchoResponse);
}

```

`EchoService`を定義して`Echo`と`EchoHistory`メソッドを定義しました。

次にここからインターフェイスとクライアントを自動生成します。

```bash
$ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
$ go get -u google.golang.org/grpc
$ npm i -D ts-protoc-gen

$ protoc \
    --plugin=protoc-gen-go=${GOPATH}/bin/protoc-gen-go \
    --plugin=protoc-gen-js=../node_modules/.bin/protoc-gen-js_service \
    --go_out=plugins=grpc:. \
    --js_out=import_style=commonjs,binary:. \
    --js_out=. \
    ./echo.proto
```

このコマンドを実行すると`echo.pb.go`と`echo_pb.js`と`echo_pb_service.js`が生成されます。

## サーバの実装

先にサーバの実装をします。

App Engineの設定ファイルである`app.yaml`は次の通りです。

```yaml:app.yaml
runtime: go
api_version: go1.8

handlers:
- url: /(.+(\.js|\.css))$
  static_files: app/\1
  upload: app/(.+(\.js|\.css))$
  expiration: 10m
  secure: always
- url: /(.+(\.gif|\.png|\.jpg))$
  static_files: app/\1
  upload: app/(.+(\.gif|\.png|\.jpg))$
  expiration: 30m
  secure: always
- url: /.*
  script: _go_app
  secure: always

```

Goのバージョンは1.8を使います。

次に`server.go`の実装です。

```go:server.go
package backend

import (
	"html/template"
	"net/http"
	"strings"
	"time"

	"golang.org/x/net/context"

	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"github.com/k2wanko/gae-grpc-web/echo"
	"github.com/k2wanko/gaegrpc"
	"google.golang.org/appengine/log"
)

var tpl *template.Template

func init() {
	l, err := time.LoadLocation("Asia/Tokyo")
	if err != nil {
		panic(l)
	}
	time.Local = l

	tpl = template.Must(template.New("").ParseFiles("index.html"))

	sv := gaegrpc.NewServer()
	echo.RegisterEchoServiceServer(sv, &EchoService{})

	wh := gaegrpc.NewWrapHandler(grpcweb.WrapServer(sv))
	http.HandleFunc("/", createAppHandler(wh))
}

func createAppHandler(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc-web") {
			h.ServeHTTP(w, r)
		} else {
			serverTop(w, r)
		}
	}
}

func serverTop(w http.ResponseWriter, r *http.Request) {
	tpl.ExecuteTemplate(w, "index.html", nil)
}

func logf(ctx context.Context, format string, args ...interface{}) {
	log.Infof(ctx, format, args...)
}

func debugf(ctx context.Context, format string, args ...interface{}) {
	log.Debugf(ctx, format, args...)
}

func warnf(ctx context.Context, format string, args ...interface{}) {
	log.Warningf(ctx, format, args...)
}

func errorf(ctx context.Context, format string, args ...interface{}) {
	log.Errorf(ctx, format, args...)
}

```

Go 1.8ですが`net/context`を使ってるのは`grpc-go`が標準の`context`パッケージを使っていないためです。

次に`Content-Type`をみて処理をわけているところはgRPC以外の通信が来たらHTMLを返すようにしています。

そしてGAEでgRPC Webを使うために止む無く登場するのが`github.com/k2wanko/gaegrpc`パッケージです。

これの仕事は主に２つあります。

１つはGAEのサービスを利用するために`appengine.NewContext`をgRPCのメソッドのハンドラに渡すことです。

`*http.Request`をメモリ上に保存しておきメソッドの実装に渡すようにしてくれます。

もう一つは`improbable-eng/grpc-web`がGAEを考慮していないためerrorが起きるところを吸収しています。

具体的にはGAEのGoの`http.ResponseWriter`には`http.CloseNotify`が実装されていないため実装がなければnilを返すようにしています。

メソッドの実装は次のようになっています。

```go:echo.go
package backend

import (
	"time"

	"github.com/k2wanko/gae-grpc-web/echo"
	"github.com/kjk/betterguid"
	"golang.org/x/net/context"
	"google.golang.org/appengine/datastore"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

// EchoService structs
type EchoService struct{}

// Echo structs message
type Echo struct {
	ID      *datastore.Key `datastore:"-"`
	Message string
	Created time.Time
}

// ToMessage returns Echo of protocl buffer
func (e *Echo) ToMessage() *echo.Echo {
	id := ""
	if e.ID != nil {
		id = e.ID.StringID()
	}
	return &echo.Echo{
		Id:      id,
		Message: e.Message,
		Created: e.Created.Unix(),
	}
}

// Echo implements EchoServiceServer
func (*EchoService) Echo(ctx context.Context, req *echo.EchoRequest) (res *echo.EchoResponse, err error) {
	grpc.SendHeader(ctx, metadata.Pairs("Pre-Response-Metadata", "Is-sent-as-headers-unary"))
	grpc.SetTrailer(ctx, metadata.Pairs("Post-Response-Metadata", "Is-sent-as-trailers-unary"))
	msg := req.GetMessage()

	logf(ctx, "Echo Message = %s", msg)

	k := datastore.NewKey(ctx, "Echo", betterguid.New(), 0, nil)
	e := &Echo{
		ID:      k,
		Message: req.GetMessage(),
		Created: time.Now(),
	}

	if _, err := datastore.Put(ctx, k, e); err != nil {
		errorf(ctx, "don't save message: %v", err)
		return nil, err
	}

	res = &echo.EchoResponse{
		Echo: e.ToMessage(),
	}

	return
}

// EchoHistory implements EchoServiceServer
func (*EchoService) EchoHistory(req *echo.EchoHistoryRequest, ss echo.EchoService_EchoHistoryServer) (err error) {
	limit := int(req.GetLimit())
	if limit <= 0 {
		limit = 10
	} else if limit > 100 {
		limit = 100
	}

	ctx := ss.Context()

	it := datastore.NewQuery("Echo").
		Limit(limit).
		Order("-Created").
		Run(ctx)
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		e := &Echo{}
		k, err := it.Next(e)
		if err == datastore.Done {
			break
		} else if err != nil {
			errorf(ctx, "don't get history: %v", err)
			return err
		}
		e.ID = k
		ss.Send(&echo.EchoResponse{
			Echo: e.ToMessage(),
		})
	}

	return
}

```

`gaegrpc`のおかげで`ctx context.Context`には既に`appengine.NewContext`の戻り値が入っているのでdatastoreへもアクセスできるようになりました。

## クライアントの実装(ブラウザ)

次はクライアントの実装です。
ブラウザでアプリケーションを作るためのフレームワークとしてVue.jsを使っていますが
Vue.jsに依存してるわけではないので好きなフレームワークと組み合わせられます。


```html:App.vue
<template>
  <div id="app">
    <h1>grpc web testing</h1>
    <ul>
      <li><input type="text" v-model="echoRequest"><button @click="echo">Echo</button><span>{{echoResponse}}</span></li>
    </ul>
    <h2>History</h2>
    <ul>
      <li v-for="echo in history" :key="echo.id">{{echo.message}}</li>
    </ul>
  </div>
</template>

<script>
import { grpc, Code, Metadata } from "grpc-web-client";
import { EchoService } from "../echo/echo_pb_service";
import { EchoRequest, EchoHistoryRequest } from "../echo/echo_pb";

const host = location.protocol + "//" + location.hostname + ":" + location.port;

export default {
  name: "app",
  data() {
    return {
      echoRequest: "",
      echoResponse: "",
      history: []
    };
  },
  mounted() {
    this.fetchEchoHistory();
  },
  methods: {
    echo() {
      if (!this.echoRequest) {
        return;
      }
      const request = new EchoRequest();
      request.setMessage(this.echoRequest);
      grpc.unary(EchoService.Echo, {
        request,
        host,
        onEnd: res => {
          const { status, statusMessage, headers, message, trailers } = res;
          console.log("EchoService.Echo.onEnd.status", status, statusMessage);
          console.log("EchoService.Echo.onEnd.headers", headers);
          if (status === Code.OK && message) {
            const resp = message.toObject();
            console.log("EchoService.Echo.onEnd.message", resp);
            this.echoResponse = resp.echo.message;
            if (this.history.length >= 10) {
              this.history.pop();
            }
            this.history.unshift(resp.echo);
          }
          console.log("EchoService.Echo.onEnd.trailers", trailers);

          this.echoRequest = "";
        }
      });
    },
    fetchEchoHistory() {
      const request = new EchoHistoryRequest();
      request.setLimit(10);
      grpc.invoke(EchoService.EchoHistory, {
        request,
        host,
        onMessage: msg => {
          const resp = msg.toObject();
          console.log("fetchEchoHistory", "onMessage", resp);
          this.history.push(resp.echo);
        },
        onEnd: () => {}
      });
    }
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-top: 60px;
  text-align: center;
}

h1,
h2 {
  font-weight: normal;
}

ul {
  /* list-style-type: none; */
  padding: 0;
}

/* li {
  display: inline-block;
  margin: 0 10px;
} */

a {
  color: #42b983;
}
</style>

```

戻り値が１つであれば`grpc.unary`を使いstreamの場合は`grpc.invoke`を使っておくとよさそうです。

使い分けるのがめんどうな場合は`grpc.invoke`だけ使えばよさそうですね。

## クライアントの実装(Node.js)

おまけですが`grpc-web-client`はNode.jsでも使えます。

```js:main.js
const {EchoHistoryRequest} = require('../echo/echo_pb')
const {EchoService} = require('../echo/echo_pb_service')

const {grpc, Code} = require('grpc-web-client')

const host = 'https://gae-grpc-web.appspot.com:443'

function main() {
  const request = new EchoHistoryRequest()
  request.setLimit(10)
  grpc.invoke(EchoService.EchoHistory, {
    request,
    host,
    onMessage: msg => {
      const resp = msg.toObject()
      console.log("fetchEchoHistory", "onMessage", resp)
    },
    onEnd: () => {}
  })
}

main()

```

[Cloud Functions](https://cloud.google.com/functions/?hl=ja)で動くということで、つまりGAEをAPIサーバにしてCloud Functionsでフロントと一貫したAPIを使ってサーバサイドレンダリングができるということです。

もちろん用途はサーバサイドレンダリングだけじゃなくCloud FunctionsとGAEの橋渡しに使えるので夢は広がります。

# まとめ

[ブラウザ] - [gRPC Web] - [App Engine]

で通信ができるようになり

モバイルアプリも僕はReact Nativeを使ってるのでgRPC Webで行けると思っています。

ネイティブはgRPC Webではなく普通にgRPCを使えばいいと思いますがGAEと直接コミュニケーションできないのが辛いところですね。

ちなみにgRPC Webは、いつかGAEがgRPCをネイティブに使えるようになったときサーバ側の移行は容易だと信じてます。

サンタさんにはgRPCを直接喋れるGAEをお願いしました。