17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

EnvoyなしにgRPC-webをNext.js+tonic(Rust)で実現する方法

Posted at

この投稿は、Rustのtonicを用いてEnvoyなしでもgRPC-webに対応したgRPCサーバーを作り、Next.jsからgRPCのエンドポイントを呼び出すまでの方法をステップ・バイ・ステップで解説するチュートリアル記事です。

gRPC-webを実現しようとすると、クライアントサイドとサーバーサイドの間に、Envoyなどのリバースプロキシが必要となります。なぜこれが必要かというと、gRPCサーバーはHTTP2なのに対し、ブラウザがHTTP2をサポートしているとは限らないというギャップを埋めるためです。EnvoyがHTTP1でgRPCリクエストを受け付け、HTTP2に変換したものをサーバーサイドに送るという仲介役を引き受けてくれます。

gRPC-web with Envoy.png

しかし、RustのgRPCサーバーtonicは、それ自体HTTP1とgRPC-webをサポートしているので、Envoyなどのミドルウェアを導入する必要がありません。この投稿では、tonicのgRPC-webを用いることで、EnvoyないしにgRPC-webを実現する方法を解説していきます。

gRPC-web without Envoy.png

なお、このチュートリアルで作ったアプリケーションのコードはGitHubにて公開しています。完成形のコードを確認したい方は、こちらもご覧ください。

プロジェクトを作る

まず、このチュートリアルの置き場を作ります。

mkdir grpc
cd grpc

次に、その中に

  • Rust製のサーバーサイドプログラムを置くディレクトリ
  • Next.js製のクライアントサイドプログラムを置くディレクトリ
  • Protocol Buffersのスキーマを置くディレクトリ

の3つをそれぞれ作ります。

mkdir server proto client

この2つを作るとプロジェクトのファイル構成は次のようになります。

.
├── client
├── proto
└── server

Protocol Buffersのスキーマを作る

APIを定義するために、まずProtocol Buffersのファイルを作ります。今回作るAPIは、人(person)の情報を送ったら、その人の名前(name)を含んだ挨拶メッセージ(greeting message)を生成して返すシンプルなAPIです。

まず、protoディレクトリを作り、その中にgreeting.protoファイルを作ります。

touch proto/greeting.proto

greeting.protoファイルに書くスキーマは次のようになります。

proto/greeting.proto
syntax = "proto3";  
  
package greeting;  
  
service Greeter {  
  rpc SayHello (Person) returns (GreetingMessage) {}  
}  
  
message Person {  
  string name = 1;  
}  
  
message GreetingMessage {  
  string text = 1;  
}

gRPC-webサーバーを作る

ここからは、gRPC-webサーバーをRustのtonicを使って組んできます。

サーバーサイドプロジェクトをセットアップする

serverディレクトリに移動して、cargo initでプロジェクトの雛形を作ります。

cd server
cargo init .

tonicをインストールする

tonicとtonic関連のパッケージをインストールします。

cargo add tonic
cargo add tonic-web
cargo add --build tonic-build
cargo add prost
cargo add tokio -F rt-multi-thread -F rt -F macros
  • tonic ─ 本体。gRPCサーバーを実装するためのライブラリ。
  • tonic-web ─ tonicをgRPC-webに対応するためのライブラリ。
  • tonic-build ─ Protocol BuffersからRustコード(prostコード)を生成するツール。
  • prost ─ Protocol BuffersのRust実装。

tonicに付随して必要なものをインストール

tonicはprotobufが必要なのでそれをHomebrewでインストールしておきます。

brew install protobuf

デバッグ用ツールをインストール

gRPC-webのサーバーをデバッグするときにGUIのgRPCクライアントがあると便利です。BloomRPCはgRPCのGUIクライアントで、これもHomebrewでインストールしておきます。

brew install --cask bloomrpc

Rustのビルドスクリプトを作る

上で作ったgreeting.protoをRustのコンパイル時にRustコードに変換できるように、Rustのビルドスクリプトbuild.rsを作っておきます。

touch build.rs

build.rsの内容は次のようにします。

build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {  
    tonic_build::compile_protos("../proto/greeting.proto")?;  
    Ok(())  
}

この設定の内容は、greeting.protoをRustコードに変換するものですが、生成されるRustコードはtargetディレクトリに格納されます。

gRPC-webサーバーを実装する

src/main.rsにgRPC-webサーバーを実装するために、main.rsの内容を次のように変更します。

src/main.rs
use greeting::greeter_server::{Greeter, GreeterServer};  
use greeting::{GreetingMessage, Person};  
use tonic::{transport::Server, Request, Response, Status};  
use tonic_web::GrpcWebLayer;  
use tower_http::cors::CorsLayer;  
  
mod greeting {  
    // greeting.protoから生成したRustコードを展開するマクロ  
    tonic::include_proto!("greeting");  
}  
  
#[derive(Default)]  
struct MyGreeter {}  
  
#[tonic::async_trait]  
impl Greeter for MyGreeter {  
    async fn say_hello(  
        &self,  
        request: Request<Person>,  
    ) -> Result<Response<GreetingMessage>, Status> {  
        // gRPCリクエストから入力値を参照する  
        let name = request.into_inner().name;  
        println!("Creating a greeting message for {:?}", name);  
        // レスポンスの内容を作成する  
        let greeting_message = GreetingMessage {  
            text: format!("Hello {}!", name),  
        };  
        // gRPCレスポンスを作成する  
        let response = Response::new(greeting_message);  
        // gRPCレスポンスを返す  
        Ok(response)  
    }  
}  
  
#[tokio::main]  
async fn main() -> Result<(), Box<dyn std::error::Error>> {  
    let addr = "127.0.0.1:50051".parse().unwrap();  
    let greeter = MyGreeter::default();  
    let allow_cors = CorsLayer::new()  
        .allow_origin(tower_http::cors::Any)  
        .allow_headers(tower_http::cors::Any)  
        .allow_methods(tower_http::cors::Any);  
    println!("GreeterServer listening on {}", addr);  
    Server::builder()  
        .accept_http1(true) // gRPC-webに対応するために必要  
        .layer(allow_cors) // CORSに対応するために必要  
        .layer(GrpcWebLayer::new()) // gRPC-webに対応するために必要  
        .add_service(GreeterServer::new(greeter))  
        .serve(addr)  
        .await?;  
    Ok(())  
}

gRPC-webサーバーを起動する

cargo runを実行してgRPC-webサーバーを起動します。

cargo run

これを実行すると、ビルドとサーバーの起動が行われます。しばらくして次のようなメッセージがターミナルに表示されたら、サーバーの起動が成功しています。

GreeterServer listening on 127.0.0.1:50051

BloomRPCでサーバーをテストする

サーバーが起動したら、greeting APIが使えるかgRPCのGUIクライアントでリクエストを出してみてテストします。

  1. BloomRPCが起動する。
  2. プラスボタンを押してProtocol Buffersファイルgreeting.protoを読み込みます(1)。
  3. 左メニューにAPIリストが表示されるので、その中から「SayHello」メソッドをクリックします(2)。
  4. アドレス入力欄に「127.0.0.1:50051」と入力 (3)。
  5. 「WEB」のトグルをonにします。(4)
  6. 「Editor」のJSONを変更して、nameプロパティにtanakaなど好きな値にしてください。(5)
  7. 実行ボタンを押す。(6)
  8. 「Response」にレスポンスの内容が表示されます。(7)

BloomGRPでgreeting APIを叩いた様子.png

BloomRPCでリクエストを出して、レスポンスに次のような内容が表示されれば、サーバーは正しく動作していることになります。

{
  "text": "Hello tanaka!"
}

これと同時に、ターミナルの標準出力に表示されるサーバーのログには次のようなログが表示されます。

Creating a greeting message for "tanaka"

クライアントを作る

ここからはNext.jsから上で作ったサーバーのAPIを呼び出せるように作っていきます。

必要なもの

  • Yarn (このチュートリアルではv1.22.19を使用)

クライアントサイドプロジェクトのセットアップ

ターミナルのタブを開いて、最初に作っておいたclientディレクトリに移動します。

cd client

次のコマンドを実行して、Next.jsの雛形プロジェクトを生成します。

yarn create next-app . --typescript

ここではインタラクティブにいろいろ聞かれますが、全部空欄で大丈夫です。これが実行され終わると、次のようなファイルが生成されます。

client/
├── README.md
├── next-env.d.ts
├── next.config.js
├── node_modules
├── package.json
├── pages
├── public
├── styles
├── tsconfig.json
└── yarn.lock

Next.jsを起動する

次のコマンドを実行して、Next.jsを起動します。

yarn dev

起動したら、http://localhost:3000をブラウザで開いてNext.jsが起動しているか確認します。

Connect-webをインストールする

gRPC-webをクライアントサイドで手軽に使いたいので、各種ライブラリと開発ツールをインストールします。

yarn add @bufbuild/connect-web @bufbuild/protobuf
yarn add -D @bufbuild/protoc-gen-connect-web @bufbuild/protoc-gen-es

ここでインストールするものの内容は次のとおりです。

  • ライブラリ
    • @bufbuild/connect-web ─ ConnectプロトコルとgRPC-webプロトコルに対応したクライアントを提供する
    • @bufbuild/protobuf ─ シリアライゼーションと基本的な型のいろいろを提供する
  • 開発ツール
    • @bufbuild/protoc-gen-connect-web ─ Protocol Buffersスキーマからサービスを生成する
    • @bufbuild/protoc-gen-es ─ リクエストやレスポンスといった基本的な型を生成する。

bufの設定ファイルを作る

上でインストールした開発ツールを使って、Protocol BuffersのスキーマファイルからTypeScriptのコードを生成するには、buf.gen.yamlという設定ファイルを作る必要があります。

touch buf.gen.yaml

buf.gen.yamlの内容は次のようにします。

version: v1  
plugins:  
  # Protocol Buffersのmessageに対応するTypeScriptコードを生成する (_pb.tsで終わるファイル名のもの)  
  - name: es  
    out: services  
    opt: target=ts  
  # Protocol Buffersのserviceに対応するTypeScriptコードを生成する (_connectweb.tsで終わるファイル名のもの)  
  - name: connect-web  
    out: services  
    opt:  
      - target=ts  
      - import_extension=none

genはプラグインを足すことで、Protocol Buffersのスキーマからさまざまな言語のコードを生成できる仕組みになっています。上の例では、esプラグインとconnect-webプラグインを使っていて、servicesディレクトリにTypeScriptファイルを生成します。

ちなみに、esプラグイン、connect-webプラグインともに、TypeScriptコードではなくJavaScriptコードやTypeScriptの型定義ファイルのみを生成することもできます。その場合は、target=jstarget=js+dtsにします。

Protocol BuffersからTypeScriptファイルを生成する

生成するにはbuf コマンドが必要になるので、Homebrewでインストールしておきます。

brew install bufbuild/buf/buf

buf generateを実行すると、TypeScriptファイルが生成できるわけですが、bufのesプラグインやconnect-webプラグインのパスがPATH環境変数に追加された環境で実行する必要があります。その環境を設ける方法はいくつか考えられますが、NPMスクリプトにするのが手軽です。NPMスクリプトにするには、次のようにpackage.jsonscriptsフィールドにbuf generateコマンドを足します。

client/package.json
{  
  // 中略
  "scripts": {  
    // 中略
    "gen:services": "buf generate ../proto/greeting.proto"  
  },
  // 中略
}

NPMスクリプトを追加したら、yarnでこれを実行します。

yarn gen:services

このコマンドを実行すると、buf.gen.yamlで指定したservicesディレクトリに、次のTypeScriptファイルが生成されます。

  • greeting_connectweb.ts ─ 内容はProtocol Buffersのserviceに対応したオブジェクト
  • greeting_pb.ts ─ 内容はProtocol Buffersのmessageに対応したクラス

生成されたTypeScriptファイルを少し眺めてみましょう。次は、greeting_pb.tsの中身です。

client/services/greeting_pb.ts
// 中略  
import { Message } from "@bufbuild/protobuf";  
  
export class Person extends Message<Person> {  
  name = "";  
  // 中略  
}  
  
export class GreetingMessage extends Message<GreetingMessage> {  
  text = "";  
  // 中略  
}

ご覧のとおり、Protocol Buffersのmessageに対応したクラスが生成されています。

次は、greeting_connectweb.tsの内容です。

client/services/greeting_connectweb.ts
import { GreetingMessage, Person } from "./greeting_pb";  
import { MethodKind } from "@bufbuild/protobuf";  
  
export const Greeter = {  
  typeName: "greeting.Greeter",  
  methods: {  
    sayHello: {  
      name: "SayHello",  
      I: Person,  
      O: GreetingMessage,  
      kind: MethodKind.Unary,  
    },  
  }  
} as const;

これはProtocol Buffersのserviceに対応したオブジェクトが生成されています。

Next.jsからtonicのgRPCを呼び出すには、これらの生成されたTypeScriptコードを用いていきます。

Next.jsとtonicを接続する

ここからは、Next.jsからtonicのgRPCを呼び出す処理を実装していきます。pages/index.tsxの内容を次のように書き換えます。コードの部分部分の解説は、インラインコメントで説明します。

client/pages/index.tsx
import {
  createGrpcWebTransport,
  createPromiseClient,
} from "@bufbuild/connect-web";
import type { PartialMessage } from "@bufbuild/protobuf";
import { type FormEvent, useState } from "react";
import { Greeter } from "../services/greeting_connectweb";
// messageスキーマの型情報もimportすることで利用可能
import type { GreetingMessage, Person } from "../services/greeting_pb";

// gRPCクライアントの初期化
const transport = createGrpcWebTransport({
  baseUrl: "http://localhost:50051",
});
const client = createPromiseClient(Greeter, transport);

export default function Home() {
  const [name, setName] = useState("");
  const [text, setText] = useState("");

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    // リクエストメッセージのオブジェクトはPartialMessageを使うと取れます
    const person: PartialMessage<Person> = { name };
    // gRPCメソッドを呼び出す
    const greetingMessage: GreetingMessage = await client.sayHello(person);
    console.log("greetingMessage: ", greetingMessage);
    setText(greetingMessage.text);
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
          placeholder="name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <button type="submit">submit</button>
      </form>
      <div>{text}</div>
    </>
  );
}

書き換えたら、ブラウザでhttp://localhost:3000/を開いてください。nameの入力欄があるので、そこに名前を入力して、「submit」ボタンを押してみてください。すると、その下に「Hello」で始まるテキストが結果として表示されます。この処理の裏ではgRPC-webの通信が行われていて、開発ツールのネットワークでHTTPリクエスト・レスポンスを確認すると、その内容も確認できます。

gRPC-webが実行された様子.png

おわり

以上で、Rustのtonicを用いてEnvoyなしでもgRPC-webに対応したgRPCサーバーを作り、Next.jsからgRPCのエンドポイントを呼び出すまでのチュートリアルは終わりです。意外とシンプルな構成でgRPC-webが実現できることがおわかりいただけたのではないでしょうか。

まとめ

  • Rustのtonicを使うと、EnvoyなしにgRPC-webが使える
  • bufを使うと、Protocol BuffersのスキーマからTypeScriptコードが生成できる
  • このチュートリアルで作ったアプリのソースコード: GitHub
17
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?