grpc-web-clientをGAE/Goで動かしてみた #golang #grpc

# [grpc-web-client](https://www.npmjs.com/package/grpc-web-client) とは

[grpc-web-client](https://www.npmjs.com/package/grpc-web-client)はgRPCをWebブラウザから使うためのWrapperライブラリです。
protocol bufferの定義やgRPCのNode.js Clientに手を加えずに利用できるので便利です。

ServerはGoの実装をサポートしています。

今回試したコードは[GitHub](https://github.com/k2wanko/tasting-grpc-web/tree/f595273db766f113844d582ba9490b/)にあります。
次のコマンドでダウンロードできます。

```bash
$ git clone https://github.com/k2wanko/tasting-grpc-web.git
```

# 依存ライブラリのインストール

Goの依存ライブラリ

```
$ go get -u -v github.com/improbable-eng/grpc-web/go/grpcweb
$ go get -u -v github.com/golang/protobuf/{proto,protoc-gen-go}
```

Clientの依存ライブラリ

```
$ npm install --save @types/google-protobuf google-protobuf grpc-web-client
$ npm install --save-dev ts-protoc-gen
```

ts-protoc-genはTypeScriptの型定義ファイルを生成してくれます。

# Echo Serviceの定義

```protobuf:echo.proto
syntax = "proto3";

package grpc.testing.echo;
option go_package = "echo";

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
  int32 message_count = 2;
}

message ServerStreamingEchoRequest {
  string message = 1;

  int32 message_count = 2;

  int32 message_interval = 3;
}

message ServerStreamingEchoResponse {
  string message = 1;
}
```

次のコマンドでサーバインターフェースとクライアントを生成します。

```bash
$ mkdir -p ./echo
$ mkdir -p ./src/proto

$ protoc \
    --go_out=plugins=grpc:./echo \
    --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
    --js_out=import_style=commonjs,binary:./src/proto \
    --ts_out=service=true:./src/proto \
    ./echo.proto
```

# Echo Service Serverの実装

`grpcweb.WrapServer`で包んであげることでHTTP1.1の上でgRPCのやりとりができます。

```go:server.go
package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"github.com/k2wanko/tasting-grpc-web/echo"
	"golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/grpc"
)

func main() {
	appengine.Main()
}

func init() {
	grpcSrv := grpc.NewServer()
	echo.RegisterEchoServiceServer(grpcSrv, &echoService{})
	wrappedServer := grpcweb.WrapServer(grpcSrv)
	http.Handle("/", wrappedServer)
}

type echoService struct{}

func (s *echoService) Echo(ctx context.Context, req *echo.EchoRequest) (res *echo.EchoResponse, err error) {
	res = &echo.EchoResponse{
		Message: req.Message,
	}
	return
}

func (s *echoService) ServerStreamingEcho(req *echo.ServerStreamingEchoRequest, ss echo.EchoService_ServerStreamingEchoServer) (err error) {
	ctx := ss.Context()

	c := int(req.MessageCount)
	if c > 10 || c >= 0 {
		c = 10
	}

	iv := int(req.MessageInterval)
	if iv > 5 || iv >= 0 {
		iv = 5
	}

	for i := 0; i < c; i++ {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			res := &echo.ServerStreamingEchoResponse{Message: fmt.Sprintf("%d:%s", i+1, req.Message)}
			ss.Send(res)
			time.Sleep(time.Duration(iv) * 100)
		}
	}
	return
}
```

Google App Engine Go SDKを入れていれば次のコマンドで実行できます。

```
$ goapp serve backend
```

# grpc-web-clientを使ったClientの実装

ClientはVue.jsのプロジェクトの雛形から作ってますが基本的にはただのTypeScriptです。
`grpc.invoke`というメソッドで呼び出します。

```html:App.vue
<template>
  <div id="#app">
    <h1>tasting-grpc-web</h1>
    <div>
      <div>
        <h2>EchoSerivce.Echo</h2>
        Request: <input type="text" placeholder="message" v-model="echoReq"><br> Result:
        <span>{{echoRes}}</span><br>
        <button @click="echo(echoReq)">echo</button>
      </div>

      <div>
        <h2>EchoSerivce.ServerStreamingEcho</h2>
        Request: <input type="text" placeholder="message" v-model="echoReq"><input type="number" placeholder="count" v-model="echoReqCount"><input type="number" placeholder="interval" v-model="echoReqInterval"><br> Result:
        <div>
          <span :key="i" v-for="(v, i) in echoStreamRes">{{v}}<br></span>
        </div><br>
        <button @click="streamingEcho(echoReq, echoReqCount, echoReqInterval)">echo</button>
      </div>

    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

import { grpc, Code, Metadata } from 'grpc-web-client'
import { EchoService } from './proto/echo_pb_service'
import {
  EchoRequest,
  EchoResponse,
  ServerStreamingEchoRequest,
  ServerStreamingEchoResponse,
} from './proto/echo_pb'

const host = ''

@Component
export default class App extends Vue {
  echoReq = ""
  echoReqCount = 10
  echoReqInterval = 1
  echoRes = ""
  echoStreamRes: string[] = []

  mounted() {
  }

  echo(message: string): void {
    new Promise<EchoResponse>((resolve, reject) => {
      const request = new EchoRequest()
      request.setMessage(message)
      grpc.invoke(EchoService.Echo, {
        request,
        host,
        onMessage: (message: EchoResponse) => {
          resolve(message)
        },
        onEnd: (code) => {
        }
      })
    }).then(res => {
      console.log('EchoService.Echo', res.getMessage())
      this.echoRes = res.getMessage()
    })
  }

  streamingEcho(message: string, count: number, interval: number) {
    const request = new ServerStreamingEchoRequest()
    request.setMessage(message)
    request.setMessageCount(count)
    request.setMessageInterval(interval)
    this.echoStreamRes.splice(0, this.echoStreamRes.length)
    const client = grpc.invoke(EchoService.ServerStreamingEcho, {
      debug: true,
      request,
      host,
      onMessage: (message: ServerStreamingEchoResponse) => {
        console.log('EchoService.ServerStreamingEcho', message.getMessage())
        this.echoStreamRes.push(message.getMessage())
      },
      onEnd: (code) => {
      }
    })

  }
}
</script>
```

# まとめ

というわけでWebからgRPCの呼び出しができたしサーバではGoの実装をそのままGAE/Goの上に置くことができました！

しかしこれには問題があって、Streamに関しては全部送りきってから出ないとonMessageが呼ばれない状態です。
なぜそうなのかまではわかってませんが見た限りクライアントの実装の問題のような気もしますが引き続き調査中です。

今回ためしたgrpc-web-client以外に[grpc/grpc-web](https://github.com/grpc/grpc-web)という公式のブラウザサポートも開発中のようなのでこっちも合わせて試します。
パッと見た限りgrpc/grpc-webの方はgatewayのようなものです。
将来的にはgatewayではなくするようです。
ただgrpc/grpc-webのjs clientを使えばStreamがリアルタイムにやりとりできないかなと思ってるのであとで試します。