この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Introduction
RPCは昔からあるクライアント−サーバー間の通信手法です。
サーバで実装されている関数をクライアントから呼んで実行します。
最近ではHTTP/HTTPSでクラサバ間通信をして、
フォーマットにXML(XML-RPC)やJSON(JSON-RPC)を利用するのが
多いようです。
(直近で私は使った記憶がないですが)
上記のRPCは各技術がメジャーなので採用しやすいですが、
パフォーマンスがそこまで高くなかったり
バイナリデータが扱いにくかったりします。
そういった問題点を解決するために開発されたのが、
Google発のRPCであるgRPCです。
gRPC?
gRPCはハイパフォーマンスなオープンソースのRPCフレームワークです。
Googleが開発したRPC技術(Stubby)が元となって開発され、オープンソース化されました。
現在はCNCFによって開発が進められています。
gRPCでは、トランスポートにHTTP/2を使い、
データのシリアライズにProtobuf(Protocol Buffers)を使います。
Protocol BuffersはGoogle発のデータフォーマットで、
データを効率よく(バイナリデータも含む)扱うことができます。
APIは、アプリ同士が異なるプログラミング言語・プラットフォームであっても、
やりとりは問題ありません。
gRPCについてはこのへんが詳しいので、
ご確認ください。
Protocol Buffersとprotoファイル
gRPCのフォーマットであるProtocol Buffersは、
やり取りするデータの型を下記のようなファイル(.proto)で定義します。
syntax = "proto3";
package example.foo;
message FooRequest {
string greet = 1;
}
message FooResponse {
string msg = 1;
}
service FooService {
rpc sayFoo (FooRequest) returns (FooResponse) {}
}
↑のように、RPCでやりとりする関数とオブジェクトの定義を明確に定義します。
プロトコル定義ができたら、対象システムで使用する
gRPC用クラスの生成を行います。
Protocol Buffersでは、定義ファイルから各言語に定義された
クラス定義ファイルを生成するツールがあるので、
それを使ってクラスを生成し、実装します。
Environment
- OS : MacOS 12.4
- Rust : 1.64.0
- Node : v18.11.0
Create gRPC Server & Client
では、RustでgRPCサーバを実装し、
Javascriptで実装したgRPCクライアントと通信してみます。
なお、使用している言語やライブラリは違いますがここでも
gRPCを試しているので見てみてください。
Rust(tonic)でgRPCサーバの実装
まずはRustでgRPCサーバの実装をしてみます。
gRPC実装はtonicを使います。
これはRustのgRPCライブラリの中でもメジャーなgRPCライブラリです。
ではプロジェクトの作成をします。
また、protoディレクトリを作成して、
そこにコード生成用のスキーマ定義ファイルを作成します。
% cargo new grpc-server && cd grpc-server
% mkdir proto
proto/user.protoファイルを下記のように記述します。
UserService.CreateUserでは、
リクエストで名前と年齢を受け取ると、
Userオブジェクトを返すように定義します。
syntax = "proto3";
package example.user;
/* リクエスト用オブジェクト */
message MyRequest {
string name = 1;
int32 age = 2;
}
/* レスポンス用オブジェクト */
message Book {
string title = 1;
string author = 2;
}
/* レスポンス用オブジェクト */
message User {
string name = 1;
int32 age = 2;
repeated Book books = 3;
}
/* サービス定義 */
service UserService {
rpc CreateUser (MyRequest) returns (User) {}
}
Cargo.tomlに依存ライブラリなどの設定を定義します。
tonic-buildはprotoファイルからコード生成するために使います。
build.rsでcargo build時に実行されるように記述します。
[package]
name = "grpc-web-server"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
tonic = "0.8.2"
bytes = "1.2.1"
prost = "0.11.0"
prost-derive = "0.11.0"
tokio = { version = "1.0", features = ["full"] }
[build-dependencies]
tonic-build = "0.8.2"
プロジェクトのルート直下にbuild.rsを作成します。
ここで記述した内容がコンパイル前に実行されます。
なので、cargo build時にprotoファイルからコードが生成され、
それを元にほかのrsファイルがコンパイルされます。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/user.proto")?;
Ok(())
}
src/main.rsにgRPCサーバ用コードを記述します。
include_proto!マクロでprotoファイルをreadし、
各モジュールをimportします。
また、MyUserService構造体を定義して、create_user関数を実装します。
このへんはprotoファイルにあわせて実装しましょう。
use tonic::{transport::Server, Request, Response, Status};
mod user {
tonic::include_proto!("example.user");
}
use user::{
user_service_server::{UserService, UserServiceServer},
Book, MyRequest,User
};
#[derive(Default)]
pub struct MyUserService {}
#[tonic::async_trait]
impl UserService for MyUserService {
async fn create_user(&self, request: Request<MyRequest>) -> Result<Response<User>, Status> {
let b1 = Book{title:"book1".to_string(),author:"author1".to_string()};
let b2 = Book{title:"book2".to_string(),author:"author2".to_string()};
let books = vec![b1,b2];
let req = request.into_inner();
println!("reqest name : {}",req.name);
println!("reqest age : {}",req.age);
let reply = user::User {
name: format!("{}", req.name).into(),
age:req.age,
books:books
};
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let user = MyUserService::default();
Server::builder()
.add_service(UserServiceServer::new(user))
.serve(addr)
.await?;
Ok(())
}
main関数でUserServiceをバインドしてgRPCサーバを起動します。
cargo runで実行すればサーバの起動完了です。
% cargo run
Compiling grpc-web-server v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 2.17s
Running `target/debug/grpc-web-server`
クライアントをJavascriptで実装する前に、
grpcurlというツールをつかってサーバにアクセスしてみましょう。
※grpcurlはgRPCサーバ用curl
Homebrewでインストールします。
% brew install grpcurl
gRPCサーバ宛にprotoファイルを指定してリクエストを送ります。
% grpcurl -proto proto/user.proto -d '{"name":"foo","age":30}' -plaintext localhost:50051 example.user.UserService.CreateUser
{
"name": "foo",
"age": 30,
"books": [
{
"title": "book1",
"author": "author1"
},
{
"title": "book2",
"author": "author2"
}
]
}
動いてるのでサーバ側はOKです。
JavaScriptでgRPCクライアントの実装
引き続きクライアントの実装に移ります。
適当なディレクトリをつくってgRPC関連のモジュールをインストールします。
% mkdir grpc-client && cd grpc-client
% yarn add @grpc/grpc-js grpc-tools
% yarn add -D grpc-tools grpc_tools_node_protoc_ts
grpc_tools_node_protocツールをつかってprotoファイルからgRPC用ファイルを生成します。
% cd path/your/grpc-client
% yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I /path/your/grpc-web-server/proto /path/your/grpc-web-server/proto/user.proto
必要なコードが生成できたので、client.mjsファイルを
作成してクライアントの実装をします。
import userpb from './codegen/proto/user_pb.js';
import grpc from '@grpc/grpc-js';
import user_grpc from './codegen/proto/user_grpc_pb.js';
//gRPCへ送るリクエストオブジェクトの作成
const request = new userpb.MyRequest();
request.setName("grpc from js");
request.setAge(30);
//gRPCサーバ用クライアント作成
const client = new user_grpc.UserServiceClient(
"localhost:50051",
grpc.credentials.createInsecure()
);
//CreateUser関数呼び出しと結果表示
client.createUser(request,function(err,user){
console.log(user.toString());
});
クライアントの実行。
gRPCで通信できました。
% node client.mjs
grpc from js,30,book1,author1,book2,author2
Summary
今回はRust-JS間でgRPC通信を試してみました。
同じprotoファイルを起点とすることで
クライアント-サーバ間のインターフェイスを統一し、
Protobufで高速通信もできるのでとても有用です。
(特に最近はマイクロサービス間の通信として使われたりします)
gRPCではストリーミング処理もできたりするので、
興味のあるかたは確認してみてください。