Original link: https://jasonkayzk.github.io/2022/12/03/Rust%E7%9A%84GRPC%E5%AE%9E%E7%8E%B0Tonic/
Tonic is an asynchronous implementation of a GRPC client and server in rust. The bottom layer uses tokio’s prost
to generate the code corresponding to Protocol Buffers;
This article explains how to use Tonic, and provides a project case that contains multiple proto files;
source code:
Rust’s gRPC implementation of Tonic
foreword
tonic
is a gRPC implementation based on HTTP/2, focusing on high performance, interoperability and flexibility;
The library was created to have first-class support for async/await and to serve as a core building block for production systems written in Rust;
characteristic:
- bidirectional streaming
- High performance asynchronous io
- interoperability
- TLS encryption support via
rustls
- load balancing
- custom metadata
- Authentication
- health examination
- …
Compiling Protobuf still needs to install protoc, you can refer to the official documentation:
In addition, in addition to this implementation, PingCAP also open sourced an implementation:
I tried it, and to be honest, it is not as easy to use as Tonic, but his benchmark is slightly higher;
Let’s start writing a project case that contains multiple proto files;
create project
The final directory structure is as follows:
$ tree . .├── Cargo.toml├── Cargo.lock ├── build.rs├── proto │ ├── basic │ │ └── basic.proto │ ├── goodbye.proto │ └── hello.proto └── src ├── bin │ ├── client.rs │ └── server.rs └── lib.rs
in:
- Services are defined in the
proto
directory; - The script to generate the rs file through proto is declared in
build.rs
; - The rs file generated after compiling proto in
lib.rs
is introduced intobuild.rs
; - The implementation of the client and server are defined in the
bin
directory;
First create a lib project:
cargo new tonic-demo --lib
In this lib, we implement the service code, and implement the client and server
through the client
and server in the bin
directory;
Modify the Cargo configuration:
Cargo.toml
[[bin]]name="server"path="src/bin/server.rs"[[bin]]name="client"path="src/bin/client.rs"[dependencies]prost = "0.11.3"tokio = { version = "1.19.2", features = ["macros", "rt-multi-thread"] }tonic = "0.8.3"[build-dependencies]tonic-build = "0.8.4"
define service
Create a proto directory and declare the corresponding services;
Because most of the information on the Internet is a proto file, and the actual project basically has a hierarchical structure;
So here I also use multiple proto files to demonstrate;
It is defined as follows:
// tonic-demo/proto/basic/basic.protosyntax = "proto3";package basic;message BaseResponse { string message = 1; int32 code = 2;}// tonic-demo/proto/hello.protosyntax = "proto3";import "basic/basic.proto";package hello;service Hello { rpc Hello(HelloRequest) returns (HelloResponse) {}}message HelloRequest { string name = 1;}message HelloResponse { string data = 1; basic.BaseResponse message = 2;}// tonic-demo/proto/goodbye.protosyntax = "proto3";import "basic/basic.proto";package goodbye;service Goodbye { rpc Goodbye(GoodbyeRequest) returns (GoodbyeResponse) {}}message GoodbyeRequest { string name = 1;}message GoodbyeResponse { string data = 1; basic.BaseResponse message = 2;}
Defined in the proto/basic
directory: BaseResponse
;
And he was introduced in both hello.proto
and goodbye.proto
;
configure compile
Let’s look at build.rs, which is also the key to compiling protobuf files!
As we all know, the code defined in build.rs
will be executed before the project code is actually compiled, and it is used to do some tricks before compiling the real project;
Therefore, we can first compile the protobuf file here;
In the above Cargo configuration we introduced:
[build-dependencies]tonic-build = "0.8.4"
Therefore it is used here:
build.rs
use std::error::Error;use std::fs;static OUT_DIR: &str = "src/proto-gen";fn main() -> Result<(), Box<dyn Error>> { let protos = [ "proto/basic/basic.proto", "proto/hello.proto", "proto/goodbye.proto", ]; fs::create_dir_all(OUT_DIR).unwrap(); tonic_build::configure() .build_server(true) .out_dir(OUT_DIR) .compile(&protos, &["proto/"])?; rerun(&protos); Ok(())}fn rerun(proto_files: &[&str]) { for proto_file in proto_files { println!("cargo:rerun-if-changed={}", proto_file); }}
First, declare the proto file we want to compile, and then create the output location of the compiled proto file (default is in the target/build
directory);
Finally, use tonic_build
compile the file on the server side;
After the project is compiled, the compiled proto file will be output to our defined src/proto-gen
;
tonic-demo/src/proto-gen/basic.rs
#[derive(Clone, PartialEq, ::prost::Message)]pub struct BaseResponse { #[prost(string, tag = "1")] pub message: ::prost::alloc::string::String, #[prost(int32, tag = "2")] pub code: i32,}
tonic-demo/src/proto-gen/hello.rs
#[derive(Clone, PartialEq, ::prost::Message)]pub struct HelloRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String,}#[derive(Clone, PartialEq, ::prost::Message)]pub struct HelloResponse { #[prost(string, tag = "1")] pub data: ::prost::alloc::string::String, #[prost(message, optional, tag = "2")] pub message: ::core::option::Option<super::basic::BaseResponse>,}/// Generated client implementations.pub mod hello_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; use tonic::codegen::http::Uri; #[derive(Debug, Clone)] pub struct HelloClient<T> { inner: tonic::client::Grpc<T>, } impl HelloClient<tonic::transport::Channel> { /// Attempt to create a new client by connecting to a given endpoint. pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: std::convert::TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, { let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; Ok(Self::new(conn)) } } impl<T> HelloClient<T> where T: tonic::client::GrpcService<tonic::body::BoxBody>, T::Error: Into<StdError>, T::ResponseBody: Body<Data = Bytes> + Send + 'static, <T::ResponseBody as Body>::Error: Into<StdError> + Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor<F>( inner: T, interceptor: F, ) -> HelloClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request<tonic::body::BoxBody>, Response = http::Response< <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, >, >, <T as tonic::codegen::Service< http::Request<tonic::body::BoxBody>, >>::Error: Into<StdError> + Send + Sync, { HelloClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } pub async fn hello( &mut self, request: impl tonic::IntoRequest<super::HelloRequest>, ) -> Result<tonic::Response<super::HelloResponse>, tonic::Status> { self.inner .ready() .await .map_err(|e| { tonic::Status::new( tonic::Code::Unknown, format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/hello.Hello/Hello"); self.inner.unary(request.into_request(), path, codec).await } }}/// Generated server implementations.pub mod hello_server { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with HelloServer. #[async_trait] pub trait Hello: Send + Sync + 'static { async fn hello( &self, request: tonic::Request<super::HelloRequest>, ) -> Result<tonic::Response<super::HelloResponse>, tonic::Status>; } #[derive(Debug)] pub struct HelloServer<T: Hello> { inner: _Inner<T>, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, } struct _Inner<T>(Arc<T>); impl<T: Hello> HelloServer<T> { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc<T>) -> Self { let inner = _Inner(inner); Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), } } pub fn with_interceptor<F>( inner: T, interceptor: F, ) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } } impl<T, B> tonic::codegen::Service<http::Request<B>> for HelloServer<T> where T: Hello, B: Body + Send + 'static, B::Error: Into<StdError> + Send + 'static, { type Response = http::Response<tonic::body::BoxBody>; type Error = std::convert::Infallible; type Future = BoxFuture<Self::Response, Self::Error>; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request<B>) -> Self::Future { let inner = self.inner.clone(); match req.uri().path() { "/hello.Hello/Hello" => { #[allow(non_camel_case_types)] struct HelloSvc<T: Hello>(pub Arc<T>); impl<T: Hello> tonic::server::UnaryService<super::HelloRequest> for HelloSvc<T> { type Response = super::HelloResponse; type Future = BoxFuture< tonic::Response<Self::Response>, tonic::Status, >; fn call( &mut self, request: tonic::Request<super::HelloRequest>, ) -> Self::Future { let inner = self.0.clone(); let fut = async move { (*inner).hello(request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let inner = self.inner.clone(); let fut = async move { let inner = inner.0; let method = HelloSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { Ok( http::Response::builder() .status(200) .header("grpc-status", "12") .header("content-type", "application/grpc") .body(empty_body()) .unwrap(), ) }) } } } } impl<T: Hello> Clone for HelloServer<T> { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, } } } impl<T: Hello> Clone for _Inner<T> { fn clone(&self) -> Self { Self(self.0.clone()) } } impl<T: std::fmt::Debug> std::fmt::Debug for _Inner<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) } } impl<T: Hello> tonic::server::NamedService for HelloServer<T> { const NAME: &'static str = "hello.Hello"; }}
have to be aware of is:
The generated HelloClient
type for the client:
- Implemented Clone, Sync and Send, so it can be used across threads;
The HelloServer
type generated for the server:
- Contains
impl<T: Hello>
, so it is required to implement the Hello Trait we defined;
Import the files generated by proto
Below we introduce the files generated by lib.rs
in lib.rs:
lib.rs
pub mod basic { include!("./proto-gen/basic.rs");}pub mod hello { include!("./proto-gen/hello.rs");}pub mod goodbye { include!("./proto-gen/goodbye.rs");}
The include!
provided by the standard library is used here to import the file;
If you do not define the output location of the compiled proto file, it will be in the target/build
directory by default;
At this time, you can also use the include_proto!("hello")
macro provided by tonic to directly import the corresponding file without specifying an additional path;
Refer to the official documentation:
Server implementation
The following implements the server side;
The implementation of the server is basically similar to other languages, just create a corresponding Service implementation for the Service defined by the corresponding proto:
tonic-demo/src/bin/server.rs
#[derive(Default)]pub struct HelloService {}#[tonic::async_trait]impl Hello for HelloService { async fn hello(&self, req: Request<HelloRequest>) -> Result<Response<HelloResponse>, Status> { println!("hello receive request: {:?}", req); let response = HelloResponse { data: format!("Hello, {}", req.into_inner().name), message: Some(BaseResponse { message: "Ok".to_string(), code: 200, }), }; Ok(Response::new(response)) }}#[derive(Default)]pub struct GoodbyeService {}#[tonic::async_trait]impl Goodbye for GoodbyeService { async fn goodbye( &self, req: Request<GoodbyeRequest>, ) -> Result<Response<GoodbyeResponse>, Status> { println!("goodbye receive request: {:?}", req); let response = GoodbyeResponse { data: format!("Goodbye, {}", req.into_inner().name), message: Some(BaseResponse { message: "Ok".to_string(), code: 200, }), }; Ok(Response::new(response)) }}#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "0.0.0.0:50051".parse()?; println!("server starting at: {}", addr); Server::builder() .add_service(HelloServer::new(HelloService::default())) .add_service(GoodbyeServer::new(GoodbyeService::default())) .serve(addr) .await?; Ok(())}
Implement the corresponding logic of the interface in the corresponding Trait, and finally register the Service in the main, the logic is very clear;
client implementation
The implementation of the client is even simpler. First, create an Endpoint connection through the address, and then directly call the corresponding function:
tonic-demo/src/bin/client.rs
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = Endpoint::from_static("https://127.0.0.1:50051"); let mut hello_cli = HelloClient::connect(addr.clone()).await?; let request = Request::new(HelloRequest { name: "tonic".to_string(), }); let response = hello_cli.hello(request).await?; println!("hello response: {:?}", response.into_inner()); let mut goodbye_cli = GoodbyeClient::connect(addr).await?; let request = Request::new(GoodbyeRequest { name: "tonic".to_string(), }); let response = goodbye_cli.goodbye(request).await?; println!("goodbye response: {:?}", response.into_inner()); Ok(())}
Is it very simple;
test
Let’s test it out, first start the server:
$ cargo run --bin server server starting at: 0.0.0.0:50051
Restart the client:
$ cargo run --bin client hello response: HelloResponse { data: "Hello, tonic", message: Some(BaseResponse { message: "Ok", code: 200 }) }goodbye response: GoodbyeResponse { data: "Goodbye, tonic", message: Some(BaseResponse { message: "Ok", code: 200 }) }
The client receives the response, and the server logs:
hello receive request: Request { metadata: MetadataMap { headers: {"te": "trailers", "content-type": "application/grpc", "user-agent": "tonic/0.8.3"} }, message: HelloRequest { name: "tonic" }, extensions: Extensions }goodbye receive request: Request { metadata: MetadataMap { headers: {"te": "trailers", "content-type": "application/grpc", "user-agent": "tonic/0.8.3"} }, message: GoodbyeRequest { name: "tonic" }, extensions: Extensions }
Steps need to be added in Github Action:
- name: Install protoc run: sudo apt-get install -y protobuf-compiler
install protoc;
Reference Code:
Summarize
It can be seen that compared to other languages, using grpc in Rust is simpler, and there is no need to write additional shell scripts generated by protoc, but it is more elegantly implemented through build.rs!
More ways to use tonic:
- Official examples given by tonic, such as streaming (Stream) grpc, load balancing, verification with tls certificate, etc.: https://github.com/hyperium/tonic/tree/master/examples
- To write streaming grpc, it is recommended to see: https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md
appendix
source code:
Open source library:
Reference article:
- https://cn.pingcap.com/blog/grpc
- https://cn.pingcap.com/blog/grpc-rs
- https://www.pingcap.com/blog/futures-and-grpc-in-rust/
- https://rustcc.cn/article?id=21934c4e-60eb-4796-80c2-70c4733032e1
- https://rust-book.junmajinlong.com/ch101/02_Protobuf_tonic.html
- https://medium.com/geekculture/quick-start-to-grpc-using-rust-c655785fc6f4
This article is reproduced from: https://jasonkayzk.github.io/2022/12/03/Rust%E7%9A%84GRPC%E5%AE%9E%E7%8E%B0Tonic/
This site is only for collection, and the copyright belongs to the original author.