JavaScript
Go
GoogleAppEngine
gRPC

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

More than 1 year has passed since last update.

grpc-web-client とは

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

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

今回試したコードはGitHubにあります。
次のコマンドでダウンロードできます。

$ 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の定義

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;
}

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

$ 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のやりとりができます。

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というメソッドで呼び出します。

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という公式のブラウザサポートも開発中のようなのでこっちも合わせて試します。
パッと見た限りgrpc/grpc-webの方はgatewayのようなものです。
将来的にはgatewayではなくするようです。
ただgrpc/grpc-webのjs clientを使えばStreamがリアルタイムにやりとりできないかなと思ってるのであとで試します。