S-JIS[2026-03-12/2026-03-31]

gRPC Rust(tonic 0.14)

Rust版gRPCのtonic 0.14のメモ。


概要

Rustのtonic(tonic-prost)0.14でgRPCのサーバーとクライアントを作ってみる。

tonic 0.12からの変更点は、Cargo.tomlのdependenciesにtonic-prostとtonic-prost-buildを指定すること。それに伴い、build.rsでtonic_prost_buildを使う)


設定

tonicを使うために、Cargo.tomlに以下の設定を追加する。

プロジェクト/Cargo.toml:

〜
[dependencies]
tonic = "0.14.5"
tonic-prost = "0.14.5"
prost = "0.14"
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }

[build-dependencies]
tonic-prost-build = "0.14.5"

tonic本体およびtonic-prostと、ビルドに使用するtonic-prost-buildを追加する。
また、protobufをビルドして生成された構造体を使う為にprostを使用する。
それと、tokioも追加しておく。


protoファイルの例

C++版gRPCサーバーの例のprotoファイルと同じものを使ってみる。

プロジェクト/proto/example.proto

syntax = "proto3";

package example.grpc;

message Int64PairRequest {
    int64 first_value = 1;
    int64 second_value = 2;
}

message Int64PairResponse {
    int64 first_value = 1;
    int64 second_value = 2;
}

service ExampleService {
    rpc SendInt64Pair(Int64PairRequest) returns (Int64PairResponse);
}

そして、protoファイルをビルドする為のbuild.rsを用意する。

プロジェクト/build.rs

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_prost_build::configure().compile_protos(&["proto/example.proto"], &["proto"])?;
    Ok(())
}

compile_protos()の第2引数はprotocコマンドの「-I」相当。(インポートするファイルをそこから検索する)


gRPCサーバーの例

プロジェクト/src/bin/server.rs

use tonic::{Request, Response, Status, transport::Server};

use crate::example::grpc::{
    Int64PairRequest, Int64PairResponse,
    example_service_server::{ExampleService, ExampleServiceServer},
};
pub mod example {
    pub mod grpc {
        tonic::include_proto!("example.grpc");
    }
}

↑tonic::include_proto!マクロの引数には、protoファイルに定義されたパッケージ名を指定する。

↓生成されたExampleServiceトレイトの実装を作成する。

struct ExampleServiceImpl {}

#[tonic::async_trait]
impl ExampleService for ExampleServiceImpl {
    async fn send_int64_pair(
        &self,
        request: Request<Int64PairRequest>,
    ) -> Result<Response<Int64PairResponse>, Status> {
        let request = request.into_inner();
        println!("request = {:?}", request);

        let response = Int64PairResponse {
            first_value: request.first_value,
            second_value: request.second_value,
        };
        Ok(Response::new(response))
    }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = 50051;
    let addr = format!("0.0.0.0:{port}").parse()?;
    println!("address = {}", addr);

    let service = ExampleServiceImpl {};

    Server::builder()
        .add_service(ExampleServiceServer::new(service))
        .serve(addr)
        .await?;

    Ok(())
}

サーバーの実行方法

cargo run --bin server

Ctrl+Cで止める。


gRPCクライアントの例

プロジェクト/src/bin/client.rs

use tonic::Request;

use crate::example::grpc::{Int64PairRequest, example_service_client::ExampleServiceClient};
pub mod example {
    pub mod grpc {
        tonic::include_proto!("example.grpc");
    }
}

↑tonic::include_proto!マクロの引数には、protoファイルに定義されたパッケージ名を指定する。

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = 50051;
    let url = format!("http://localhost:{port}");
    println!("address = {}", url);

    let mut client = ExampleServiceClient::connect(url).await?;

    let request = Request::new(Int64PairRequest {
        first_value: 123,
        second_value: 456,
    });
    let response = client.send_int64_pair(request).await?;

    println!("response = {:?}", response.into_inner());

    Ok(())
}

Endpointを使う例

Endpoint構造体を使って接続する例。[2026-03-16]

    let port = 50051;
    let url = format!("http://localhost:{port}");
    println!("address = {}", url);

    let uri: tonic::transport::Uri = url.parse()?;
    let endpoint = tonic::transport::Endpoint::from(uri);
    let mut client = ExampleServiceClient::connect(endpoint).await?;

Channelを使う例

Channel構造体を使って接続する例。[2026-03-16]

    let port = 50051;
    let url = format!("http://localhost:{port}");
    println!("address = {}", url);

    let uri: tonic::transport::Uri = url.parse()?;
    let endpoint = tonic::transport::Endpoint::from(uri);
    let channel = endpoint.connect().await?; // Channel
    let mut client = ExampleServiceClient::new(channel);

クライアントの実行方法

cargo run --bin client

UNIXドメインソケットの例

gRPCではUNIXドメインソケットを使って通信することが出来る。[2026-03-16]

ただしtonic(というかtokio)では、UNIXドメインソケットのライブラリーはUNIXでしか使用できない。(Windowsではコンパイルエラーになる)


UNIXドメインソケットgRPCサーバーの例

UNIXドメインソケットを受け付けるgRPCサーバーでは、tokio-streamクレートを使う。(Windowsではコンパイルできない)

プロジェクト/Cargo.toml:

〜
[dependencies]
〜
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.18"

プロジェクト/src/bin/uds_server.rs

use tokio::net::UnixListener;
use tokio_stream::wrappers::UnixListenerStream;
〜
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = "/tmp/grpc-example.sock"; // UNIXドメインソケット
    let _ = std::fs::remove_file(path);

    let uds = UnixListener::bind(path)?;
    let uds_stream = UnixListenerStream::new(uds);

    let service = ExampleServiceImpl {};

    Server::builder()
        .add_service(ExampleServiceServer::new(service))
        .serve_with_incoming(uds_stream)
        .await?;

    Ok(())
}

サーバーの実行方法

cargo run --bin uds_server

UNIXドメインソケットgRPCクライアントの例

UNIXドメインソケットで接続するgRPCクライアントでは、connect_with_connector()でChannelを構築する方法と、connect()にパスを渡す方法がある。


UNIXドメインソケットgRPCクライアントのconnect_with_connectorの例

hyper-utilとtowerクレートを使う。(Windowsではコンパイルできない)

プロジェクト/Cargo.toml:

〜
[dependencies]
〜
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
hyper-util = "0.1.20"
tower = "0.5.3"

プロジェクト/src/bin/uds_client.rs:

use hyper_util::rt::TokioIo;
use tokio::net::UnixStream;
use tonic::Request;
use tower::service_fn;
〜
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = "/tmp/grpc-example.sock"; // UNIXドメインソケット
//  let path = "unix:///tmp/grpc-example.sock";

    let endpoint = tonic::transport::Endpoint::try_from("http://[::]:50051")?;
    let channel = endpoint
        .connect_with_connector(service_fn(move |_: tonic::transport::Uri| async move {
            let stream = UnixStream::connect(path).await?;
            Ok::<_, std::io::Error>(TokioIo::new(stream))
        }))
        .await?;
    let mut client = ExampleServiceClient::new(channel);

    let request = Request::new(Int64PairRequest {
        first_value: 123,
        second_value: 456,
    });
    let response = client.send_int64_pair(request).await?;

    println!("response = {:?}", response.into_inner());

    Ok(())
}

Endpoint::connect_with_connector()でChannelを作る。
この中でUNIXドメインソケットのパスを指定するので、Endpointインスタンスの生成の際には適当なURL(実際には使われない)を指定しておく。


UNIXドメインソケットgRPCクライアントのconnectの例

パスを「unix://」で始めれば、Client::connect()で単純に指定できる。

(UNIX専用の構造体は使わないので、Windowsでもコンパイルできる)

プロジェクト/src/bin/uds_client.rs

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = "unix:///tmp/grpc-example.sock";

    let mut client = ExampleServiceClient::connect(path).await?;

    let request = Request::new(Int64PairRequest {
        first_value: 123,
        second_value: 456,
    });
    let response = client.send_int64_pair(request).await?;

    println!("response = {:?}", response.into_inner());

    Ok(())
}

クライアントの実行方法

cargo run --bin uds_client

vsockの例

gRPCではvsockを使って通信することが出来る。[2026-03-31]


vsock gRPCサーバーの例

vsockで受け付けるgRPCサーバーでは、tokio-vsockクレートを使う。(Windowsではコンパイルできない)

〜
[dependencies]
〜
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.18"
tokio-vsock = "0.7.2"
async-stream = "0.3.6"

プロジェクト/src/bin/vsock_server.rs

use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::server::Connected;
use tonic::{Request, Response, Status, transport::Server};
〜

VsockStreamを処理するためのVsockIo構造体を自作する。
(自作せずにTokioIoを使いたいところだが、TokioIoはConnectedトレイトを実装していないようなので、vsockのgRPCサーバーでは使えない)

struct VsockIo(tokio_vsock::VsockStream);

impl Connected for VsockIo {
    type ConnectInfo = ();

    fn connect_info(&self) -> Self::ConnectInfo {}
}

// AsyncRead/AsyncWriteをVsockStreamに委譲
impl tokio::io::AsyncRead for VsockIo {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        Pin::new(&mut self.0).poll_read(cx, buf)
    }
}

impl tokio::io::AsyncWrite for VsockIo {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<std::io::Result<usize>> {
        Pin::new(&mut self.0).poll_write(cx, buf)
    }

    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        Pin::new(&mut self.0).poll_flush(cx)
    }

    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        Pin::new(&mut self.0).poll_shutdown(cx)
    }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cid = tokio_vsock::VMADDR_CID_ANY;
    let port = 50051;
    let addr = tokio_vsock::VsockAddr::new(cid, port);
    let listener = tokio_vsock::VsockListener::bind(addr)?;

    let incoming = async_stream::stream! {
        loop {
            match listener.accept().await {
                Ok((stream, addr)) => {
                    println!("accepted connection from {:?}", addr);
                    yield Ok(VsockIo(stream));
                },
                Err(e) => {
                    eprintln!("accept error: {:?}", e);
                    yield Err(e);
                }
            }
        }
    };

    let service = ExampleServiceImpl {};

    Server::builder()
        .add_service(ExampleServiceServer::new(service))
        .serve_with_incoming(incoming)
        .await?;

    Ok(())
}

サーバーの実行方法

cargo run --bin vsock_server

vsock gRPCクライアントの例

vsockで接続するgRPCクライアントは、tokio-vsockクレートを使う。(Windowsではコンパイルできない)

プロジェクト/Cargo.toml:

〜
[dependencies]
〜
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
hyper-util = "0.1.20"
tower = "0.5.3"
tokio-vsock = "0.7.2"

プロジェクト/src/bin/vsock_client.rs

use hyper_util::rt::TokioIo;
use tonic::Request;
use tower::service_fn;
〜
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cid = 1; // 1はループバック(接続先は同一ホスト)
    let port = 50051;
    let addr = tokio_vsock::VsockAddr::new(cid, port);

    let channel = tonic::transport::Endpoint::from_static("http://[::]:50051")
        .connect_with_connector(service_fn(move |_| async move {
            let stream = tokio_vsock::VsockStream::connect(addr).await?;
            Ok::<_, std::io::Error>(TokioIo::new(stream))
        }))
        .await?;
    let mut client = ExampleServiceClient::new(channel);

    let request = Request::new(Int64PairRequest {
        first_value: 123,
        second_value: 456,
    });
    let response = client.send_int64_pair(request).await?;

    println!("response = {:?}", response.into_inner());

    Ok(())
}

Endpoint::connect_with_connector()でChannelを作る。
この中でvsockの接続先を指定するので、Endpointインスタンスの生成の際には適当なURL(実際には使われない)を指定しておく。


クライアントの実行方法

cargo run --bin vsock_client

gRPC Rustへ戻る / gRPCへ戻る / 技術メモへ戻る
メールの送信先:ひしだま