この記事は自分がgRPC-webについて学ぼうと思い、gRPC-Web Hello World Guideを参考に手を動かした時のことをまとめました。
はじめにざっくりとgRPC-webとは何かを説明したあと、触ってみてハマったところなどを書いていきたいと思います。
まだ理解できていないところも多いため訂正や補足ありましたら、コメントいただけると幸いです。
gRPC-webとは
webクライアントがHTTPサーバーを経由することなく、gRPCバックエンドサービスと直接通信できるようにするJavaScriptクライアントライブラリです。
クライアント側とサーバー側のデータ型とサービスインターフェイスをProtocol Bufferで定義することができ、end-to-endのgRPCアプリケーションアーキテクチャを簡単に構築できることがgRPC-Webを使用することの大きな利点の一つになります。
クライアントはバックエンドに対して直接呼び出すことはできず、クライアント呼び出しをgRPCに適した呼び出しに変換する必要があり、その役割はEnvoyがクライアントによって生成されたHTTP/1.1呼び出しをバックエンドのサービスで処理できるHTTP/2呼び出しに変換することで実現しています。
触ってみる
Protocol Bufferを定義する
HelloRequestを渡すとHelloResponseを返すrpcメソッドSayHelloをもつサービスGreeterを定義します。
syntax="proto3";packagehelloworld;serviceGreeter{rpcSayHello(HelloRequest)returns(HelloReply);}messageHelloRequest{stringname=1;}messageHelloReply{stringmessage=1;}
サービスを実装する
クライアントからリクエストを受け取り、それを元にレスポンスを作成してクライアントに送り返します。
varPROTO_PATH=__dirname+'/helloworld.proto';vargrpc=require('@grpc/grpc-js');varprotoLoader=require('@grpc/proto-loader');varpackageDefinition=protoLoader.loadSync(PROTO_PATH,{keepCase:true,longs:String,enums:String,defaults:true,oneofs:true});varprotoDescriptor=grpc.loadPackageDefinition(packageDefinition);varhelloworld=protoDescriptor.helloworld;functiondoSayHello(call,callback){callback(null,{message:'Hello! '+call.request.name});}functiongetServer(){varserver=newgrpc.Server();server.addService(helloworld.Greeter.service,{sayHello:doSayHello,});returnserver;}if(require.main===module){varserver=getServer();server.bindAsync('0.0.0.0:9090',grpc.ServerCredentials.createInsecure(),(err,port)=>{assert.ifError(err);server.start();});}exports.getServer=getServer;
クライアントコードを書く
クライアント用のjsファイルを以下コマンドで生成します。
import_styleにtypescriptを指定すれば、typescriptで出力できます。
$protoc -I=. helloworld.proto \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
生成したファイルを元にclient.jsを実装します。
const{HelloRequest,HelloReply}=require('./helloworld_pb.js');const{GreeterClient}=require('./helloworld_grpc_web_pb.js');varclient=newGreeterClient('http://localhost:8080');varrequest=newHelloRequest();request.setName('World');client.sayHello(request,{},(err,response)=>{console.log(response.getMessage());});
package.json
package.jsonを作成します。
{"name":"grpc-web-simple-example","version":"0.1.0","description":"gRPC-Web simple example","main":"server.js","devDependencies":{"@grpc/grpc-js":"~1.0.5","@grpc/proto-loader":"~0.5.4","async":"~1.5.2","google-protobuf":"~3.14.0","grpc-web":"~1.2.1","lodash":"~4.17.0","webpack":"~4.43.0","webpack-cli":"~3.3.11"},"scripts":{"build":"webpack client.js --mode development"}}
index.html
簡単なHTMLを実装します。
<!DOCTYPE html><htmllang="en"><head><metacharset="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>
プロキシの設定ファイル
Envoyの設定ファイルを記述します。
admin:access_log_path:/tmp/admin_access.logaddress:socket_address:{address:0.0.0.0,port_value:9901}static_resources:listeners:-name:listener_0address:socket_address:{address:0.0.0.0,port_value:8080}filter_chains:-filters:-name:envoy.filters.network.http_connection_managertyped_config:"@type":type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManagercodec_type:autostat_prefix:ingress_httproute_config:name:local_routevirtual_hosts:-name:local_servicedomains:["*"]routes:-match:{prefix:"/"}route:cluster:greeter_servicemax_grpc_timeout:0scors:allow_origin_string_match:-prefix:"*"allow_methods:GET, PUT, DELETE, POST, OPTIONSallow_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-timeoutmax_age:"1728000"expose_headers:custom-header-1,grpc-status,grpc-messagehttp_filters:-name:envoy.filters.http.grpc_web-name:envoy.filters.http.cors-name:envoy.filters.http.routerclusters:-name:greeter_serviceconnect_timeout:0.25stype:logical_dnshttp2_protocol_options:{}lb_policy:round_robinload_assignment:cluster_name:cluster_0endpoints:-lb_endpoints:-endpoint:address:socket_address:address:host.docker.internalport_value:9090
クライアント用のjsコードをコンパイルする
npm install
npm run build
動かしてみる
gRPCサービスを立ち上げます。
$ node server.js
次にEnvoyを立ち上げます。
上記のenvoy.yml
でポート:8080でブラウザからのリクエストを受け取り、ポート:9090に転送するようにEnvoyを構成しています。
docker run -d -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro \
-p 8080:8080 -p 9901:9901 envoyproxy/envoy:v1.16.1
webサーバーを立ち上げます
python3 -m http.server 8081
全て正常に起動させた後、localhost:8081にアクセスすると、
devツールのconsoleにhello world
が表示されます。
ハマったところ
公式のサンプルのRead.meに書いてあるenvoy.ymlだとバックエンドまでリクエストが行かず、consoleに何も表示されない事象が発生しかなり時間を使ってしまいました。
原因
↓元のコード
static_resources:listeners:-name:listener_0address:socket_address:{address:0.0.0.0,port_value:8080}filter_chains:-filters:-name:envoy.http_connection_managerconfig:codec_type:autostat_prefix:ingress_httproute_config:name:local_routevirtual_hosts:-name:local_servicedomains:["*"]routes:-match:{prefix:"/"}route:cluster:greeter_servicemax_grpc_timeout:0scors:allow_origin_string_match:-prefix:"*"allow_methods:GET, PUT, DELETE, POST, OPTIONSallow_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-timeoutmax_age:"1728000"expose_headers:custom-header-1,grpc-status,grpc-messagehttp_filters:-name:envoy.grpc_web-name:envoy.cors-name:envoy.routerclusters:-name:greeter_serviceconnect_timeout:0.25stype:logical_dnshttp2_protocol_options:{}lb_policy:round_robinhosts:[{socket_address:{address:host.docker.internal,port_value:9090}}] // ←ここを修正
clustersのhostsフィールドにポートが指定されていたのですが、
Envoyのドキュメントを確認すると、hostsフィールドは非推奨になっており、load_assginmentフィールドに置き換えないといけなかったようでそこを修正したら正常にHello Wolrd
が表示されるようになりました。
まとめ
ただHello Worldするだけでしたが、理解が追いつかない部分が多く苦労しました。
それでもなんとなくですが概要は理解できるようになったと思います。
次はチャットアプリなどを作ってもっと理解を深めたいと思います。