1. Introduction
In this codelab, you'll use gRPC-Rust to create a client and server that form the foundation of a route-mapping application written in Rust.
By the end of the tutorial, you will have a client that connects to a remote server using gRPC to get the name or postal address of what's located at specific coordinates on a map. A fully fledged application might use this client-server design to enumerate or summarize points of interest along a route.
The service is defined in a Protocol Buffers file, which will be used to generate boilerplate code for the client and server so that they can communicate with each other, saving you time and effort in implementing that functionality.
This generated code takes care of not only the complexities of the communication between the server and client, but also data serialization and deserialization.
What you'll learn
- How to use Protocol Buffers to define a service API.
- How to build a gRPC-based client and server from a Protocol Buffers definition using automated code generation.
- An understanding of client-server communication with gRPC.
This codelab is aimed at Rust developers new to gRPC or seeking a refresher of gRPC, or anyone else interested in building distributed systems. No prior gRPC experience is required.
2. Before you begin
Prerequisites
Make sure you have installed the following:
Get the code
So that you don't have to start entirely from scratch, this codelab provides a scaffold of the application's source code for you to complete. The following steps will show you how to finish the application, including using the protocol buffer compiler plugins to generate the boilerplate gRPC code.
First, create the codelab working directory and cd into it:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
Download and extract the codelab:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-rust-getting-started/start_here
Alternatively, you can download the .zip file containing only the codelab directory and manually unzip it.
The completed source code is available on GitHub if you want to skip typing in an implementation.
3. Define the service
Your first step is to define the application's gRPC service, its RPC method, and its request and response message types using Protocol Buffers. Your service will provide:
- An RPC method called
GetFeature
that the server implements and the client calls. - The message types
Point
andFeature
that are data structures exchanged between the client and server when using theGetFeature
method. The client provides map coordinates as aPoint
in itsGetFeature
request to the server, and the server replies with a correspondingFeature
that describes whatever is located at those coordinates.
This RPC method and its message types will all be defined in the proto/route_guide.proto
file of the provided source code.
Protocol Buffers are commonly known as protobufs. For more information on gRPC terminology, see gRPC's Core concepts, architecture, and lifecycle.
Service method
Let's first define our service methods and then define our message types Point
and Feature
. The proto/routeguide.proto
file has a service
structure named RouteGuide
that defines one or more methods provided by the application's service.
Add the rpc
method GetFeature
inside the RouteGuide
definition. As explained earlier, this method will look up the name or address of a location from a given set of coordinates, so have GetFeature
return a Feature
for a given Point
:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
This is a unary RPC method: a simple RPC where the client sends a request to the server and waits for a response to come back, just like a local function call.
Message types
In the proto/route_guide.proto
file of the source code, first define the Point
message type. A Point
represents a latitude-longitude coordinate pair on a map. For this codelab, use integers for the coordinates:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
The numbers 1
and 2
are unique ID numbers for each of the fields in the message
structure.
Next, define the Feature
message type. A Feature
uses a string
field for the name or postal address of something at a location specified by a Point
:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
4. Generate the client and server code
We have already given you the generated code from the .proto
file in the generated directory.
As with any project, we need to think of the dependencies that are necessary for our code. For Rust projects, the dependencies will be in Cargo.toml
. We have already listed the necessary dependencies in the Cargo.toml
file.
If you want to learn how to generate code from the .proto
file yourself, refer to these instructions.
The generated code contains:
- Struct definitions for message types
Point
andFeature
. - A service trait we'll need to implement:
route_guide_server::RouteGuide
. - A client type we'll use to call the server:
route_guide_client::RouteGuideClient<T>
.
Next, we'll implement the GetFeature
method on the server-side, so that when the client sends a request, the server can reply with an answer.
5. Implement the service
In src/server/server.rs
, we can bring the generated code into scope through gRPC's include_generated_proto!
macro and import the RouteGuide
trait and Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
We can start by defining a struct to represent our service, we can do this on src/server/server.rs
for now:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Now, we need to implement the route_guide_server::RouteGuide
trait from our generated code.
Unary RPC
The RouteGuideService
implements all our service methods. The get_feature
function on the server side is where the main work is done: this takes a Point
message from the client and returns in a Feature
message the corresponding location information from a list of known places. Here's the function's implementation in src/server/server.rs
:
#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
async fn get_feature(&self, request: Request<Point>) -> Result<Response<Feature>, Status> {
println!("GetFeature = {:?}", request);
let requested_point = request.get_ref();
for feature in self.features.iter() {
if feature.location().latitude() == requested_point.latitude() {
if feature.location().longitude() == requested_point.longitude(){
return Ok(Response::new(feature.clone()))
};
};
}
Ok(Response::new(Feature::default()))
}
}
In the method, populate a Feature
object with the appropriate information for the given Point
, and then return it.
Once we've implemented this method, we also need to start up a gRPC server so that clients can actually use our service. Replace main()
with this.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:10000".parse().unwrap();
println!("RouteGuideServer listening on: {addr}");
let route_guide = RouteGuideService {
features: load(),
};
let svc = RouteGuideServer::new(route_guide);
Server::builder().add_service(svc).serve(addr).await?;
Ok(())
}
Here's what is happening in main()
, step by step:
- Specify the port we want to use to listen for client requests
- Create a
RouteGuideService
with features loaded in by calling the helper functionload()
- Create an instance of the gRPC server using
RouteGuideServer::new()
using the service we created. - Register our service implementation with the gRPC server.
- Call
serve()
on the server with our port details to do a blocking wait until the process is killed.
6. Create the client
In this section, we'll look at creating a Rust client for our RouteGuide service in src/client/client.rs
.
As we did in src/server/server.rs
, we can bring the generated code into scope through gRPC's include_generated_code!
macro and import the RouteGuideClient
type.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
Call service methods
In gRPC-Rust, RPCs operate in a blocking/synchronous mode, which means that the RPC call waits for the server to respond, and will either return a response or an error.
To call service methods, we first need to create a channel to communicate with the server. We create this by first creating an endpoint, connecting to that endpoint, and passing the channel made when connected to RouteGuideClient::new()
as follows:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
Ok(())
}
In this function, when we create the client, we wrap the generic channel created above with the generated code stub that implements the specific methods defined in the .proto service.
Simple RPC
Calling the simple RPC GetFeature
is nearly as straightforward as calling a local method. Add this in main()
.
println!("*** SIMPLE RPC ***");
let point = proto!(Point{
latitude: 409_146_138,
longitude: -746_188_906
});
let response = client
.get_feature(Request::new(point))
.await?.into_inner();
Ok(())
As you can see, we call the method on the stub we got earlier. In our method parameters we create and populate a request protocol buffer object (in our case Point
). If the call doesn't return an error, then we can read the response information from the server from the first return value.
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
In all, the client's main()
function should look like this:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
//Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
println!("*** SIMPLE RPC ***");
let point = proto!(Point{
latitude: 409_146_138,
longitude: -746_188_906
});
let response = client
.get_feature(Request::new(point))
.await?.into_inner();
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
Ok(())
}
7. Try it out
First, to run our Client and Server, let's add them as binary targets to our crate. We need to edit our Cargo.toml
accordingly and add the following:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Then, execute the following commands from our working directory:
- Run the server in one terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Run the client from another terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
You'll see output like this, with timestamps omitted for clarity:
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906
8. What's next
- Learn how gRPC works in Introduction to gRPC and Core concepts
- Work through the Basics tutorial