grpc-webを触ってみた

webブラウザJavaScriptからgRPCができるgRPC-webが先日リリースされました。実際に実行するまでの流れや自動生成されたコードをどのように使うのかなど知りたかったので、そのgRPC-webを実際に試してみました。

github.com 

試したこと

  • リポジトリ内にあるexampleをもとに、実際にprotoファイルからgRPC-webでコードを生成し実行
  • gRPC-webでTypeScriptを生成して同様に実行
  • バンドルしたファイルの大きさの確認 

hello world

公式のリポジトリ内にexampleとして用意されている helloworldから試していきます。

helloworldの構成としては次のような形になっています。

webブラウザ(grpc-web) --- envoy --- grpcサーバ(nodejs)

README.mdにあるように実行までは次の流れです。

  1.  makeコマンドからprotoc-gen-grpc-webをインストールして、protoファイルからjsのコードを生成
  2. node server.js でnodeのgRPCサーバを立ち上げる.
  3. dockerを使って、envoyを立ち上げる
  4. webpackで、クライアントサイドjsをバンドルし、1つにまとめる
  5. webサーバを立ち上げて、index.htmlとmain.jsを読み込む  

ただこの流れで試したみたのですが、自分の環境だとエラーが発生し動作を確認できませんでした。
エラーの原因は、envoyが意図通りに実行できていないというものでした。

 

envoyののdocker起動は↓のようになっていました。

$ docker run -d -p 8080:8080 --network=host helloworld/envoy

ただ、自分の環境で--network=hostの設定をつけようとすると、

WARNING: Published ports are discarded when using host network mode

のようにwarningが発生しポートフォワーディングが設定できなかったというものです。--network=host を指定しなければ、正しくポート8080で受け付けられる状態で起動はすることはできました。

これで大丈夫かと思ったのですが、まだ動きませんでした。

このサンプルではenvoyだけがdockerで他はそうではない状態です。そしてenvoyの設定ファイルのenvoy.yamlには、gRPCのnodeサーバへのプロキシ部分がlocalhost:9090のように記述されていたため、ただenvoyのコンテナが立ち上がっても、envoyからみるとgRPCサーバはlocalhost:9090ではないため正しく実行できなかったようです。--network=hostの状態で動作させることができれば、このままで大丈夫だとは思います。

なので対応として、gRPCのnodeサーバもdockerで起動してenvoyとnodeサーバ間で通信できるようにしてみました。

webブラウザ(grpc-web) -- envoy(docker) -- gRPC nodeサーバ(docker)

これでようやくブラウザから通信が成功し、一連の動作を確認できるようになりました。エラーになっていなかったクライアント部分はdocker化せずにそのままで試しています


実行すると次のようなリクエストをdeveloper consoleで確認することができました。

f:id:terachanple:20181031213556p:plain

main.js以下の3つのリクエストがgrpc-webでのリクエストです。

またコンソール上に出力された結果としてはこのようになりました。

f:id:terachanple:20181031213511p:plain

 TypeScript化

次にgrpc-webではTypeScriptにも出力できるようなので、そちらも試してみました。ただTypeScriptのサポートはまだ実験段階のようです。
TypeScriptへの出力は、次のようなコマンドで生成されます。

protoc -I=. helloworld.proto \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=typescript,mode=grpcwebtext:.

このときに出力されるファイルは次の3つです

  • helloworld_pb.js
  • helloworld_pb.d.ts
  • HelloworldServiceClientPb.ts

全てがTypeScriptというわけではなく、一部jsファイルも含まれている状態です。示したコマンドにあるように --js_out=import_style= にtypescriptを指定できませんでした。 (指定した場合でもエラーにはならずファイルは出力されましたが、そTypeScriptではなく HelloRequestのようなリクエストやレスポンスの実体部分が、hellorequest.jsのように別々のjsファイルのとして出力されました)

typescriptを指定できなかったですが、--js_out=のオプションはつけないとhelloworld_pb.d.tsHelloworldServiceClientPb.tsだけが生成されることになり、リクエストやレスポンスの定義ファイルだけで実体がない状態になってしまい、実行できません。なのでtypescriptだけではなく一部jsを含む形になりました。

全てがtypescriptというわけではないですが型定義はあるのと、jsファイルも自動生成されたもので編集はしないものなので、コードを書くときには気にしなくとも大丈夫な部分ものだとは思います。

 

protoファイルからtypescriptでの生成もできたので、clientのjsをtypescriptへ書き換え、jsのときと同様の動作も確認できたのでtypescriptへの書き換えもうまくいきました。このときtypescriptをwebpackで扱えるようにするため、ts-loader, typescript をnpm installして追加しています。

typescriptで書いたクライアントのコードは次の通りです。

import * as grpcWeb from 'grpc-web';
import { GreeterClient } from './HelloworldServiceClientPb';
import { HelloRequest, RepeatHelloRequest, HelloReply} from './helloworld_pb';

class HelloworldApp {
  greeterService: GreeterClient;

  constructor(greeterService: GreeterClient) {
    this.greeterService = greeterService;
  }

  sayHelloworld(name: string) {
    const request = new HelloRequest();
    request.setName(name);

    this.greeterService.sayHello(request, {}, (err, response) => {
      console.log(response.getMessage());
    });
  }

  streamRequest(name: string, count: number) {
    const streamRequest = new RepeatHelloRequest();
    streamRequest.setName(name);
    streamRequest.setCount(count);

    const stream = this.greeterService.sayRepeatHello(streamRequest, {});
    stream.on('data', (response: HelloReply) => {
      console.log(response.getMessage());
    });
  }

  sayHelloAfterDelay(name: string, delay: number) {
    const request = new HelloRequest();
    request.setName(name);

    const deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + delay);

    this.greeterService.sayHelloAfterDelay(request, {deadline: deadline.getTime().toString()}, (err, response) => {
      console.log('Got error, code = ' + err.code + ', message = ' + err.message);
    });
  }
}

const greeterService = new GreeterClient('http://localhost:8080', null, null);
const app = new HelloworldApp(greeterService);
app.sayHelloworld('world');
app.streamRequest('world', 5);
app.sayHelloAfterDelay('world', 1);

バンドルされたファイルサイズを確認

最後webpackで生成されたmain.jsのファイルサイズがどれくらいになっているのかも確認してみました。 (確認にはtypescirptに書き換えたものを使っています)
特に最適化などは何も行っていない状態で、生成されたファイルは確認する403KBと、400KBを超えていました。

何が大きいのか確認していきます。どのファイルが大きいのかを調べるときにはwebpack-bundle-size-analyzer を使いました。
結果は↓のような内訳になっていました。

$ npx webpack -p client.ts --json | webpack-bundle-size-analyzer
grpc-web: 217.97 KB (48.8%)
google-protobuf: 156.66 KB (35.1%)
buffer: 47.47 KB (10.6%)
base64-js: 3.85 KB (0.862%)
ieee754: 2.02 KB (0.452%)
webpack: 489 B (0.107%)
isarray: 132 B (0.0289%)
<self>: 17.85 KB (4.00%)

grpc-webとgoogle-protobufで8割以上占めている状態です。書き方によってはもっと小さくできるのかもしれないですが、今回の書き方だとこのような結果になりました。
また参考までに webpackのoptimization.splitChunks を設定してnode_moduleだけをvendor.jsとしてまとめ、それ以外をmain.jsとした場合それぞれのファイルサイズは次のようになりました。

8.3K main.js
395K vendors.js

gRPC-webを使ってみて

先日リリースされたgrpc-webを使ってjsとtypescriptでwebブラウザからgRPCを試してみましたが、protoというサーバとクライアントで共通のものから自動生成できるのはやはり便利ですし、実装も楽でした。
今回は簡単な実装のみだったのでReactやVueなども使ったアプリケーションを作って見ようかと思ってます。
ファイルサイズの規模感については正直まだわかっていないところですが、生成されたファイルは大きそうな印象ではあるので、ファイルサイズについてもまたどこかで調べてみようと思います。