This is just a quick shell service, to show the basics of building a gRPC service and client from the perspective of code infrastructure. If you want to learn how to do interesting things in gRPC & Rust, I'd recommend starting with the Tonic RouteGuide.
This isn't anything fancy, and loosely inspired by this article, I just wanted to focus more on form than function.
cd ~/dev # everyone has their own place
mkdir grpc-service-rs
cd grpc-service-rs
cargo new --bin time-service-server
cargo new --bin time-service-client
cargo new --lib time-bindings
cargo new --lib time-common
mkdir -p proto/github/canardleteer/grpc_service_rs/v1alpha1
touch proto/github/canardleteer/grpc_service_rs/v1alpha1/time.proto
touch Cargo.toml
- I made this a workspace, by editing
Cargo.toml
. - I also added
rust-toolchain.toml
. - I added
time-*
relative path dependencies to other `time-*`` packages as appropriate. - And for all crates, while writing code, I added
Cargo.toml
entries as appropriate. - I later added some Docker "stuff," mostly in the
docker
directory.
I had to build out a protobuf declaration. This service just has 2 verbs, one that returns the time, and another that's not implemented and a stub.
I lint & format with buf.
I also created a default proto/buf.yaml
to scope rules, if needed.
# This should pass.
buf lint proto
# This should also pass.
buf format proto
I don't use buf
to build Rust bindings, I let prost
do that.
Normally, I would let proto
be a relative submodule, but not for this
example.
This is just a crate that builds the proto and manages the client/service generated code.
...to build the protobuf bindings. I keep these in a separate package than the
rest, just because this is a workspace, and it's reasonable to. The build.rs
could live in each one independently. build.rs
can become more interesting as you
start to add things, like derive macros to the messages, or multiple proto
files, but this is boring for this example.
This is generally just code generation shenanigans. YMMV.
-
You can dive deeper into using
buf
for Rust code generation, but it's not really necessary for this example. If you're interested, you can convert thetime-bindings/build.rs
file into abuf.gen.yaml
by using the protoc-gen-prost plugin. -
If you want to do something like JSON transcoding (or anything supported by serde), you can take a look at adding attributes to generated types with tonic_build. In general, annotations on generated code, is fairly useful.
-
There's a lot of useful stuff in tonic_build::Builder that's worth learning about (
generate_default_stubs
, for example).
If you don't have protoc
installed, now would be a good time to install it.
I recommend grabbing an upstream protoc
from https://github.com/google/protobuf/,
but you can probably find an older one on your system via the package manager.
On Ubuntu, for instance:
sudo apt install protobuf-compiler
This is the actual Service implementation of an extremely boring service. Also in here, is the Resource implementation for this particular gRPC binding.
I have not implemented the SomethingUnimplemented
method, just to show
that default implementations work and return NOT_IMPLEMENTED
.
- You can review the comments in the time-server & time-client implementations.
- You'll probably want grpcurl if you don't have it already.
In one terminal window:
cargo run --bin time-service-server
In another terminal window:
cargo run --bin time-service-client
The output, from your client run, should look something like:
2023-12-13T18:59:41.800217Z WARN client: Text to stdout Level set to: Some(LevelFilter::INFO)
at src/main.rs:84
Response from service was: 1702493981
And that's it! Leave the service running, we're going to do some additional introspection.
grpcurl -plaintext localhost:50051 describe
And you should see everything, including the comments in your proto file.
-> grpcurl -plaintext localhost:50051 describe
github.canardleteer.grpc_service_rs.v1alpha1.SimpleTimestampService is a service:
service SimpleTimestampService {
// This exists to show an unimplemented method.
rpc SomethingUnimplemented ( .github.canardleteer.grpc_service_rs.v1alpha1.WhatTimeIsItRequest ) returns ( .github.canardleteer.grpc_service_rs.v1alpha1.WhatTimeIsItResponse );
// Returns the services current timestamp with no additional information.
rpc WhatTimeIsIt ( .github.canardleteer.grpc_service_rs.v1alpha1.WhatTimeIsItRequest ) returns ( .github.canardleteer.grpc_service_rs.v1alpha1.WhatTimeIsItResponse );
}
grpc.health.v1.Health is a service:
service Health {
// If the requested service is unknown, the call will fail with status
// NOT_FOUND.
rpc Check ( .grpc.health.v1.HealthCheckRequest ) returns ( .grpc.health.v1.HealthCheckResponse );
// Performs a watch for the serving status of the requested service.
// The server will immediately send back a message indicating the current
// serving status. It will then subsequently send a new message whenever
// the service's serving status changes.
//
// If the requested service is unknown when the call is received, the
// server will send a message setting the serving status to
// SERVICE_UNKNOWN but will *not* terminate the call. If at some
// future point, the serving status of the service becomes known, the
// server will send a new message with the service's serving status.
//
// If the call terminates with status UNIMPLEMENTED, then clients
// should assume this method is not supported and should not retry the
// call. If the call terminates with any other status (including OK),
// clients should retry the call with appropriate exponential backoff.
rpc Watch ( .grpc.health.v1.HealthCheckRequest ) returns ( stream .grpc.health.v1.HealthCheckResponse );
}
grpc.reflection.v1alpha.ServerReflection is a service:
service ServerReflection {
// The reflection service is structured as a bidirectional stream, ensuring
// all related requests go to a single server.
rpc ServerReflectionInfo ( stream .grpc.reflection.v1alpha.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1alpha.ServerReflectionResponse );
}
grpcurl -plaintext localhost:50051 list
And you'll see what we offer:
-> grpcurl -plaintext localhost:50051 list
github.canardleteer.grpc_service_rs.v1alpha1.SimpleTimestampService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
If you want to download grpc-health-probe,
you can and use that, or you can use grpcurl
.
-> grpc_health_probe -addr 127.0.0.1:50051
status: SERVING
-> grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
{
"status": "SERVING"
}
You can build the container:
docker build -t time:latest -f docker/Dockerfile .
You can test the container:
docker network create time-network
docker run --rm -it -d \
--name time_server \
--net time-network \
-p 50051:50051 \
time:latest
docker run --rm -it \
--net time-network \
-e USE_CLIENT_BINARY=true \
time:latest -a time_server
# cleanup
docker rm -f time_server
docker network remove time-network
- We run the following steps in our GitHub Actions for all branches & PRs:
buf lint
cargo check
cargo fmt
cargo clippy
cargo test
- For Pull Requests, we perform a:
buf breaking
- For pushes to "special" branches, we perform a:
buf push
docker build
docker push
I've omitted versioning for this example, but auto versioning pipelines are easy. I the past I've had luck with cargo-smart-release, but there are likley more and better tools now.
- We can view the schema in the Buf Schema Registry: https://buf.build/canardleteer/grpc-service-rs.
- We can view the docker image on Dockerhub: https://hub.docker.com/r/canardleteer/grpc-service-rs
Basic docker compose
with Envoy:
docker compose up --build
grpcurl -plaintext localhost:10200 describe
grpcurl -plaintext localhost:10200 github.canardleteer.grpc_service_rs.v1alpha1.SimpleTimestampService/WhatTimeIsIt
NOTE: I haven't quite gotten gRPC transcoding working yet.
- There is a mostly commented out configuration in a second Envoy listener in
envoy/envoy.yaml
- There is a commented out file mapping for
time_service.binpb
indocker-compose.yaml
To generate envoy/time_service.binpb
, you'll need to do the following:
buf build --as-file-descriptor-set -o envoy/time_service.binpb