Last week I was showcasing WebAssembly in the cloud using krustlet and Fermyon Spin at Cloudland 2022 in Brühl (Germany). While preparing for the talk, I recognized how much I’d missed Rust 🦀 over the past months.
Although I’ve been reading articles and watching some videos on Rust in general from time to time, I missed doing some hands-on. So I dedicated some time to do a quick refresher for Rust, updated my local installation rustup update
, and wrote a small gRPC server in Rust using tonic. However, before we dive into the tutorial, let’s quickly look at what tonic is and what we can do with it.
- What is tonic
- Let’s build the gRPC service with Rust and tonic
- Let’s build a gRPC client with Rust and tonic
- Further optimizations
- What we’ve covered in this post
- Conclusion
What is tonic
Tonic is a super lightweight gRPC implementation with batteries (prost, hyper, and protobuf) included to build both gRPC servers and clients. The server and client can leverage HTTP/2 and code generation to generate the necessary code for individual services from standard protocol buffer specifications (.proto
).
Let’s build the gRPC service with Rust and tonic
Consider this post a practical and structured introduction to tonic
where we build the - perhaps - most simple gRPC service ever. You can find the entire source explained in this tutorial here on GitHub.
Creating the client & server projects
We’ll use cargo
(the Rust package manager) to create our projects and bring in all necessary dependencies.
#move to your source directory
cd ~/source
# create a directory for the entire sample (as we will build server and client)
# alternatively, you can do a mkdir and cd...
mkdir rusty-grpc && cd rusty-grpc
# create the projects with cargo
cargo new server
cargo new client
# create a folder for the service definition (proto)
mkdir protos
Define the service definition
Having the folder structure and the projects in place, it’s time to create the service definition using protocol buffers (protos). We will create a single service definition for the sake of this tutorial. That said, move to the protos
folder and create a new file called voting.proto
(touch voting.proto
while being in the ~/source/rusty-grpc/protos
folder) and add the following service definition:
syntax = "proto3";
package voting;
service Voting {
rpc Vote (VotingRequest) returns (VotingResponse);
}
message VotingRequest {
string url = 1;
enum Vote {
UP = 0;
DOWN = 1;
}
Vote vote = 2;
}
message VotingResponse {
string confirmation = 1;
}
The proto
is pretty self-explaining. We want to expose a service that allows its consumers to vote up or down for a specific url
. Nothing fancy. However, it’s more than enough for demonstration purposes.
Let tonic generate the service facade
Now that we’ve defined the proto
, it’s time to add the necessary dependencies to our server project and instruct tonic to generate the server-side code based on that service definition.
Let’s install the necessary dependencies for our server project using cargo
:
cd server
# add dependencies
cargo add tonic
cargo add prost
tokio = { version = "1.19.2", features = ["macros", "rt-multi-thread"] }
# add build dependencies
cargo add tonic-build --build
Your Cargo.toml
(~/source/rusty-grpc/server/Cargo.toml
) should now look like the following snippet:
[package]
name = "server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
prost = "0.10.4"
tokio = { version = "1.19.2", features = ["macros", "rt-multi-thread"] }
tonic = "0.7.2"
[build-dependencies]
tonic-build = "0.7.2"
Having all dependencies and the build dependency to tonic-build
in place, let’s add instructions and make tonic-build
generate the plumbing for our service. To do so, let’s create a dedicated file in the server
directory with the name build.rs
(touch ~/source/rusty-grpc/server/build.rs
) and add the following content:
fn main () -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("../protos/voting.proto")?;
Ok(())
}
Implement the service
All of the following steps take place in server/main.rs
. First, let’s add all necessary modules and bring the generated gRPC facade into a dedicated module:
use tonic::{transport::Server, Request, Response, Status};
use voting::{VotingRequest, VotingResponse, voting_server::{Voting, VotingServer}};
pub mod voting {
tonic::include_proto!("voting");
}
Having this in place, we can take care of implementing the actual service. We do so by defining a VotingService
struct and implementing the Voting
trait asynchronously (tokio
allows us to implement async traits):
#[derive(Debug, Default)]
pub struct VotingService {}
#[tonic::async_trait]
impl Voting for VotingService {
async fn vote(&self, request: Request<VotingRequest>) -> Result<Response<VotingResponse>, Status> {
let r = request.into_inner();
match r.vote {
0 => Ok(Response::new(voting::VotingResponse { confirmation: {
format!("Happy to confirm that you upvoted for {}", r.url)
}})),
1 => Ok(Response::new(voting::VotingResponse { confirmation: {
format!("Confirmation that you downvoted for {}", r.url)
}})),
_ => Err(Status::new(tonic::Code::OutOfRange, "Invalid vote provided"))
}
}
}
The actual logic of the service implementation is straight forward. Depending on what’s provided as vote
, we generate a proper response.
Create the underlying server and wire everything up
Having the service implementation in place, we must add the final part of our server implementation, we have to bootstrap the actual server and hook up our service. Replace the automatically generated main
method with the following code:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let address = "[::1]:8080".parse().unwrap();
let voting_service = VotingService::default();
Server::builder().add_service(VotingServer::new(voting_service))
.serve(address)
.await?;
Ok(())
}
We use the builder pattern exposed by tonic::Server
to register our service, bind the server to a particular socket and start it asynchronously.
Run the server
At this point, you can already start the server by executing cargo run
in the ~/sources/rusty-grpc/server
folder.
Let’s build a gRPC client with Rust and tonic
Now that we’ve our server up and running, it’s time to implement a client that consumes our gRPC service. Again we’ll use tonic to do the heavy lifting for us, so we can focus on consuming the gRPC service.
Install necessary dependencies for the client project
With a new rust project in place, let’s bring in necessary dependencies using cargo
:
cd client
# install dependencies
cargo add tonic
cargo add prost
cargo add tokio -F "macros rt-multi-thread"
# install build-dependencies
cargo add tonic-build --build
At this point the Cargo.toml
should look similar to this:
[package]
name = "client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
prost = "0.10.4"
tokio = { version = "1.19.2", features = ["macros", "rt-multi-thread"] }
tonic = "0.7.2"
[build-dependencies]
tonic-build = "0.7.2"
Now that all dependencies and the build dependency are specified in Cargo.toml
, we again instruct tonic-build
to generate the plumbing for our service. It’s the same instruction we used for the server. However, to keep both projects independent, create another build.rs
this time in the~/source/rusty-grpc/client/
folder and add the following content:
fn main () -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("../protos/voting.proto")?;
Ok(())
}
Consume the gRPC service from the client
Consuming a gRPC service with tonic
is straightforward, for demonstration purposes, let’s add the generated code again using a module and add necessary use
statements:
use voting::{VotingRequest, voting_client::VotingClient};
pub mod voting {
tonic::include_proto!("voting");
}
For the sake of this tutorial, we want to invoke the service as long as the user keeps on providing actual votes. Let’s replace the main
function and use a loop
to achieve that:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = VotingClient::connect("http://[::1]:8080").await?;
loop {
println!("\nPlease vote for a particular url");
let mut u = String::new();
let mut vote: String = String::new();
println!("Please provide a url: ");
stdin().read_line(&mut u).unwrap();
let u = u.trim();
println!("Please vote (d)own or (u)p: ");
stdin().read_line(&mut vote).unwrap();
let v = match vote.trim().to_lowercase().chars().next().unwrap() {
'u' => 0,
'd' => 1,
_ => break,
};
// here comes the service invocation
}
Ok(())
}
Finally, lets replace the comment with the actual service invocation:
let request = tonic::Request::new(VotingRequest {
url: String::from(u),
vote: v,
});
let response = client.vote(request).await?;
println!("Got: '{}' from service", response.into_inner().confirmation);
That’s it. No more is required to call an existing gRPC service in Rust using tonic
. That said, here is the final ~/source/rusty-grpc/client/main.rs
for reference:
use std::io::stdin;
use voting::{voting_client::VotingClient, VotingRequest};
pub mod voting {
tonic::include_proto!("voting");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = VotingClient::connect("http://[::1]:8080").await?;
loop {
println!("\nPlease vote for a particular url");
let mut u = String::new();
let mut vote: String = String::new();
println!("Please provide a url: ");
stdin().read_line(&mut u).unwrap();
let u = u.trim();
println!("Please vote (d)own or (u)p: ");
stdin().read_line(&mut vote).unwrap();
let v = match vote.trim().to_lowercase().chars().next().unwrap() {
'u' => 0,
'd' => 1,
_ => break,
};
let request = tonic::Request::new(VotingRequest {
url: String::from(u),
vote: v,
});
let response = client.vote(request).await?;
println!("Got: '{}' from service", response.into_inner().confirmation);
}
Ok(())
}
Test it by running the gRPC client
Invoke cargo run
from within the ~/source/rusty-grpc/client
folder and see the service response being printed to STDOUT
as shown here:
❯ cargo run
Compiling client v0.1.0 (/Users/abc/source/rusty-grpc/client)
Finished dev [unoptimized + debuginfo] target(s) in 2.61s
Running `target/debug/client`
Please vote for a particular url
Please provide a url:
https://rust-lang.org
Please vote (d)own or (u)p:
u
Got: 'Happy to confirm that you upvoted for https://rust-lang.org' from service
Please vote for a particular url
Please provide a url:
https://thorsten-hans.com
Please vote (d)own or (u)p:
u
Got: 'Happy to confirm that you upvoted for https://thorsten-hans.com' from service
Please vote for a particular url
Please provide a url:
Further optimizations
We can also combine client and server into a single rust project by adding two [[bin]]
blocks to Cargo.toml
. This would make perfect sense because they share all dependencies and the build instructions. However, having separate projects for client and server made it easier to explain all steps in proper order while maintaining the distinction between client and server.
On top of the core functionality, there are more crates available for tonic
. For example, consider adding tonic-reflection
to support gRPC service discoverability or tonic-health
to add standard gRPC health checking capabilities for every service.
These are definitely topics worth checking out my blog regularly to spot upcoming articles on those :D
What we’ve covered in this post
- Created rust projects and managed their dependencies with
cargo
- Implemented a service definition using protocol buffers
- configured
tonic-build
to generate necessary code in both client and server - implemented client and server logic
- bootstrapped the gRPC server and added the
VotingService
as an endpoint
Conclusion
gRPC is commonly used to build APIs in distributed systems (e.g., Microservices). I enjoyed diving into tonic
to do a quick Rust refresher. I will give it a spin when addressing more sophisticated (or real-world) requirements to see how it behaves under load while more complex API surfaces.
Additional capabilities like the previously mentioned health checking capabilities provided by tonic-health
sound quite exciting and are definitely on my list for the upcoming evenings :D