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.
The Rust community has developed many gRPC implementations, notably the tonic
and grpc
crates. Both provide a full implementation of gRPC protocols.
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.
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.
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");
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.
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(()) }
To test our small application, run cargo run --bin server
and then cargo run --bin client
.
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"); }
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(); } }
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)); }
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 |
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 application. 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
5 Replies to "Rust and gRPC: A complete guide"
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
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.
Totally agree! Just go for the examples from the authors of the tonic crate. This is written by someone that isn’t ready to teach.
Hello, same here. I’m still confused about the generated rust code.
Better off just following https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md because this “guide” is badly formatted.
“Start by creating a protocol buffer file.” What should the file be called? This is far from a “complete guide”.