前から単語は知りつつも、何ができるのか、どう実装するのかよく分かっていなかった gRPC
を、実際にコードを書いて理解しようと足掻いた記事になります。
gRPC 及び RPC の概要説明はすでに先人たちが素晴らしい記事を投稿してくださっているので、ここでは多言語間での実装によって、雰囲気をやんわり味わいたい方にご覧いただければ幸いです。
実装するもの
ブラウザから、記事の投稿、閲覧ができる機能を作成します。
多言語対応による恩恵を受けるため、 Server 側 を Go、 Client 側 を Node.js で実装してみます。
作成したプロジェクトは Dockernize し、https://github.com/endotakuya/grpc-example にあげています。
適宜ご確認いただければ幸いです。
開発の流れ
1. .proto ファイル の生成
実装するメソッドは2つ。
Get/Post 辺りを試したかったので、 記事を1件取得する First
と 記事を投稿する Post
です。
syntax = "proto3";
package article;
service ArticleService {
rpc First (Empty) returns (Article) {}
rpc Post (Article) returns (Empty) {}
}
message Empty {}
message Article {
int32 id = 1;
string title = 2;
string content = 3;
enum Status {
DRAFT = 0;
PUBLISH = 1;
}
Status status = 4;
}
最低限の記事を構成するフィールドを Article
に定義していきます。
一番右の数字部分はタグと呼ばれるものらしく、 message 内 でユニークになる必要があるらしいです。
引数なしの場合は、上記のようなフィールドが無い Empty
メッセージを作るか、以下のようのな形にすることも可能です。
import "google/protobuf/Empty.proto";
...
service ArticleService {
rpc First (google.protobuf.Empty) returns (Article) {}
...
}
ただ、 google.protobuf.Empty
が JS 側で使えなかったことと、コンパイルで吐き出されるコードに違いがなかったため、 Empty
を定義しました。
2. .proto をコンパイル
サーバーとクライアントのひな形になるコード(インターフェイス)を生成します。
今回は、Go用に吐き出します。
$ protoc article/article.proto --go_out=plugins=grpc:.
以降は生成した article.pb.go
を元に実装を進めていきます。
3. サーバー側の実装
準備として、以下の package を go get
しておきましょう。
RUN go get -u \
google.golang.org/grpc \
github.com/golang/protobuf/protoc-gen-go
次に article.proto
に定義した、First
メソッド、 Post
メソッドを実装していきます。
article.pb.go
の ArticleServiceServer
を確認しましょう。
type ArticleServiceServer interface {
First(context.Context, *Empty) (*Article, error)
Post(context.Context, *Article) (*Empty, error)
}
後は純粋に中身を書いていけばOKです。
(エラーハンドリングされていない部分は気にせず…)
...
type server struct{}
func (s *server) First(ctx context.Context, in *pb.Empty) (*pb.Article, error) {
db, _ := initDb()
defer db.Close()
article := pb.Article{}
dist := []interface{}{&article.Id, &article.Title, &article.Content, &article.Status}
err := db.QueryRow("SELECT * FROM articles ORDER BY id DESC LIMIT 1").Scan(dist...)
return &article, err
}
func (s *server) Post(ctx context.Context, in *pb.Article) (*pb.Empty, error) {
db, _ := initDb()
defer db.Close()
stmtIns, err := db.Prepare(fmt.Sprintf("INSERT INTO %s (title, content, status) VALUES (?, ?, ?)", "articles"))
defer stmtIns.Close()
_, err = stmtIns.Exec(in.Title, in.Content, in.Status)
return &pb.Empty{}, err
}
...
なんてことはないと思います。articles
テーブルに対して、クエリを叩きます。
go run server.go
で起動させて、Server 側の実装は終了です。
4. クライアント側の実装
せっかく gRPC を使用しているので、クライアントは Node.js で実装します。
grpc-caller を使うと、直接 proto ファイルからインターフェイスを参照して実装できるみたいです。
Yarn で入れた package は以下のとおりです。
{
"dependencies": {
"body-parser": "^1.18.3",
"ejs": "^2.6.1",
"express": "^4.16.4",
"grpc": "^1.16.1",
"grpc-caller": "^0.11.0"
}
}
First
メソッドを叩いて、記事を1件取得してみます。
const PROTO_PATH = __dirname + '/../article/article.proto';
const SERVER_HOST = process.env.SERVER_HOST || 'localhost';
const client = caller(`${SERVER_HOST}:50051`, PROTO_PATH, 'ArticleService');
...
app.get('/', (req, res) => {
client.first({}, (err, response) => {
res.send(response);
});
});
client.first()
部分が実際にメソッドを叩いている部分ですね。
すでに以下のようなデータが入っているとします。
+----+-----------------+--------------------------------+--------+
| id | title | content | status |
+----+-----------------+--------------------------------+--------+
| 3 | お腹空いた | 肉が食べたい | 1 |
+----+-----------------+--------------------------------+--------+
Go 側のサーバーが立ち上がっていることを確認し、 express を起動させます。
$ node app.js
Listening on port 3000
実際に http://localhost:3000 へアクセスすると、以下のようなレスポンスが来ます。
実際に SQL を叩いているのは Go 側ですが、同じインターフェイスによって JS 側でレスポンスを受け取ることが出来ました。
同じように、ブラウザ上のフォームから値を入力して、記事を投稿します。
app.post('/new', (req, res) => {
let data = {
title: req.body.title,
content: req.body.content,
status: parseInt(req.body.status, 10)
};
client.post(data, (err, response) => {
// ...
});
});
client.post()
の引数として、 データを投げてあげればOKです。
http://localhost:3000/new で、下図のフォームから送信してあげると、Go 側のサーバーで DB に Insert されます。
gRPC のデバッグ
いちいち Client を実装するのも面倒ですよね。
そんな時は、grpc_cli というツールを使うと、その名の通り CLI 上から gRPC によるサーバーのあれやこれを確認することができます。
ls
gRPC で動いている Service 一覧
$ grpc_cli ls localhost:50051
article.ArticleService
grpc.reflection.v1alpha.ServerReflection
type
特定の message の定義を取得
$ grpc_cli type localhost:50051 article.Article
message Article {
enum Status {
DRAFT = 0;
PUBLISH = 1;
}
int32 id = 1[json_name = "id"];
string title = 2[json_name = "title"];
string content = 3[json_name = "content"];
.article.Article.Status status = 4[json_name = "status"];
}
call
実際にメソッドを呼ぶ
$ grpc_cli call localhost:50051 ArticleService.First ''
connecting to localhost:50051
id: 3
title: "\343\201\212\350\205\271\347\251\272\343\201\204\343\201\237"
content: "\350\202\211\343\201\214\351\243\237\343\201\271\343\201\237\343\201\204"
status: PUBLISH
Rpc succeeded with OK status
まとめ
RPC/gRPC に関連する情報は多く、和訳されているものもいくつかあったので、「なんとなくこういうものなんだ」と理解するのはそこまで難しくありませんでした。
ただ、実際に実装してみた結果、比較的大きなシステムに組み込む場合の共通のインターフェイスの設計や、細かいところでいくと .proto の分割粒度、多言語間での型の扱いなど、まだまだ試してみないと分からない部分も多かったので、実践的に導入しつつ検証してみようと思います。