Write A Simple Grpc Server In Rust In Five Minutes
Write a simple gRPC server in rust in less than 5 minutes
If you’ve seen my article on writing gRPC servers in go, this article will be very similar, but a lot more magical, thanks to the amaaaazing tonic
crate in the tokio
stack.
Let’s get started
Create a crate, and add the dependencies
1
2
3
4
(base) Documents ❯ cargo new calculator
(base) Documents ❯ cd calculator
(base) calculator git:master ❯ cargo add tokio -F full (base) calculator git:master ❯ cargo add tonic
(base) calculator git:master ❯ cargo add tonic-build
Go ahead and install the protobuf compiler with your package manager of choice
1
(base) calculator git:master ❯ brew install protobuf
Create a proto
directory in your project root, next to your src
directory, and define your protobuf messages and rpc’s
1
2
(base) calculator git:master ❯ mkdir proto
(base) calculator git:master ❯ touch proto/calc.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";
package calc;
service CalcService {
rpc Add(Input) returns (Result);
rpc Subtract(Input) returns (Result);
rpc Multiply(Input) returns (Result);
rpc Divide(Input) returns (Result);
}
message Input {
int32 a = 1;
int32 b = 2;
}
message Result {
int32 c = 1;
}
The four rpc’s here accept the Input
message and returns it’s result in the Result
crate
The magic part
Usually at this stage, you’d have to jump to the gRPC docs to hunt for the protoc
command to generate the proto code, and worry about placing it somewhere in your package, which can become a hassle unless you are in go and have first class support from gRPC itself.
Take a look at quickgrpc if you need to do this in python
Anyway…
in rust, we have amazing tooling from tonic, thanks to macro magic which completely hides all this from the developer, so you can focus on implementing your logic the rusty way 😄
So this works by telling the compiler to invoke a macro provided by tonic at compile time, so the generated code is available for use as just another crate, as you’ll see soon
Start by adding a build.rs
in your project root
1
2
3
4
5
6
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
tonic_build::compile_protos("proto/calc.proto")?;
Ok(())
}
Cool, now let’s import these and implement the rpc methods, in your main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
use std::error::Error;
use proto::calculator_server::{Calculator, CalculatorServer};
use tonic::transport::Server;
mod proto{
tonic::include_proto!("calc");
}
#[derive(Debug, Default)]
struct CalcService{}
#[tonic::async_trait]
impl Calculator for CalcService{
async fn add(
&self,
req: tonic::Request<proto::Input>
) -> Result<tonic::Response<proto::Result>, tonic::Status>{
let input = req.get_ref();
let result = proto::Result{c: input.a+input.b};
Ok(tonic::Response::new(result))
}
async fn subtract(
&self,
req: tonic::Request<proto::Input>
) -> Result<tonic::Response<proto::Result>, tonic::Status>{
let input = req.get_ref();
let result = proto::Result{c: input.a-input.b};
Ok(tonic::Response::new(result))
}
async fn multiply(
&self,
req: tonic::Request<proto::Input>
) -> Result<tonic::Response<proto::Result>, tonic::Status>{
let input = req.get_ref();
let result = proto::Result{c: input.a*input.b};
Ok(tonic::Response::new(result))
}
async fn divide(
&self,
req: tonic::Request<proto::Input>
) -> Result<tonic::Response<proto::Result>, tonic::Status>{
let input = req.get_ref();
let result = proto::Result{c: input.a/input.b};
Ok(tonic::Response::new(result))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>>{
let calc = CalcService::default();
Server::builder()
.add_service(CalculatorServer::new(calc))
.serve("[::1]:3000".parse()?)
.await?;
Ok(())
}
note: since tonic needs the trait to implement async methods, you need to add an macro to enable that.
- The handler receives a
tonic::Request
containing a struct that corresponds to your proto message- Your can get a reference to your message struct using theget_ref
method - Once you perform your logic, you can intantiate the struct corresponding to your result message and wrap it in
tonic::Response
and return
Finally, let’s set up the server
1
2
3
4
5
6
7
8
9
10
11
12
use proto::calculator_server::CalculatorServer;
use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>>{
let calc = CalcService::default();
Server::builder()
.add_service(CalculatorServer::new(calc))
.serve("[::1]:3000".parse()?)
.await?;
Ok(())
}