loading...

Rust gRPC A beginners guide to gRPC with Rust

anshulgoyal15 profile image Anshul Goyal Updated on ・16 min read

Table of Contents

  1. Introduction
  2. Protocol Buffer
  3. Rust and gRPC
  4. Creating a Server
  5. Creating a Client
  6. Streaming in gRPC
  7. Authentication
  8. Conclusion

Introduction

HTTP and JSON is a very popular method for creating web APIs. HTTP and JSON make sense because it uses a very popular protocol. HTTP and JSON are text-based protocol which causes a performance problem since serializing JSON is a slow process, most HTTP and JSON or Rest APIs do not support streaming which means we cannot start processing the data before it arrives. Rest APIs have very good tooling and community support. Almost every programming language has a high-quality implementation for HTTP and serializing JSON. Rest architecture doesn’t fit every use case, it is difficult to provide client library for Rest APIs for every language and maintain these libraries. Since there is no language-independent method for defining the structure of JSON and HTTP requests, therefore, it is difficult to generate client libraries. gRPC is an attempt to tackle these problems.

Brief Intro to gRPC

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, backends to one and other. gRPC came with plug able support for load balancing, authentication, tracing, etc. gRPC supports bidirectional streaming over HTTP/2. gRPC provides an idiomatic implementation in 10 languages. gRPC can generate efficient client libraries and uses the protocol buffer format for transferring data over the wire. Protocol buffers are a binary format for data transmission. Since protocol buffers are a binary protocol, it can be serialized fast and the structure of each message must be predefined.

A Little about Rust

Rust is a systems programming language. Rust provides high level ergonomic with low-level control. Rust provides control over memory management without the hassle associated with it. Rust has good support for Asynchronous operation making it a good fit for writing networking applications. Rust has zero cost abstraction making it blazing fast.

Work Flow Source(https://blog.logrocket.com/creating-a-crud-api-with-node-express-and-grpc/)

Protocol Buffers

Protocol buffers are extensible, language-neutral data serializing mechanism. It is fast, small, and simple. Protocol buffers have a predefined structure with its syntax for defining messages and services. Services are functions that can be executed and messages are arguments passed to the function and values returned by these functions. There are two versions of protocol buffers. This tutorial would use version 3.

Syntax

Protocol Buffers have a very simple syntax. There are two things to be defined in a protocol buffer.

  • service It defines all the functionality that can be called on a particular service or server.
  • message It defines arguments and returns values of an RPC call.

The syntax and package must be defined in every protocol buffer file. Protocol buffer files are saved with .proto extension.

    // 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;
    }
Enter fullscreen mode Exit fullscreen mode

A service is defined by using service keyword then defining call using rpc keyword, send is the name of the call, it can be used to make a call, SayRequest define the argument send call takes and SayResponse defines the value returned by the call. Any number of the call can be defined in service.

    service Say {
    // function which can be called
      rpc Send (SayRequest) returns (SayResponse);
      rpc Take (SayRequest) returns (SayResponse);
    }
Enter fullscreen mode Exit fullscreen mode

Implementation of these function is not defined in the *.proto* file, these implementations are provided by the server

Assign Number to Fields
The number assigned to a field is very important because it is used to recognize the field in binary data. It takes 1 byte to encode 0-15 numbers and 2 bytes for encoding 16-2047, it is wise to use 0-15 for frequently occurring data. It is also recommended to reserve a few numbers so that, these reserved numbers can be used later if some changes are made to format.

Different Data Types

Prototype support many data types include string, int, float, boolean, etc. These types can be repeated using repeated field attributes.

Protocol buffer syntax is explained in great detail in official documentation.

Rust and gRPC

Rust ecosystem has grown quite big with very good quality crates. tonic is very performant gRPC implementation for Rust. This tutorial uses tonic as the gRPC implementation and tonic-build for compiling .proto files to client libraries.
Let us start by creating a new cargo project using cargo init. Now we need to create an add a few dependencies and build dependencies. These will help us with our server and client.

    [package]
    name = "grpc"
    version = "0.1.0"
    authors = ["Anshul Goyal <anshulgoel151999@gmail.com>"]
    edition = "2018"

    [dependencies]
    prost = "0.6.1"
    tonic = {version="0.2.0",features = ["tls"]}
    tokio = {version="0.2.18",features = ["stream", "macros"]}
    futures = "0.3"

    [build-dependencies]
    tonic-build = "0.2.0"
Enter fullscreen mode Exit fullscreen mode

This should be a configuration for Cargo.toml file. prost provides basic types for gRPC, tokio provide asynchronous runtime and futures for handling asynchronous streams.

Compiling Protocol Buffers

We would use build.rs for compiling our .proto files and include then in binary. tonic-build crate provides a method compile_protos which take the path to .ptoto file and compile it to rust definitions. First, we create a folder in the root directory named proto it will contain all of out .proto files. We create a say.proto file in this directory. With our Say service shown in the above example.

We create a build.rs with the following code.

    fn main()->Result<(),Box<dyn std::error::Error>>{
    // compiling protos using path on build time
       tonic_build::compile_protos("proto/say.proto")?;
       Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

The above code will compile proto/say.proto file and save it in an OUT_DIR and add an environment variable OUT_DIR which is available at build time so that we can use it later in our code. We can also provide different options for compiling the protocol buffers. Now your directory structure should look like this:

    ├── build.rs
    ├── Cargo.lock
    ├── Cargo.toml
    ├── proto
    │   └── say.proto
    ├── src
        ├── main.rs
Enter fullscreen mode Exit fullscreen mode

Now we have compiled our .proto files we would use it in our code using tonic utility. We would create a module for our server and client. Let us name it hell.rs and we would add the following code.

    // this would include code generated for package hello from .proto file
    tonic::include_proto!("hello");

Enter fullscreen mode Exit fullscreen mode

Creating a Server

Now we have compiled the protocol buffers we are ready to build our server. We have to provide the implementation for every service and rpc we defined. Service would be defined as traits and rpc would be a member function on these traits. Since Rust doesn't support async traits we have to use an asyc_trait macro for overcoming this limitation. We create a file named server.rs and add the following code.

**tonic-build** would automatically compile **.proto** following rust naming conventions and best practices.

    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(())
    }
Enter fullscreen mode Exit fullscreen mode

In Rust, messages are represented as structs and services as traits and RPC as functions. We impl the trait for our struct and pass it to our server. In our example, we would create a send function which takes Request as an argument which contains details about the request and wraps our message SayRequest which can be accessed using .get_ref method. Now let us run this by adding a bin block to our Cargo.toml.

    [[bin]]
    name = "server"
    path = "src/server.rs"
Enter fullscreen mode Exit fullscreen mode

This will help us testing and maintaining code in save repo but it is not suggested for a large project.
Now if we run this with command cargo run --bin server. We can see our server running at 127:0:0:1:50051.

Example

Creating a Client

Our server is ready, now let's test it by creating a client. Since we have compiled our protocol buffer we can import our hello.rs file and use it. We create a client.rs file and add the following code.

    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(())
    }
Enter fullscreen mode Exit fullscreen mode

We add a bin key to our Cargo.toml

    [[bin]]
    name = "client"
    path = "src/client.rs"
Enter fullscreen mode Exit fullscreen mode

We create a channel that is an HTTP/2 connection that can be used then from our client. HTTP/2 support streams that can be used by gRPC. Now if we run our client with command cargo run --bin client we can see see the response.

Error Handling

Error handling in gRPC is done using Status. tonic provide Status enum which can be returned in case of error with appropriate error message.

    Err(Status::unauthenticated("Token not found"))
Enter fullscreen mode Exit fullscreen mode

gRPC support bare-bone error handling but you can extend yourself using protocol buffer here is detail explanation

Streaming In gRPC

HTTP/2 supports streaming and gRPC provides a nice interface for using it. We can start sending the response even before the client finish sends the request. We can use it to provide an efficient service. The server has not to wait for the request from the client to complete the request. We need to make a few changes to our protocol buffers so that it reflects that we support streaming. We need to make changes to our rust code also. Rust has quite good support for asynchronous I/O. We would tokio to stream response and request.

Streaming Response

We would start by the streaming server since most of the time server would be sending a large amount of data. We would use a queue for sending data by multiplexing different task on a single thread. tokio provide very excellent multi sender single receiver channel.

Changes to protocol buffers

We change the code of protocol buffer to the following. We use a stream keyword in rpc and specify that the rpc call will return a stream of messages SayResponse.

    service Say {
    // function which can be called
      rpc Send (SayRequest) returns (SayResponse);
    // we specify that we return a stream
      rpc SendStream(SayRequest) returns (stream SayResponse);
    }
Enter fullscreen mode Exit fullscreen mode

Changes to Server Code
We would use tokio::sync::mpsc for communicating between futures. We send multiple responses using this channel. We would use tokio::spawn to create a new task that can be then scheduled. We add the following code to our server.rs file.

    use tokio::sync::mpsc;
    use tonic::{transport::Server, Request, Response, Status};
    use hello::say_server::{Say, SayServer};
    use hello::{SayRequest, SayResponse};
    mod hello;

    #[derive(Default)]
    pub struct MySay {}
    #[tonic::async_trait]
    impl Say for MySay {
    // Specify the output of rpc call
        type SendStreamStream=mpsc::Receiver<Result<SayResponse,Status>>;
    // implementation for rpc call
        async fn send_stream(
            &self,
            request: Request<SayRequest>,
        ) -> Result<Response<Self::SendStreamStream>, Status> {
    // creating a queue or channel
            let (mut tx, rx) = mpsc::channel(4);
    // creating a new task
            tokio::spawn(async move {
    // looping and sending our response using stream
                for _ in 0..4{
    // sending response to our channel
                    tx.send(Ok(SayResponse {
                        message: format!("hello"),
                    }))
                    .await;
                }
            });
    // returning our reciever so that tonic can listen on reciever and send the response to client
            Ok(Response::new(rx))
        }
        async fn send(&self, request: Request<SayRequest>) -> Result<Response<SayResponse>, Status> {
            Ok(Response::new(SayResponse {
                message: format!("hello {}", request.get_ref().name),
            }))
        }
    }

    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let addr = "[::1]:50051".parse().unwrap();
        let say = MySay::default();
        println!("Server listening on {}", addr);
        Server::builder()
            .add_service(SayServer::new(say))
            .serve(addr)
            .await?;
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

We need to change the main function, we just add a new function to trait and a type to specify our output. In this new send_stream function we create a channel so that we can send a response and return the receiver. The receiver implements theStream trait so it can be streamed by HTTP/2 and the sender can be used by multiple threads and it implements Sink trait. We have created a bounded channel but we can also use an unbounded channel.

Changes in Client Code
We need to make changes to response handling. Since it would be a stream now, we would just listen to this stream and print the response. Streams help to write non-blocking code and use resources more efficiently.

    use hello::say_client::SayClient;
    use hello::SayRequest;
    mod hello;

    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
        .connect()
        .await?;
        let mut client = SayClient::new(channel);
        let request = tonic::Request::new(
            SayRequest {
               name:String::from("anshul")
            },
        );
    // now the response is stream
        let mut response = client.send_stream(request).await?.into_inner();
    // listening to stream
        while let Some(res) = response.message().await? {
            println!("NOTE = {:?}", res);
        }
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Streaming Request

Sometimes all the data is not available, for example in a game all the data is not available then it would make stream the data and send all the data available and sending rest when available. This allows using data more efficiently on user devices. We need a few changes to our code.

Changes to protocol buffer
We would use the stream keyword to specify the argument is a stream. We would use stream to specify that our rpc takes a stream as an argument.

    // service which can be executed
    service Say {
    // function which can be called
      rpc Send (SayRequest) returns (SayResponse);
      rpc SendStream(SayRequest) returns (stream SayResponse);
    // taking a stream as response
      rpc ReceiveStream(stream SayRequest) returns (SayResponse);
    }
Enter fullscreen mode Exit fullscreen mode

Changes to Server
We need our server to accept the stream as the request. We would listen on the stream and collect. Then we would respond when the stream finishes. It will save our resources since we can wait on stream asynchronously.

    use tokio::sync::mpsc;
    use tonic::{transport::Server, Request, Response, Status};
    use hello::say_server::{Say, SayServer};
    use hello::{SayRequest, SayResponse};
    mod hello;
    #[derive(Default)]
    pub struct MySay {}
    #[tonic::async_trait]
    impl Say for MySay {
    // .. rest of rpcs
    // create a new rpc to receive a stream
        async fn receive_stream(
            &self,
            request: Request<tonic::Streaming<SayRequest>>,
        ) -> Result<Response<SayResponse>, Status> {
    // converting request into stream
            let mut stream = request.into_inner();
            let mut message = String::from("");
    // listening on stream
            while let Some(req) = stream.message().await? {
                message.push_str(&format!("Hello {}\n", req.name))
            }
    // returning response
            Ok(Response::new(SayResponse { message }))
        }
    }
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let addr = "[::1]:50051".parse().unwrap();
        let say = MySay::default();
        println!("Server listening on {}", addr);
        Server::builder()
            .add_service(SayServer::new(say))
            .serve(addr)tes());
            .await?;
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Changes to Client
We would now program our client to send a stream to our server. For this, we would mimic a stream using futures crate and create a stream from a vector.

    use futures::stream::iter;
    use hello::say_client::SayClient;
    use hello::SayRequest;
    mod hello;
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
            .connect()
            .await?;
        let mut client = SayClient::new(channel);
    // creating a stream
        let request = tonic::Request::new(iter(vec![
            SayRequest {
                name: String::from("anshul"),
            },
            SayRequest {
                name: String::from("rahul"),
            },
            SayRequest {
                name: String::from("vijay"),
            },
        ]));
    // sending stream
        let response = client.receive_stream(request).await?.into_inner();
        println!("RESPONSE=\n{}", response.message);
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Bidirectional Stream

The bidirectional stream is also supported by gRPC. The bidirectional stream is just a combination of streaming requests and streaming responses. Here is a quick example.

Protocol buffer


    // version of protocol buffer used
    syntax = "proto3";
    // package name for buffer will be used later
    package hello;
    // service which can be executed
    service Say {
    // takes a stream and returns a stream
      rpc Bidirectional(stream SayRequest) returns (stream 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;
    }
Enter fullscreen mode Exit fullscreen mode

Sever

    use tokio::sync::mpsc;
    use tonic::{transport::Server, Request, Response, Status};
    use hello::say_server::{Say, SayServer};
    use hello::{SayRequest, SayResponse};
    mod hello;
    #[derive(Default)]
    pub struct MySay {}
    #[tonic::async_trait]
    impl Say for MySay {
    // defining return stream
        type BidirectionalStream = mpsc::Receiver<Result<SayResponse, Status>>;
        async fn bidirectional(
            &self,
            request: Request<tonic::Streaming<SayRequest>>,
        ) -> Result<Response<Self::BidirectionalStream>, Status> {
    // converting request in stream
            let mut streamer = request.into_inner();
    // creating queue
            let (mut tx, rx) = mpsc::channel(4);
            tokio::spawn(async move {
    // listening on request stream
                while let Some(req) = streamer.message().await.unwrap(){
    // sending data as soon it is available
                    tx.send(Ok(SayResponse {
                        message: format!("hello {}", req.name),
                    }))
                    .await;
                }
            });
    // returning stream as receiver
            Ok(Response::new(rx))
        }
    }
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let addr = "[::1]:50051".parse().unwrap();
        let say = MySay::default();
        println!("Server listening on {}", addr);
        Server::builder()
            .add_service(SayServer::new(say))
            .serve(addr)
            .await?;
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Client

    use futures::stream::iter;
    use hello::say_client::SayClient;
    use hello::SayRequest;
    mod hello;
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
            .connect()
            .await?;
        let mut client = SayClient::new(channel);
    // creating a client
        let request = tonic::Request::new(iter(vec![
            SayRequest {
                name: String::from("anshul"),
            },
            SayRequest {
                name: String::from("rahul"),
            },
            SayRequest {
                name: String::from("vijay"),
            },
        ]));
    // calling rpc
        let mut response = client.bidirectional(request).await?.into_inner();
    // listening on the response stream
        while let Some(res) = response.message().await? {
            println!("NOTE = {:?}", res);
        }
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Example

Authentication

Authentication is a very important aspect of a system. gRPC comes with plug able authentication support. gRPC support mainly two types of authentication:

  • Token-based authentication
  • TLS based authentication ## Token-Based Authentication

In this tutorial, we would use JWT based authentication. JWT or JSON web token provides an open-source and stateless authentication mechanism. We would jsonwebtoken crate for creating JWT and validating it. We would just see how we can use JWT with gRPC.

Server
We would need to add an interceptor, that would validate token, if the token is not valid, we would just close the request, if the token is valid then we forward the request to our handlers.

    fn interceptor(req:Request<()>)->Result<Request<()>,Status>{
        let token=match req.metadata().get("authorization"){
            Some(token)=>token.to_str(),
            None=>return Err(Status::unauthenticated("Token not found"))
        };
        // do some validation with token here ...
        Ok(req)
    }
Enter fullscreen mode Exit fullscreen mode

If we return Ok then the request would be passed on to functions but if we return Err with status the request is closed with provided status. We create a service with this interceptor.

    let say = MySay::default();
    let ser = SayServer::with_interceptor(say,interceptor);
    Server::builder().add_service(ser).serve(addr).await?;
Enter fullscreen mode Exit fullscreen mode

Client
We need to add an interceptor to our client also. We would add it to our main function as a closure.

        let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
            .connect()
            .await?;
        let token = get_token();// an method to get token can be a rpc call etc.
        let mut client = SayClient::with_interceptor(channel, move |mut req: Request<()>| {
    // adding token to request.
            req.metadata_mut().insert(
                "authorization",
                tonic::metadata::MetadataValue::from_str(&token).unwrap(),
            );
            Ok(req)
        });
Enter fullscreen mode Exit fullscreen mode

Mutual TLS Based Authentication

TLS stands for Transport Layer Security, it is recommended by gRPC documentation to encrypt HTTP/2 connection with TLS. We would TLS to authenticate both client and server. This is called Mutual TLS. We would create a private key and public key for both client and server. We would also create a Certificate Authority certificate so that we can sign our TLS certificate. We would require OpenSSL for creating certificates.

Creating Certificates

OpenSSL is a command-line utility for creating keys and encryption-related stuff. We would start by creating a Certification Authority certificate.

openssl genrsa -des3 -out my_ca.key 2048
Enter fullscreen mode Exit fullscreen mode

This would act as our signing key. We would use it to sign our TLS certificate. Next, we create our certificate which is called the root CA certificate. It is used to validate if our TLS certificate is
validated or not.

openssl req -x509 -new -nodes -key my_ca.key -sha256 -days 1825 -out my_ca.pem
Enter fullscreen mode Exit fullscreen mode

This command would ask you a few questions. Details enter in this doesn’t matter. If you can get this certificate on every device on earth you become a certificate signing authority like Let’s Encrypt etc. Now let's create our server key and certificate.

openssL genrsa -out server.key 2048
Enter fullscreen mode Exit fullscreen mode

This command will generate a key for our server. Now we create a certificate signing request for our key.

openssl req -new -sha256 -key server.key -out server.csr
Enter fullscreen mode Exit fullscreen mode

This would ask you some questions. Now we create a server.ext file. This file would contain our name, our domain, or subdomain.

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
Enter fullscreen mode Exit fullscreen mode

We add our identity in the form of DNS. Now we run the following command

openssl x509 -req -in server.csr -CA my_ca.pem -CAkey my_ca.key -CAcreateserial -out server.pem -days 1825 -sha256 -extfile server.ext
Enter fullscreen mode Exit fullscreen mode

You don’t need to provide your private key or server key. This might ask you a few questions and passcode provided when generating the Certificate Authority key.
You can generate a Certificate for the client using the same certificate authority. Now we have all the required certificates to let configure our server and client.

Configuring Client and Server

tonic support TLS using rust-tls. We can configure TLS by following the method.

This shows how to configure the client for TLS.

        // getting certificate from disk
        let cert=include_str!("../client.pem");
        let key=include_str!("../client.key");
        // creating identify from key and certificate
        let id=tonic::transport::Identity::from_pem(cert.as_bytes(),key.as_bytes());
        // importing our certificate for CA
        let s=include_str!("../my_ca.pem");
        // converting it into a certificate
        let ca=tonic::transport::Certificate::from_pem(s.as_bytes());
        // telling our client what is the identity of our server
        let tls=tonic::transport::ClientTlsConfig::new().domain_name("localhost").identity(id).ca_certificate(ca);
        // connecting with tls
        let channel = tonic::transport::Channel::from_static("http://[::1]:50051")
            .tls_config(tls)
            .connect()
            .await?;
Enter fullscreen mode Exit fullscreen mode

This shows how to configure for TLS.

        let say = MySay::default();
    // reading cert and key disk
        let cert = include_str!("../server.pem");
        let key = include_str!("../server.key");
    // creating identity from cert and key
        let id = tonic::transport::Identity::from_pem(cert.as_bytes(), key.as_bytes());
    // reading ca root from disk
        let s = include_str!("../my_ca.pem");
    // creating certificate
        let ca = tonic::transport::Certificate::from_pem(s.as_bytes());
    // creating tls config
        let tls = tonic::transport::ServerTlsConfig::new()
            .identity(id)
            .client_ca_root(ca);
    // creating server with tls
        Server::builder()
            .tls_config(tls)
            .add_service(ser)
            .serve(addr)
            .await?;
        Ok(())

Enter fullscreen mode Exit fullscreen mode

Conclusion

We have gone through basic protocol buffer and gRPC. We have created our server. We also created a client for interacting with the server. We learned how to compile our .proto file in rust client. We also learned how to stream responses and requests. We also created a bidirectional stream. We learned two different authentication strategy. We implemented JWT and Mutual TLS based authentication. Now you have a basic understanding of gRPC, you can create your own micro-service based app. gRPC comes with support for load-balancing,tracing and health tracking. Now you can explore further functionality of gRPC.

Code Sample

https://github.com/anshulrgoyal/rust-grpc-demo

Discussion

pic
Editor guide
Collapse
wotzhs profile image
Sean Wong

Hi Anshul, I am very new to Rust development, i find this article extremely helpful, however I am not quite clear with the statement below:

This will help us testing and maintaining code in save repo but it is not suggested for a large project

is this referring to the bin block in the cargo.toml? is so, may I know what would be the proper way to run the rust grpc server?

Collapse
anshulgoyal15 profile image
Anshul Goyal Author

Yes, the better way is to use cargo wrokspaces.

Collapse
wotzhs profile image
Sean Wong

Thanks Anshul, I have been looking at different gRPC crates and I think another way that to not use the cargo run --bin server is to use the statically generated code from protoc --rust_out and use them to build the grpc server manually.

Particularly I found github.com/tikv/grpc-rs.

I understood completely that this article was meant to demonstrate the dynamically genarated grpc server & client, so I hope this comment is not to be taken in the wrong light.

Thread Thread
anshulgoyal15 profile image
Anshul Goyal Author

Hi, Sean I always like to generate stubs at build time. It allows us to maintain a sync between protocol buffer definations and stubs. I think it is best practice to not to generate stubs before hand.

Thread Thread
wotzhs profile image
Sean Wong

yup, agreed, generating stub at build time guarantees the latest protobuf code, that's my preference too.

Collapse
akhilerm profile image
Akhil Mohan

Can you point to the complete code repository ?

Collapse
mikkelhjuul profile image
MikkelHJuul

He would have to add it, because it doesn't seem to be on his github.
for some small points, the name of the impl has to be Say (given by the .proto), the file hell.rs should probably have been hello.rs and is placed in folder src. This is my findings so far.
This should bring you to a state where if you have not added anything to your implementation the compiler will spit at you: not all trait items implemented, missing: ... which makes perfect sense

Collapse
anshulgoyal15 profile image
Collapse
anshulgoyal15 profile image
Anshul Goyal Author

Hi
Sorry for the late reply.
Here is the code repo github.com/anshulrgoyal/rust-grpc-...

Collapse
akhilerm profile image
Akhil Mohan

Thank you.

Collapse
myleftfoot profile image
Stéphane Trottier

good sample, had to tweak the JWT sample code.

had to change

mut req: Request

to

mut req: tonic::Request

Collapse
geoxion profile image
Dion Dokter

Very good post! This is exactly what I needed 😁

Collapse
shionryuu profile image
Shion Ryuu

title "Token-Based Authentication" is not correct show.

"use" is missing before "jsonwebtoken crate".