Beyond Rest: gRPC in Microservices Part I

Beyond Rest: gRPC in Microservices Part I

Rest remains untouched when it comes to APIs since its creation in 2000. It's the most used API architecture and millions of applications relies on it. It's widely believed that the Rest's statelessness was a primary factor in the rise of Microservices architecture and the loosely coupled applications.

Having said that, the world is moving steadily beyond Rest. In the former years other architectures have appeared and proved themselves. In this set of articles we will explore some of these architectures, specially gRPC and GraphQL and discover some practices where we can combine them with Rest.

We will build some services using multiple programming languages to solve business cases (In this blog all cases will include cats and dogs) but rest assured that these cases can very much be an abstraction for real-world problems.

Our focus today in this article is gRPC. So, what's gRPC?

gRPC (Google Remote Procedure Call): gRPC is an open-source RPC architecture designed by Google to achieve high-speed communication and light-weight between Microservices.

It makes it simple to integrate services written in multiple programming languages without having to write client libraries for each language yourself, but use a liaison between all services to do it for you. This liaison is called Protobuf messaging format. This makes gRPC steadily becoming the architecture of choice for Microservices communications and has been adopted by thousands of large projects.

It is built upon HTTP/2.0 which makes bi-directional communication possible, gRPC servers allow cross-language unary calls or stream calls.

We will explore this architecture by solving a business case.

Untitled (1)-2.png

I want to play with cats and dogs. But I can only reach them through a web-server called Unicorn. Unicorn need to communicate with me, and with cats and with dogs!

A cat or a dog can't play with me unless their owner is notified and sends them a message to play.

As we all know, Unicorns can communicate with me with Rest, because I talk Rest easily like most front-ends do. But Cats talk Go, dogs talk Python and owners only know Rust.

Untitled Diagram-4.png

All code can be found here .

Since all communications will always have two main parts: A client and a server. If a service wishes to communicate with other service, then it will need a client for this service it wishes to talk to. Having said that, we can determine the following:

  • animal-server will have a client for cat-service and a client for dog-service.
  • owner-service will have no clients, as it wishes to speak to no one, it only responds.
  • cat-service will have a client for owner-service.
  • dog-service will have a client for owner-service.

In this part we will focus in communicating the cat-service and owner-service and in the next part we will continue in building the dog-service and animal-server and watch some other use-cases where streaming would be most efficient.

One of gRPC greatest features is the ability to generate client libraries for most widely used programming languages using the Protobufs. This will mean we won't write client libraries for each service we have, and define models and so, but we will define our data only once in the protobufs and generate to each language it's libraries accordingly.

First since we have 3 microservices, 1 API Gateway server and Proto files, we can start by defining our folder structure.

Screen Shot 2021-02-23 at 5.47.04 PM.png

since data is now agnostic of languages, we will create proto directory where we will place our proto messages and services. Inside the directory we will create three files: cat.proto, dog.proto and owner.proto

First, make sure you have protoc installed following the steps here developers.google.com/protocol-buffers/docs..

Proto Files

add those lines to cat.proto.

syntax = "proto3";

package proto;

message CatRequest {
    string catName = 1;
}

message CatResponse {
    string catMeaw = 1;
}

service cat {
      rpc CallCat (CatRequest) returns (CatResponse);
    }

Messages here are data models and rpc are the calls. Here we basically have a CallCat service, that accepts CatRequest which has the name of the cat, and returns a message from this cat (well, which is meaw).

We do the same with dog.proto

syntax = "proto3";

package proto;

message DogRequest {
    string dogName = 1;
}

message DogResponse {
    string dogBark = 1;
}

service dog {
      rpc CallDog (DogRequest) returns (DogResponse);
    }

and owner.proto

syntax = "proto3";

package proto;

message NotifyRequest {
    string animalName = 1;
    string animalType = 2;
}

message NotifyResponse {
    string message = 1;
}

service Owner {
      rpc Notify (NotifyRequest) returns (NotifyResponse);
    }

Rust Owner-Microservice

We will start by writing the owner-service in Rust.

Visit rustup.rs to setup Rust if you hadn't yet. Then go to the work directory and write

cargo new rust-owner-microservice

This will create a boiler-plate for any rust project. go to rust-owner-microservice and create new file and call it build.rs. You will find a file called Cargo.toml which is the manifest. We will use Tonic on top of rust to deal with gRPC, so we need to add this dependency in the Cargo.toml

[package]
name = "rust-owner-microservice"
version = "0.1.0"
authors = ["mahmoudsalem <mahmoud.salem@halan.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[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"

# server binary
[[bin]]
    name = "server"
    path = "src/server.rs"

in src directory create server.rs and proto.rs

The folder structure should eventually look like this

Screen Shot 2021-02-23 at 5.46.33 PM.png

Now we have all the files that we need. Start by opening build.rs. We need to tell it to compile the owner.proto file so we can use it to create the server. So add this lines to the file.

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

In proto.rs add only one line to make it understand that we work inside the proto package. We defined proto package on top of each proto file we created up there.

tonic::include_proto!("proto");

And now we're ready to write our server. The owner-service is a very simple service, it only has one rpc Notify, it accepts the animal name and type, and returns a confirmation that the owner has been notified.

// we use tonic to establish the server communication
use tonic::{transport::Server, Request, Response, Status};

// proto package that encapsulates all the generated code from owner.proto
// we can directly use them. 

use proto::owner_server::{Owner, OwnerServer};
use proto::{NotifyResponse, NotifyRequest};
mod proto; 

#[derive(Default)]
pub struct MyOwner {}
#[tonic::async_trait]
impl Owner for MyOwner {
    // notify is the single rpc we declared in owner.proto
    async fn notify(&self,request:Request<NotifyRequest>)->Result<Response<NotifyResponse>,Status>{
        println!("Owner of {0} {1} is notified", request.get_ref().animal_name, request.get_ref().animal_type);
        Ok(
            Response::new(NotifyResponse{
             message:format!("Owner of {0} {1} is notified", request.get_ref().animal_name, request.get_ref().animal_type),
        })
    )
    }
}
#[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 owner = MyOwner::default();
    println!("Server listening on {}", addr);
// adding our service to our server.
    Server::builder()
        .add_service(OwnerServer::new(owner))
        .serve(addr)
        .await?;
    Ok(())
}

All that's left is to run

cargo run --bin server

and our server will be up and running. A great tool we can use while working with gRPC is BloomRPC. All you do is import the .proto file, and the tool has a UI that asks you for the request and the port. In our case, we used 50051 for this service.

Screen Shot 2021-02-23 at 1.09.00 AM.png

Golang Cat-Microservice

Now it's time to create our cat-microservice. Return to your working directory (outside the rust-owner-microservice) and create a new directory golang-cat-microservice. Make sure you have Golang installed by following the steps here golang.org/doc/install

First we need to install golang grpc client

cd golang-cat-microservice
go mod init golang-cat-microservice
go get -u github.com/golang/protobuf/protoc-gen-go
go get -u google.golang.org/grpc

Now we should create a server package, a client package, a proto package and main.go file

The folder structure will look like this

Screen Shot 2021-02-23 at 5.44.59 PM.png

Let's start by generating the proto files.

Unlike the owner-service where we only interacted with the owner.proto file, now in the cat-service we will interact with two protos. The cat.proto to build our server, and the owner.proto to create the client that will talk with the owner-service.

protoc -I=../proto --go_out=plugins=grpc:./proto ../proto/owner.proto 
protoc -I=../proto --go_out=plugins=grpc:./proto ../proto/cat.proto

Then it's time to create the client that should talk with owner-service

In the client directory create client.go file and add the following code

package client

import (
    "context"
    "golang-cat-microservice/proto"
    "log"
    "os"

    "google.golang.org/grpc"
)

type Client struct {
    connection *grpc.ClientConn
    Client     proto.OwnerClient
}

func NewOwnerClient(ctx context.Context) *Client {
    opts := grpc.WithInsecure()
    cc, err := grpc.Dial("localhost:50051", opts)
    if err != nil {
        log.Fatal(err)
    }

    client := proto.NewOwnerClient(cc)

    return &Client{
        connection: cc,
        Client:     client,
    }
}

func (cc *Client) Close() {
    err := cc.connection.Close()
    if err != nil {
        os.Exit(0)
    }
}

We create a Client struct that has the client connection and client itself. We also have a Close() method so we be able to close the connection when needed.

And eventually we have a NewOwnerClient function which the server will call once it served, so we don't generate a new client each time we call an API.

In the server directory add server.go file and add this to the file

package server

import (
    "context"
    "fmt"
    c "golang-cat-microservice/client"
    "golang-cat-microservice/proto"
)

type Server struct {
    oc proto.OwnerClient
}

func NewCatService(ctx context.Context) *Server {
    oc := c.NewOwnerClient(ctx)
    return &Server{
        oc: oc.Client,
    }
}

func (s *Server) CallCat(ctx context.Context, request *proto.CatRequest) (*proto.CatResponse, error) {
    catName := request.CatName
    fmt.Println(catName + " will " + "MEEEWWWWW")
    response := &proto.CatResponse{
        CatMeaw: catName + ": " + "MEEEWWWWW",
    }

    req := &proto.NotifyRequest{AnimalName: catName, AnimalType: "cat"}

    resp, err := s.oc.Notify(ctx, req)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(resp.Message)
    return response, nil
}

Here we have a server struct that has the OwnerClient and a function NewBranchService which we will use to declare our OwnerClient.

We have CallCat function which is an implementation for the CallCat rpc, it accepts the cat name and returns ... meaw. Then it uses the OwnerClient to notify the client and receives the Owner's message, then we print this message.

Time to create our main.go file and place this code inside:

package main

import (
    "context"
    "fmt"
    "golang-cat-microservice/proto"
    "golang-cat-microservice/server"
    "log"
    "net"

    "google.golang.org/grpc"
)

func main() {
    address := "localhost:50056"
    lis, err := net.Listen("tcp", address)
    if err != nil {
        log.Fatalf("Error %v", err)
    }

    service := server.NewCatService(context.Background())
    fmt.Println("Cat Server is running")
    s := grpc.NewServer()
    proto.RegisterCatServer(s, service)

    s.Serve(lis)
}

In the main we declare the cat-service address which is localhost:50056 and create New service then start serving. Now all that's left is

go run main.go

And start testing using the BloomRPC tool.

Screen Shot 2021-02-23 at 1.30.55 AM.png

import cat.proto in BloomRPC and choose the CallCat rpc. Type your favorite cat's name and call. You will find a response with it's meaw along with a log from the owner-service that the owner is notified that his/her cat is meawing at you.

Summary

In this article we tried to discuss gRPC and what's the benefits of using it, and start implementing a microservices system solving a specific task with multiple programming languages like Go, Rust and Python. The steps you do in each one of them is almost the same:

  • You build your server using grpc language-specific client and your generated protobuf code.
  • You create clients when needed to talk with other microservices withouthaving to write language-specific client library.

Next part we will continue building our system by building the dog-microservices and our Rest client, the animal-server. And will go have use-cases on streaming and how to benefit from it.

Feel free to ask anything in the comments!

And again, all code written here can be found here .

 
Share this