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の定義
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のやりとりができます。
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
というメソッドで呼び出します。
<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がリアルタイムにやりとりできないかなと思ってるのであとで試します。