kumaran@throw.of.a.biased.dice:~$

Life. Code. Tech. More

gRPC + Rust

As a continuation of the previous post, I intend to implement a gRPC service in Rust. I shall use the tonic crate for implementing it in this example. To start with we need rust lang and cargo installed as mentioned here.

Let’s create a cargo project for the same.

cargo new grpc-example-pokedex

This will create a new rust project with a Cargo.toml file and a src folder. Tonic and other dependencies we need for the project needs to be added to the Cargo.toml file

[dependencies]
tonic = "0.2"
prost = "0.6"
tokio = { version = "0.2", features = ["macros"] }

[build-dependencies]
tonic-build = "0.2"

Tonic is used for implementing gRPC server and client. Prost is a protocol buffer implementation in Rust. Tokio is used for writing asynchronous application in Rust. By running cargo install --path ., it installs all the mentioned dependencies. It shall also create a Cargo.lock file to lock the dependencies version.

Defining the proto and generate code

Let’s define the proto and generate the required structs from the proto file. Create the pokedex.proto under src/proto as below

syntax = "proto3";
package pokedex;

enum PokemonType {
    NORMAL = 0;
    FIRE = 1;
    GROUND = 2;
    WATER = 3;
    GRASS = 4;
}
message Query {
    string value = 1;
}
message PokemonResponse {
    int32 id = 1;
    string name = 2;
    repeated PokemonType pokemonType = 3;
}
service PokeDex {
    rpc GetPokemonByName(Query) returns (PokemonResponse);
}

To generate the client stubs and the structs from the proto file, create a build.rs file under src:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    if std::fs::metadata("src/generated").is_ok() {
        std::fs::remove_dir_all("src/generated")?;
    }
    std::fs::create_dir("src/generated")?;
    tonic_build::configure()
        .build_server(true)
        .build_client(true)
        .out_dir("src/generated")
        .compile(
            &["./proto/pokedex.proto"],
            &["./proto"],
        )?;
    Ok(())
}

This will generate the code every time the project is built under the src/generated directory. This directory can be exposed as a mod by adding a mod.rs to this directory. But I prefer keeping the generated directory untouched, I’d prefer to create a pokedexpb directory to expose the generated structs. Hence I created a pokedexpb package and created a mod.rs inside that directory to achieve the same.

include!("../generated/pokedex.rs");

Implementing the server

Let’s now implement the server. Let’s create a server.rs under src to implement it.

mod pokedexpb;

use tonic::{transport::Server, Status, Response, Request};
use pokedexpb::poke_dex_server::{PokeDex, PokeDexServer};
use pokedexpb::{PokemonResponse, Query, PokemonType}

#[derive(Clone)]
pub struct PokeDexContext {}

#[tonic::async_trait]
impl PokeDex for PokeDexContext {
    async fn get_pokemon_by_name(
        &self,
        _: tonic::Request<Query>,
    ) -> Result<tonic::Response<PokemonResponse>, tonic::Status> {
        return Result::Ok(tonic::Response::new(PokemonResponse {
            id: 6,
            name: "Charizard".to_string(),
            pokemon_type: vec![PokemonType::Fire as i32],
        }))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = PokeDexContext {};

    println!("Starting ther server at :5000")

    Server::builder()
        .add_service(PokeDexServer::new(context.clone()))
        .serve("0.0.0.0:5000")
        .await?;

    Ok(())
}

Only a few lines, now the server is ready. Optionally, we can add these lines to Cargo.toml. To dig in detail, we are creating a struct called PokeDexContext and implementing the trait PokeDex (generated by build.rs) on that struct. Then in the main function we are building the server by adding PokeDexContext server and starting it using the serve method.

[[bin]]
name = "server"
path = "src/server.rs"

Now we can start the server with the command:

$ cargo run --bin server

which should start the server listening at port 5000.

Implementing the client

To implement a client for the same, let’s start with a client.rs under src:

pub mod pokedexpb;

use pokedexpb::poke_dex_client::PokeDexClient;
use pokedexpb::Query;
use pokedexpb::{Pokemon, PokemonType};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = PokeDexClient::connect("0.0.0.0:5000").await?;

    let request = tonic::Request::new(Query {
        value: "Charizard".to_string(),
    });
    let response = client.get_pokemon_by_name(request).await?;
    println!("Get Pokemon By Name Response = {:?}", response);

    Ok(())
}

In detail, we are creating a PokeDexClient from the generated code to connect with the server running in 0.0.0.0:5000. We then create the request and call the method. Less than 10 lines to implement the client. Similar to the server, we can add these lines to Cargo.toml.

[[bin]]
name = "client"
path = "src/client.rs"

Now we can run the client with the command

$ cargo run --bin client

This implements a gRPC client and server in Rust in less than 50 lines.

Next steps

To improve on this demo, we can fetch the details from the DB. I’ve used diesel which is a good ORM library to start with. I have put this example here for reference with a few additional operations.

Disclaimer

Pokémon, Pokêdex names and information (c) 1995-2014 Nintendo/Game freak. Those are being referenced here entirely for education purposes only.