こちらを参考にgRPC-Webを使ってHello! Worldを表示するチュートリアルをやってみました。
参考:https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld
目次
全体構成
今回のgRPC-Webを使ったHello! Worldを表示するための全体構成はこののようになります。
gRPC-Webでは、gRPCの呼び出しに変換する必要があり、Envoyプロキシを通して実現しています。
参考:https://qiita.com/betchi/items/0e9040f21b1f8ecbf321
参考:https://blog.envoyproxy.io/envoy-and-grpc-web-a-fresh-new-alternative-to-rest-6504ce7eb880
参考:https://qiita.com/namusyaka/items/71cf27fd3242adbf348c
Protocol BuffersでgRPCサービスを定義する
ここではRPCメソッド、リクエスト、レスポンスを持つサービスを定義します。
・helloworld.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } |
Node.jsでgRPCサービスを実装
次に、Node.jsでgRPCサービスを実装します。ここでは、クライアントからリクエストを受け取り、call.request.nameを介してメッセージフィールドにアクセスできます。その後、レスポンスを作成し、クライアントに送り返します。
・server.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | const PROTO_PATH = __dirname + '/helloworld.proto'; const grpc = require('grpc'); const protoLoader = require('@grpc/proto-loader'); const packageDefinition = protoLoader.loadSync( PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true } ); const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); const helloworld = protoDescriptor.helloworld; function doSayHello(call, callback) { callback(null, { message: 'Hello! ' + call.request.name }); } function getServer() { const server = new grpc.Server(); server.addService(helloworld.Greeter.service, { sayHello: doSayHello, }); return server; } if (require.main === module) { const server = getServer(); server.bind('0.0.0.0:9090', grpc.ServerCredentials.createInsecure()); server.start(); } exports.getServer = getServer; |
Envoyプロキシの設定
次に、ブラウザのgRPC-Webリクエストをバックエンドに転送するようにEnvoyプロキシを構成する必要があります。ここでは、ポート:8080でリッスンするようにEnvoyを構成し、ポート:9090でクラスターにgRPC-Webリクエストを転送します。
・envoy.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: greeter_service max_grpc_timeout: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.grpc_web - name: envoy.cors - name: envoy.router clusters: - name: greeter_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin hosts: [{ socket_address: { address: localhost, port_value: 9090 }}] |
Envoy用Dockerfileの作成
Envoyを起動させるためにDockerfileを作成します。
・envoy.Dockerfile
1 2 3 | FROM envoyproxy/envoy:v1.14.1 COPY ./envoy.yaml /etc/envoy/envoy.yaml CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml |
クライアントコードの作成
HelloRequest、HelloReplyおよびGreeterClientクラスをインポートするファイルは、前に定義したprotocジェネレータによって生成されます(後で生成します)。
GreeterClientクラスをインスタンス化し、HelloRequestprotobufオブジェクトにフィールドを設定します。ファイルで定義したのと同じように、client.sayHello()を介してgRPC呼び出しを行うことができます。
・client.js
1 2 3 4 5 6 7 8 9 10 11 | const {HelloRequest, HelloReply} = require('./helloworld_pb.js'); const {GreeterClient} = require('./helloworld_grpc_web_pb.js'); var client = new GreeterClient('http://localhost:8080'); var request = new HelloRequest(); request.setName('World'); client.sayHello(request, {}, (err, response) => { console.log(response.getMessage()); }); |
package.jsonの作成
package.jsonを作成します。
・package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "name": "grpc-web-simple-example", "version": "0.1.0", "description": "gRPC-Web simple example", "devDependencies": { "@grpc/proto-loader": "~0.5.4", "async": "~1.5.2", "google-protobuf": "~3.12.0", "grpc": "~1.24.2", "grpc-web": "~1.1.0", "lodash": "~4.17.0", "webpack": "~4.43.0", "webpack-cli": "~3.3.11" } } |
index.htmlの作成
最後にindex.htmlを作成します。
・index.html
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>gRPC-Web Example</title> <script src="./dist/main.js"></script> </head> <body> <p>Open up the developer console and see the logs for the output.</p> </body> </html> |
最終的に作成・生成したファイルは以下のようになりました。
Protobufメッセージとクライアントサービススタブの生成
protocのインストール
こちらを参考にprotocをインストールします。
https://github.com/protocolbuffers/protobuf/releases
以下でインストールされたか確認します。
1 2 | $ which protoc /usr/local/bin/protoc |
protoc-gen-grpc-webのインストール
こちらからprotoc-gen-grpc-web-1.1.0-darwin-x86_64をダウンロードし、パスを通します。
https://github.com/grpc/grpc-web/releases
1 2 | $ sudo mv ~/Downloads/protoc-gen-grpc-web-1.1.0-darwin-x86_64 \ /usr/local/bin/protoc-gen-grpc-web |
1 | $ chmod +x /usr/local/bin/protoc-gen-grpc-web |
protocとprotoc-gen-grpc-webのインストールが完了したら、以下のコマンドでjsファイルを生成します。
1 2 3 | $ protoc -I=. helloworld.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. |
以下の2つのファイルが生成されたことを確認します。
- helloworld_pb.js: HelloRequestとHelloReplyクラスが含まれたファイル
- helloworld_grpc_web_pb.js: GreeterClientクラスが含まれたファイル
protoc-gen-grpc-web: program not found or is not executableのエラーが出た場合は、こちらを参考にしてください。
https://hsuzuki.hatenablog.com/entry/2018/10/30/184544
クライアントのjsコードをコンパイル
クラインとのjsコードをブラウザが認識できるようにコンパイルします。webpackでclient.jsがビルドされdist/main.jsが生成されます。
1 | $ npm install |
1 | $ npx webpack client.js --mode development |
実際に動かしてみる
以上でサンプルを動かす準備は整いました。gRPCサービス、Envoyプロキシ、クライアントをそれぞれ立ち上げます。
gRPCサービス(ポート:9090リッスン)を立ち上げます。
1 | $ node server.js |
次にEnvoyプロキシを実行します。このenvoy.yamlファイルは、ポート:8080でリッスンし、転送するようにEnvoyを構成します。
1 | $ docker build -t helloworld / envoy -f ./envoy.Dockerfile . |
1 | $ docker run -d -p 8080:8080 -p 9901:9901 helloworld / envoy |
最後にwebサーバを立ち上げます。
1 | $ python3 -m http.server 8081 |
全て正常に立ち上げたらlocalhost:8081にアクセスします。
デベロッパーツールからコンソールを立ち上げるとHello! Worldと表示されていることが確認できます。
client.jsのsetNameをTestにして再度ビルドするとちゃんとHello! Testに変わっていることが確認できます。
1 | request.setName('Test'); |
便利な拡張機能
こちらのChrome拡張機能を使うことでgRPCサーバーから返されたリクエスト、レスポンス、エラーオブジェクトのデシリアライズされたJSONが表示されるようになります。