gRPC入門〜特徴の整理とTypeScriptで動かしてみるところまで〜
はじめに
gRPCとは?
クライアント・サーバ間やマイクロサービス間の通信に使用されるプロトコルで、以下のような特徴があります。
Protocol Buffersをベースとしたスキーマ駆動開発およびコードの自動生成
- gRPCではProtocol Buffersと呼ばれる言語でインターフェース仕様を記述します。これさえ記述すれば、さまざまなプログラミング言語で使用できるコード(主にインターフェースの型定義)を自動生成することができるので、サーバ、クライアントともにインターフェースの部分に悩むことがなくなり、ビジネスロジックの開発に集中できます。REST APIでもサードパーティのライブラリ(例えばSwagger+各言語のライブラリ)を使えばスキーマ駆動開発はできますが、手間がかかったり、強制力はないため、上記のようなやり方を知りながらも、クライアント・サーバそれぞれで型定義を作っているプロジェクトは多いのではないかと思います。一方でgRPCの場合は、標準でコードの自動生成機能が搭載されているので、徹底したスキーマ駆動開発ができる点が特徴です。
高効率で高速な通信
- gRPCで使うProtocol Buffersでは、メッセージをJSONなどのテキスト形式ではなく、軽量化したバイナリ形式でやりとりします(Protocol Buffersがバイナリのシリアライズ・デシリアライズをやってくれるので、実装の段階でこれを意識することはありません)。また、HTTP/2をベースにしているので、HTTP/2が持つストリーミングの機能を使えます(今回の記事では紹介しませんが、Protocol Buffersを定義する時に、ストリーミングの形式であることを明示する必要はあります)。ストリーミングについて雑に説明すると、「HTTP/1.1ではリクエスト・レスポンス型の通信モデルのため、1回の接続で1つずつ処理しなければならなかったが、HTTP/2のストリーミングを使うと1回の接続で複数のリクエスト、レスポンスをやりとりできるようになる」といったイメージです。
関数のように自由にインターフェースを定義できる
- RESTのAPIの場合、基本的にリソース名とHTTPメソッドでインターフェースを表現する形式であるため、CRUDのような単純なAPIであれば表現しやすいですが、その範疇を越えるAPIを作ろうとした時に、命名に悩むケースは多いです。一方で、gRPCには良くも悪くもそういった制約がないので、関数を作るかのように、自由にインターフェースを命名することが可能です。
Webでの使用に制限がある
- ブラウザとの通信にgRPCを使いたい場合、gRPC-Webという別の技術を使う必要があるようです。ただ、これをやろうと思うと、プロキシサーバなどを立てる必要があるようです(ここに関しては正直理解があやふやです)。
一方で、下記で紹介するConnectでは、Reactなどとセットで使われているケースがみられるので、こういったサードパーティのライブラリを使うことで、徐々にWebでも使える状態になっていっているようです。
使用するツール
gRPCの特徴がわかったところで、実際に動くものを作っていきますが、まずは使用するツールについて記載します。
Buf CLI
gRPCでの開発を包括的に支援するツールです。
Protocol Buffersの定義ファイルをコードに変換するツールとしては、protoc
というものもありますが、調べていると、protoc
からBuf CLI
に移行した、という記事がよくヒットしたので、今はこちらのツールの方が勢いがあるのかなというイメージです。
以下の機能を持ちます。
- コード自動生成
- lint、format
- gRPCのコマンドライン実行(curlコマンド的なことができる)
以下でインストールできます。
brew install bufbuild/buf/buf
Connect
上記のBuf CLIを提供しているBuf Technologies社が開発したgRPCのフレームワークです。
複数の言語に対応していて、Node.js、Go、Web(JavaScript)、Kotolin、Swiftに今のところ対応しているみたいです。
Connectは、gRPCとgRPC-Webだけでなく、ブラウザ上でも通信できるConnectという独自のプロトコルもサポートしているようですが、本記事ではgRPCのプロトコルを使用します。
実際に構築する
今回は例として、クライアントから渡された二つの数を足し算するアプリを作っていきます。
ディレクトリ構成は以下です。
※genディレクトリには自動生成されるファイルが入ります。
.
├── buf.gen.yaml
├── buf.work.yaml
├── proto
│ ├── buf.yaml
│ └── calc
│ └── v1
│ └── calc.proto
└── src
├── calc-service.ts
├── client.ts
├── gen
│ └── calc
│ └── v1
│ ├── calc_connect.ts
│ └── calc_pb.ts
└── server.ts
Protocol Buffers(proto)ファイルの定義
RPCの一連のメソッドを集めたservice、リクエスト・レスポンスの型であるmessageを定義していきます。
messageの右側の数字は、タグナンバーというもので、同じメッセージの中で一意である必要があります。
syntax = "proto3";
package calc.v1;
import "google/protobuf/timestamp.proto";
service CalcService {
rpc Add(AddRequest) returns (AddResponse);
}
message AddRequest {
int32 a = 1;
int32 b = 2;
}
message AddResponse {
int32 result = 1;
google.protobuf.Timestamp responded_at = 2;
}
また、プロジェクトルートに以下のbuf.work.yaml
を配置して、protoディレクトリ以下にprotoファイルがあることを記載します。
version: v1
directories:
- proto
コードの自動生成
リポジトリ直下に、buf.gen.yaml
を用意します。
version: v1
plugins:
- plugin: buf.build/bufbuild/es
out: ./src/gen
opt: target=ts
- plugin: buf.build/bufbuild/connect-es
out: ./src/gen
opt: target=ts
ここでは、2つのプラグインを使っています。
- protoc-gen-es:messageの型に関するTypeScriptのコードを生成。
- protoc-gen-connect-es:Connect用の、serviceの定義となるTypeScriptのコードを生成。
ここまでできたら、以下コマンドを打つことでコードを自動生成できます。
buf generate
サーバの作成
前述した通り、Connectはさまざまな言語に対応していますが、今回はNode.jsを使っていきます。
公式サイトを見ると、素のNode.jsに加えて、Next.js、Express、Fastifyなどのフレームワークに対するプラグインが提供されているようですが、この記事では公式にもチュートリアルが載っているfastifyを使います。
まずは、必要なパッケージをインストールします。
npm i fastify @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-fastify
サーバのコードを書いていきます。
import { ConnectRouter } from "@connectrpc/connect";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import fastify from "fastify";
import { calcServiceImpl } from "./calc-service";
import { CalcService } from "./gen/calc/v1/calc_connect";
async function main() {
const server = fastify({ http2: true });
// サービス定義と実装を紐づける
const routes = (router: ConnectRouter) => {
router.service(CalcService, calcServiceImpl);
};
await server.register(fastifyConnectPlugin, { routes });
// サーバの起動
await server.listen({ host: "localhost", port: 8080 });
console.log("server is listening at", server.addresses());
}
main();
import { ServiceImpl } from "@connectrpc/connect";
import { CalcService } from "./gen/calc/v1/calc_connect";
export const calcServiceImpl: ServiceImpl<typeof CalcService> = {
add(req, ctx) {
return {
result: req.a + req.b,
};
},
};
以下コマンドでサーバを起動できます。
npx tsx src/server.ts
以下コマンドで、curlっぽいことができます。
buf curl --protocol grpc --http2-prior-knowledge --schema ./proto --data '{"a": 10, "b": 11}' http://localhost:8080/calc.v1.CalcService/Add
以下が返ってくると成功です。
{
"result": 21
}
クライアントの作成
Node.jsを使ってクライアントを作成していきます。
まずは、必要なパッケージのインストールから行います。
npm i @connectrpc/connect-node
続いて、コードを書いていきます。
import { createPromiseClient } from "@connectrpc/connect";
import { createGrpcTransport } from "@connectrpc/connect-node";
import { CalcService } from "./gen/calc/v1/calc_connect";
async function main() {
const client = createPromiseClient(
CalcService,
createGrpcTransport({
httpVersion: "2",
baseUrl: "http://localhost:8080",
})
);
const res = await client.add({ a: 1, b: 2 });
console.log(`addのレスポンス:${res.result}`);
}
main();
以下コマンドで実行できます。
npx tsx src/client.ts
以下のように書き変えることで、ブラウザからも実行できるようです。
まとめ
この記事では、gRPCの特徴と実際に簡単なプログラムを動かして作ってみるところまで紹介しました。
gRPCの特徴で記載した内容は、絶対gRPCでなければならない理由にはなりにくいかもしれませんが、gRPCの方がベターだよね、というケースは結構ありそうなので、実際に使ってみつつ、引き続き勉強していきたいです。
Discussion