Anshul Goyal I love to code and use new technologies.

Rust and gRPC: A complete guide

5 min read 1426

Rust and gRPC: A Comprehensive Guide

gRPC is an open-source remote procedure call system developed by Google. gRPC allows the system to communicate in and out of data centers, efficiently transferring data from mobile, IoT devices, and backends to one and other.

gRPC comes with pluggable support for load balancing, authentication, tracing, etc., supports bidirectional streaming over HTTP/2, and provides an idiomatic implementation in 10 languages.

Furthermore, gRPC can generate efficient client libraries and uses the protocol buffer format to transfer data over the wire. Protocol buffers are a binary format for data transmission. Since they are binary, protocol buffers can be serialized fast. The structure of each message must be predefined.

gRPC support in Rust

The Rust community has developed many gRPC implementations, notably the tonic and grpc crates. Both provide a full implementation of gRPC protocols.

What is tonic?

tonic is a fast production-ready gRPC library with async/await support out of the box. It focuses on flexibility and reliability. tonic has full implementation of gRPC protocols over HTTP/2. tonic has built-in support for compiling protocol buffer to Rustlang. It also supports unidirectional as well as bidirectional streaming.

What is grpc?

grpc is not production-ready but is worth keeping an eye on. The crate has a working gRPC protocol implementation and supports TLS.

To show tonic and grpc in action, let’s walk through creating a demo gRPC app.

Building a gRPC app using tonic

We’ll start by creating a Rust project using cargo new grpc-demo-tonic. Create src/server.rs and src/client.rs to hold code for the gRPC server and client, respectively. We’ll add a build.rs to compile protocol buffers.

Some basic boilerplate, such as build.rs, is necessary to compile protocol buffer to Rust code. Some changes to Cargo.toml and file structure are also required.

Cargo.toml:

[package]
name = "grpc-demo-tonic"
version = "0.1.0"
authors = ["anshul <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

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

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

File structure

├── build.rs
├── Cargo.lock
├── Cargo.toml
├── src
    ├── client.rs
    └── server.rs

Start by creating a protocol buffer file.

    // version of protocol buffer used
    syntax = "proto3";

    // package name for the buffer will be used later
    package hello;

    // service which can be executed
    service Say {
    // function which can be called
      rpc Send (SayRequest) returns (SayResponse);
    }

    // argument
    message SayRequest {
    // data type and position of data
      string name = 1;
    }

    // return value
    message SayResponse {
    // data type and position of data
      string message = 1;
    }



We can include generated rust code in-app using `tonic`. Let’s create an `hello.rs` file to reuse in both server and client.

    // this would include code generated for package hello from .proto file
    tonic::include_proto!("hello");

Creating a gRPC server with tonic

Create a service by implementing the Say trait for a struct. The service may include multiple RPCs. Since Rust doesn’t support async traits, we have to use an asyc_trait macro to overcome this limitation.



Add the following code to server.rs.

    use tonic::{transport::Server, Request, Response, Status};
    use hello::say_server::{Say, SayServer};
    use hello::{SayResponse, SayRequest};
    mod hello; 

    // defining a struct for our service
    #[derive(Default)]
    pub struct MySay {}

    // implementing rpc for service defined in .proto
    #[tonic::async_trait]
    impl Say for MySay {
    // our rpc impelemented as function
        async fn send(&self,request:Request<SayRequest>)->Result<Response<SayResponse>,Status>{
    // returning a response as SayResponse message as defined in .proto
            Ok(Response::new(SayResponse{
    // reading data from request which is awrapper around our SayRequest message defined in .proto
                 message:format!("hello {}",request.get_ref().name),
            }))
        }
    }

    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // defining address for our service
        let addr = "[::1]:50051".parse().unwrap();
    // creating a service
        let say = MySay::default();
        println!("Server listening on {}", addr);
    // adding our service to our server.
        Server::builder()
            .add_service(SayServer::new(say))
            .serve(addr)
            .await?;
        Ok(())
    }

The above example uses tokio for async runtime and executor. The MySay struct implements the service Say. The Server type provided by tonic takes services and creates an HTTP server supporting the gRPC protocol on the given address.

Creating a gRPC client with tonic

Since gRPC defines request and response in a machine-readable format, we don’t need to implement client-side code. The code generated by the protocol buffer compiler already includes client code you can use by directly importing it.

use hello::say_client::SayClient;
use hello::SayRequest;
mod hello;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// creating a channel ie connection to server
    let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
    .connect()
    .await?;
// creating gRPC client from channel
    let mut client = SayClient::new(channel);
// creating a new Request
    let request = tonic::Request::new(
        SayRequest {
           name:String::from("anshul")
        },
    );
// sending request and waiting for response
    let response = client.send(request).await?.into_inner();
    println!("RESPONSE={:?}", response);
    Ok(())
}

Output for cargo run–bin client

To test our small application, run cargo run --bin server and then cargo run --bin client.

Building a gRPC app using grpc

First, create a Rust project using cargo new grpc-demo-grpc. We need to add two binaries: server and client, just like in the tonic demo. We must also add the proto/hello.proto file and build.rs

Cargo.toml:

[package]
name = "grpc-demo-grpc"
version = "0.1.0"
authors = ["anshul <[email protected]>"]
edition = "2018"

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

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


[dependencies]
protobuf        = "2"
httpbis         = { git = "https://github.com/stepancheg/rust-http2" }
grpc ="*"
grpc-protobuf="*"

[build-dependencies]
protoc-rust-grpc = "0.8.2"

build.rs:

protoc_rust_grpc requires protoc compiler in the PATH variables. It cab be downloaded from official website.

fn main() {
    // compile protocol buffer using protoc
    protoc_rust_grpc::Codegen::new()
    .out_dir("src")
    .input("./proto/hello.proto")
    .rust_protobuf(true)
    .run()
    .expect("error compiling protocol buffer");
}

Creating a server with grpc

The grpc crate’s APIs are very similar to tonic crate’s. Like tonic, grpc generates code for gRPC communication.

For creating service, the Say trait is implemented on a struct. The implemented struct is passed to the add_server method on the ServerBuilder struct provided by the grpc crate.

use grpc::{ServerHandlerContext,ServerRequestSingle,ServerResponseUnarySink};
// importing generated gRPC code
use hello_grpc::*;
// importing types for messages
use hello::*;
mod hello;
mod hello_grpc;
struct MySay;
impl Say for MySay {
    // rpc for service
    fn send(
        &self,
        _: ServerHandlerContext,
        req: ServerRequestSingle<SayRequest>,
        resp: ServerResponseUnarySink<SayResponse>,
    ) -> grpc::Result<()> {
        // create Response
        let mut r = SayResponse::new();
        let name = if req.message.get_name().is_empty() {
            "world"
        } else {
            req.message.get_name()
        };
        // sent the response
        println!("greeting request from {}", name);
        r.set_message(format!("Hello {}", name));
        resp.finish(r)
    }
}


fn main() {

    let port =50051;
    // creating server
    let mut server = grpc::ServerBuilder::new_plain();
    // adding port to server for http
    server.http.set_port(port);
    // adding say service to server
    server.add_service(SayServer::new_service_def(MySay));
    // running the server
    let _server = server.build().expect("server");
    println!(
        "greeter server started on port {}",
        port,
    );
    // stopping the program from finishing
    loop {
        std::thread::park();
    }
}

Creating a client with grpc

Making an RPC call is as simple as creating a client and sending the data. You simply create a request and send it through the client, then wait for a response.

use std::env;
use std::sync::Arc;

// importing generated gRPC code
use hello_grpc::*;
// importing types for messages
use hello::*;
mod hello;
mod hello_grpc;

use grpc::ClientStub;
use grpc::ClientStubExt;
use futures::executor;
fn main() {

    let name = "anshul";
    let port =50051;
    let client_conf = Default::default();
// create a client
    let client=SayClient::new_plain("::1", port, client_conf).unwrap();
// create request
    let mut req = SayRequest::new();
    req.set_name(name.to_string());
// send the request
    let resp = client
        .send(grpc::RequestOptions::new(), req)
        .join_metadata_result();
// wait for response
    println!("{:?}", executor::block_on(resp));
}

RPC Call Response

Conclusion

Rust has excellent support for gRPC. tonic in particular is a fast, production-ready gRPC implementation.

In this tutorial, we learned how to create a gRPC app using both the tonic and grpc crates. We explored protocol buffer and walked through how to compile it to Rust code.

tonic and grpc both support TLS-based authentication using the nativetls crate. This is just the tip of the iceberg; both tonic and grpc have almost full implementations of error handling, load balancing, and authentication.

Here’s a quick visual comparison:

Feature tonic grpc
Production-ready Yes No
Full async/await support Yes No
Request and response streaming Yes Yes
TLS-based authentication Yes Yes

 

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Anshul Goyal I love to code and use new technologies.

4 Replies to “Rust and gRPC: A complete guide”

  1. This article could benefit from a bit of proof-reading/editing.
    “We can include generated rust code in-app using `tonic`.” should not be in a code-block.
    tonic and tokio needed to be added to Cargo.toml – and I’m unsure what the “use” clauses are trying to refer to (e.g. use hello::say_server::SayServer;) – but this may also be because I’m fairly new to Rust.

    FWIW, I’m finding this helpful for resolving my confusion: https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md

    1. agreed.
      I tried to follow the tonic step as well but where to put the `.proto`, nor how to build is well defined here.

      The given link is much clearer thx.

Leave a Reply