Unary calls with gRPC

blog-image

A simpler client/server relationship for faster service.

In our last post, we went over how to define messages using protocol buffers as an introduction to getting started with gRPC.

In today’s post, we’ll define a service and make our first gRPC call.

In gRPC, there are 4 types of calls that can be made.

  • Unary
  • Server Streaming
  • Client Streaming
  • Bi-directional

I’ve decided to turn this into a series and only cover Unary today for two reasons :

  • Unary calls are going to be the simplest to start with because they strongly resemble the request/response architecture of RESTful services.
  • I wan’t to reduce the average reading time of my blog posts.

It’s worth mentioning that I’ll be using the following format for this gRPC series.

Enjoy!

Creating our project

Let’s use the following directory structure.

.
└── calculator
    ├── calc_client
    ├── calc_server
    └── calcpb

Writing and compiling the protobuf

We’re going to create a basic .proto to start without defining any message schemas just yet.

We’re going to update this later. For now let’s just set an empty service.

/calculator/calcpb/calc.proto

//declare our version
syntax = "proto3";

//name our package
package calc;

//I'm going to be using Go but you
//can do this for whatever language you want.
option go_package="calcpb";

//define an empty service
service Calculate{

}

Let’s compile this :

protoc calculator/calcpb/calc.proto --go_out=plugins=grpc:.

The first arg is the path to our .proto file from project root. The out flag describes what language we wan’t to generate code and we are using the gRPC plugin as we want our generated code to be used with gRPC.

Now we have :

.
└── calculator
    ├── calc_client
    ├── calc_server
    └── calcpb
        ├── calc.pb.go
        └── calc.proto

We now have a new file located at calculator/calcpb/calc.pb.go that has tons of useful boiler-plate code for us to use. Let’s import the calcpb library into our server and client files so we can use some of that code.

Initialize server and client

Let’s set up a basic server and client.

/calculator/calc_server/server.go

package main


import (
    "context"
    "fmt"
    "net"
    "log"

    "github.com/fuskovic/grpcDemo/calculator/calcpb"

    "google.golang.org/grpc"
)

type server struct{}

func main(){
    //50051 is the default gRPC port
    lis, err := net.Listen("tcp", "0.0.0.0:50051")
    if err != nil{
        log.Fatalf("failed to listen on 50051 : %v\n", err)
    }

    s := grpc.NewServer()
    calcpb.RegisterCalculateServer(s, &server{})

    fmt.Println("server starting on port : 50051")
    if err := s.Serve(lis); err != nil{
        log.Fatalf("failed to start server : %v\n", err)
    }
}`

Notice we are able to register the Calculate service on our server using the auto-generated RegisterCalculateServer function from our calcpb import.

/calculator/calc_client_/client.go

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/fuskovic/grpcDemo/calculator/calcpb"

    "google.golang.org/grpc"
)

func main(){
    //grpc.WithInsecure() means our connection is not encrypted.
    //we'll cover securing our gRPC connections with SSL/TLS in a separate post.
    conn,err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil{
        log.Fatalf("error : %v\n", err)
    }
    defer conn.Close()

    c := calcpb.NewCalculateClient(conn)
    fmt.Println("Client successfully initialized")
}

Again, notice how we are able to instantiate a new client instance for our Calculate service with the auto-generated NewCalculateClient func from the calcpb import.

Time to start making calls.

Implementing a Unary API

/calculator/calcpb/calc.proto

syntax = "proto3";

package calculator;
option go_package="calcpb";

message SumRequest{
    int32 num_one = 1;
    int32 num_two = 2;
}

message SumResponse{
    int32 sum = 1;
}

service Calculate{
    rpc Sum(SumRequest) returns (SumResponse) {};
}

If you’ve been keeping up with my posts, the only part that should be new to you is the service at the bottom. Let’s dissect this.

We are defining a new service called Calculate with an endpoint called Sum. Sum takes input in the form of an object/data-structure that has been serialized according to our SumRequest message schema, e.g. two numbers. It will return a response that has been serialized according to the SumResponse message schema, e.g. one number(the sum of the two numbers from the request).

The empty curly braces are for adding options to our endpoint but we don’t need to worry about that now. If you compile your .proto file again then the contents of calculator/calcpb/calc.pb.go will be overwritten to reflect the update.

If you look in the updated calc.pb.go file, you will see that structs have been created to represent the data structures we defined in our .proto messages.

Along with the structs for each message, Get methods for all of the struct fields have also been auto-generated for us. Also the handler for the Sum endpoint has also been generated for us. How cool is that?

Now we need to update our server and client accordingly starting with the server.

/calculator/calc_server/server.go

This new server method takes the request and returns the sum of it’s fields in our response object.

type server struct{}

//new code
func (s *server) Sum(ctx context.Context, req *calcpb.SumRequest) (*calcpb.SumResponse, error){
        result := req.GetNumOne() + req.GetNumTwo()
        return &calcpb.SumResponse{
            Sum : result,
        }, nil
    }

In 7 lines of code we’ve determined what we do with the field values from our request and send a response. gRPC takes care of the rest.

Next, we can create a function on the client-side to call our Sum endpoint.

/calculator/calc_client_/client.go

    c := calcpb.NewCalculateClient(conn)
    fmt.Println("Client successfully initialized")

    //new code
    unaryCall(c)
    }

    //new code
    func unaryCall(c calcpb.CalculateClient){
        req := &calcpb.SumRequest{
            NumOne : 6,
            NumTwo: 7,
        }
        res, err := c.Sum(context.Background(), req)
        if err != nil{
            log.Fatalf("error calling sum endpoint : %v\n", err)
        }
        log.Printf("Sum response from server: %v\n", res.Sum)
    }

Now we can start our server :

go run calculator/calc_server/server.go 
server starting on port : 50051

and then our client in a new shell :

go run calculator/calc_client/client.go 
Client successfully initialized
2019/08/16 12:15:52 Sum response from server: 13

That’s it! We’ve successfully made our first gRPC call using a Unary style API.

Much love,

-Faris