こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター の山下です。 今回は、TypeScriptを使って、gRPCのアプリケーションを開発する際の方法について紹介します。
gRPCとProtocol Buffers
gRPCはGoogleの開発した様々な環境で動作するオープンソースのRPCフレームワークです。 gRPCは負荷分散、トレース、ヘルスチェック、認証などの機能をサポートし、効率的な通信の仕組みを提供しています。
gRPCは、Protocol Buffersというインターフェース記述言語(IDL)を用いてサービスを定義して利用します。このProtocol BuffersもgRPCと同じくGoogleが開発しています。gRPCでアプリケーションを開発する際は、まずProtocol Buffersから通信に関するプログラムを自動的に構築して開発することになります。
Protocol Buffersは多くの言語をサポートしています。今回利用するTypeScriptを直接サポートはしていませんが、Node.jsをサポートしているので問題なく利用できます。
詳細な情報はgRPCの公式サイトを参照してください。 https://grpc.io/
Protocol Buffersについての詳細な情報は以下のURLを参照してください。 https://developers.google.com/protocol-buffers
この記事では、Node.jsのサポートを利用してTypeScriptのアプリケーション開発の手順について解説します。
gRPCの通信方式
gRPCでは4種類の通信方式がある。
Unary RPC 1つのリクエストに対して一つのレスポンスを返す一般的な通信です。
Server streaming RPC クライアントから送られてきた一つのリクエストに対して、サーバは複数回に分けてレスポンスを返す通信方式です。
Client streaming RPC クライアントからリクエストを分割して送る方式でサーバーはすべてのリクエストを受け取ってからレスポンスを返す方式です。
Bidirectional streaming RPC サーバーとクライアントが一つのコネクションを確立しお互いに任意のタイミングでリクエストとレスポンスを送りあう通信方式です。
また、gRPCでは、ブラウザで利用するgRPC WebとHTTP/2を利用して通信を行うgRPC over HTTP/2の2種類の通信方式が存在しています。 gRPC over WEBはブラウザを中心に策定された仕様となっています。このため現在は、Client streaming RPC、Bidirectional streaming RPCを行うことが出来ません。
この記事では、このうちHTTP/2を▼使ったUnary RPCの開発手順について紹介します。
TypeScriptを用いたgRPCの開発手順
TypeScriptを用いたgRPCアプリケーションは以下のような流れで開発します。
- Protocol Buffersの定義を
proto
ファイルで行う protoc
プログラムを用いてproto
ファイルからソースコードを生成する- 生成したプログラムにロジックを追加して完成させる
という流れになります。 ここでは、その流れを順番に追ってみます。
Protocol Buffers でサービスとメッセージを記述する
Protocol Buffersではサービスとメソッド(rpc名と呼ぶ方が適切かもしれませんが、本記事ではメソッドで統一します)を定義します。メソッドがAPIの概念に近いものとなり、メソッドが通信でやりとりする内容をメッセージとして定義することとなります。 Protocol Buffersの例を以下に示します。なおProtocol Buffersにはバージョンがあり本記事ではバージョン3を想定しています。
syntax = "proto3"; message HelloRequest { string name = 1; } message HelloResponse { string result =1; } service Hello { rpc hello(HelloRequest) returns (HelloResponse); }
上記の定義では、Hello
というサービスと hello
メソッド、HelloRequest
と HelloResponse
というメッセージが定義されています。
。hello
を呼びだす場合には、HelloRequest
を引数として呼び出し、その返り値としてはHelloResponse
というメッセージが返ってくることを表しています。
なお、ここではProtocol Buffersの文法の詳細を述べません。詳細については、公式のドキュメントを参照してください。 https://developers.google.com/protocol-buffers/docs/proto3
記載する際は以下のスタイルガイドが参考になります。 https://developers.google.com/protocol-buffers/docs/style
スタイルガイドには以下のようにサービス名、メッセージ名とフィールド名についての命名の規則が記載されています。
Use CamelCase (with an initial capital) for message names – for example, SongServerRequest. Use underscore_separated_names for field names (including oneof field and extension names) – for example, song_name.
このガイドの内容に従わなくても問題はおきません。しかし、従っていない場合は生成されるソースコードが不自然なものとなってしまいます。
例えば、フィールド名をuserId
のような名前を付けた場合、これに対応するメソッドやプロパティの名前はgetUserid
、setUserid
といった形になってしまいます。
一方でuser_id
というフィールド名を用いた場合はgetUserId
、setUserId
という名前で生成されます。コードの読みやすさなどの観点からフィールド名にはスネークケースを用いる方が良さそうですね。
Protocol Buffersの更新について
Protocol Buffersのprotoファイルを更新して、メッセージのフォーマットを変更する場合には注意が必要となります。
特に、フィールド番号は安易に変更、削除してしまうと古いプログラムと新しいプログラムでメッセージのフォーマットが一致しなくなり通信できなくなってしまうので注意が必要です。
極力新しいフィールドを追加していく形で更新していく事が望ましいです。
また削除も可能であれば避けOBSOLETE_
といったプレフィックスをつけて残しておく事が望ましいです。
後々誤って削除したフィールド番号が再利用されてしまうといったトラブルを回避するためです。
また各種メッセージの種類には互換性があり、同じフィールド番号でもメッセージの種類の変更は可能な場合があります。
その他の注意点については公式のドキュメントを熟読し、慎重に更新を行っていく必要があるので注意してください。 https://developers.google.com/protocol-buffers/docs/proto3#updating
Protocol Buffersからソースコードを生成する
protocプログラムとそのオプションについて
Protocol Buffersからコードを生成するプログラム(protoc
)は、オプションの記述方法に注意が必要です。
protoc
を利用する際は、xxx_out=....
というオプションが並ぶことになります。
このオプションは、以下のように解釈します。
--xxx_out
はプラグインと出力先の指定を意味します。これは、proto-gen-xxx
というプラグイン名の場合は、xxx_out
という対応関係になっています。
proto-gen-go
プラグインなら--go_out
。proto-gen-grpc-gateway
プラグインなら--grpc-gateway_out
という具合になっています。
プラグイン自体もオプションも同時に指定することが出来て、--xxx_out=プラグインのオプション:出力先
という形になります。
例えば、--js_out=import_style=commonjs,binary:${PROTO_DEST}
と書いてある場合を考えます。
これは、js_out
プラグインにimport_style=commonjs,binary(=true)
という引数を渡すことになります。そして、その出力先は${PROTO_DEST}
ということです。
TypeScript用のprotocについて
protoc
は Node.js に対応していますが、TypeScriptの型定義などを直接生成する機能は持っていません。
今回は、grpc_tools_node_protoc_ts
を用いてprotoc
の生成した Node.js のソースコードに型定義を自動生成して利用します。これは、grpc-tools
というgRPC公式に含まれているgrpc_tools_node_protoc
というツールを拡張したものです。
以下が公式ドキュメントとその使い方です。 https://github.com/agreatfool/grpc_tools_node_protoc_ts#how-to-use
# ソースコードの出力先 PROTO_DEST=./src/proto # protoファイルが置いてあるディレクトリ PROTO_DIR=./proto # Protocol BuffersからnodeのgRPCコードを自動生成 grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=${PROTO_DEST} \ --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* # typescript用の型定義を作成 grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=${PROTO_DEST} \ -I ${PROTO_SRC} \ ${PROTO_SRC}/*
もしくは以下のように一括で生成する方法もあります。
# 定義と実装を同時に生成 yarn run grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=grpc_js:${PROTO_DEST} \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=grpc_js:${PROTO_DEST} \ -I ${PROTO_DIR} \ ${PROTO_DIR}/*.proto
生成されたプログラムを利用してみる
Hello
サービスの簡単なサーバ、クライアントの実装を以下に示します。
通信に関わる処理は全てgRPC側が行ってくれているため、実際に記述する部分はロジックに対応する部分だけとなります。通信に用いるメッセージを組み立てる関数も自動生成されているので、それを利用して構築できます。
// サーバプログラム import * as grpc from '@grpc/grpc-js'; import { sendUnaryData } from '@grpc/grpc-js/build/src/server-call'; import { HelloRequest, HelloResponse } from '../proto/hello_pb'; import { HelloService } from '../proto/hello_grpc_pb'; const HelloServer = { hello: (call: grpc.ServerUnaryCall<HelloRequest, HelloResponse>, callback: sendUnaryData<HelloResponse>): void => { const request = call.request; const response = new HelloResponse(); console.log("Message from client"); response.setResult("Hello," + request.getName()) callback(null, response); } } function serve(): void { const server = new grpc.Server(); server.addService(HelloService, HelloServer); server.bindAsync(`localhost:6543`, grpc.ServerCredentials.createInsecure(), (err, port) => { if (err) { throw err; } console.log(`Listening on ${port}`); server.start(); }); } serve();
// クライアントプログラム import * as grpc from '@grpc/grpc-js'; import { HelloClient } from '../proto/hello_grpc_pb'; import { HelloRequest, HelloResponse } from '../proto/hello_pb'; function hello(): Promise<HelloResponse> { const client = new HelloClient( `localhost:6543`, grpc.credentials.createInsecure(), ); // HelloRequestを作るためのクラス、メソッドが用意されているのでそれを用いてメッセージを作成する const request = new HelloRequest(); request.setName("ISID"); // サーバに対してサービスの実行を要求する return new Promise<HelloResponse>((resolve, reject) => { console.log("Send Hello Message"); client.hello(request, (err, response) => { if (err) { return reject(err); } // ここで結果を受け取っている console.log("Receive Message"); return resolve(response); }); }); } (async () => { console.log("Client Start"); const result = await hello(); console.log(result.getResult()); })();
実際のプロジェクト構成と動作例
ここでは、今回のサンプルで利用したプロジェクトの構成を紹介します。
プロジェクト構成例
ディレクトリの構成は以下のような構成です。
. ├── package.json ├── proto │ └── hello.proto ├── scripts │ └── build-protos.sh ├── src │ ├── client │ │ └── index.ts │ ├── proto │ └── server │ └── index.ts └── tsconfig.json
また、package.jsonの中身は以下のような内容です。
{ "name": "grpc-hello", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { }, "scripts": { "lint": "yarn run eslint --fix --ext .ts src", "clean": "rm -rf ./dist && rm -rf ./src/proto && mkdir -p ./src/proto ", "build": "sh ./scripts/build-protos.sh ./hello.proto ./src/proto && yarn run tsc; cp -r ./src/proto ./dist/src/proto", }, "devDependencies": { "@grpc/grpc-js": "^1.4.2", "@types/eslint": "^7.28.2", "@types/google-protobuf": "^3.15.5", "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", "eslint": "^8.1.0", "grpc-tools": "^1.11.2", "grpc_tools_node_protoc_ts": "^5.3.2", "ts-node": "^10.4.0", "tsconfig-paths": "^3.11.0", "typescript": "^4.4.4" } }
動作確認
まず yarn build
で Protocol Buffers からコード生成、TypeScriptからJavaScriptへのトランスパイルを実行します。
$ yarn $ yarn build
サーバの起動
$ node ./dist/src/server/ Listening on 6543
クライアントの実行
$ node ./dist/src/client Client Start Send Hello Message Receive Message Hello,ISID
サーバー側のログも確認すると
Message from client
というようなログが出ています。 gRPCを使った通信プログラムが実装できてそうです。
まとめ
今回は、gRPCを用いたアプリケーション開発の方法やその注意点について紹介しました。 gRPCは Protocol Buffers を定義するだけで高品質な通信プログラムが実装できる素晴しい技術ですね。 機会があれば積極的に利用していきたいと思います。
私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。
執筆:@yamashita.tsuyoshi、レビュー:@handa.kenta (Shodoで執筆されました)