gRPCを使ってRust – JavaScript通信

2022.10.26

この記事は公開されてから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ではストリーミング処理もできたりするので、
興味のあるかたは確認してみてください。

References