A dive into GRPC using Go

A dive into GRPC using Go

·

7 min read

Hello everyone, I wanted to do a quick article about GRPC and what exactly is GRPC with a quick demo on a couple of implementations using Go. Let's begin

What is GRPC?

Generally RPC stands for remote procedure calls, RPCs are calls that can be made from one server to another to invoke a certain action, it's a request response protocol where you remotely invoke a certain action and get it's response back. but isn't this similar to the REST architecture? Yes it is however there are some key differences which we will discuss next.

What is the difference between GRPC and REST?

  • By default GRPC is built on top of HTTP/2 which allows multiplexing several streams into one TCP connection while normal rest relies on HTTP1.1 however it also can use HTTP/2

  • In REST you are obligated to use one of the HTTP Methods (GET, POST, PUT .. etc) but sometimes I want to do something that neither of these methods make sense in the context of it. For example if I want to do a RPC that restarts a certain service or even shuts it down, neither of these methods really make sense do they?

  • REST doesn't have a formal API contract between both parties, Protocol buffers are pre defined with a structure that you know exactly what goes through the wire.

  • Bi-directional streaming (when instead of sending just a response they send back a stream of data) is not possible using REST, this is possible using GRPC, we'll take a quick look at it in the code example.

  • GRPC uses something called Protocol Buffers(binary based protocol) as a way of sending data back and forth I won't be going into detail of what it specifically is, but it's size is so much smaller compared to a normal HTTP text protocol so network wise it transfers data of less size back and forth.

  • Not only lighter data but Protocol Buffers are also language neutral, you don't have to adjust between languages when using it. All the work goes into writing the .proto file and simply running the Protobuf generator to generate the code necessary for the language chosen. (Hang in there we'll get to this part in the code)

Now that we have a basic understanding of what it is let's take a quick demo to help make things look more clear. In this example I'll be having a Protocol Buffer file to define the machine readable contract that will contain the following

currency.proto
syntax = "proto3";

option go_package = "grpc/protos";

message RateRequest{
    string From =1;
    string To = 2;
}
message RateResponse{
    float Rate=1;
}

service Currency{
    rpc GetRate(RateRequest) returns (RateResponse);
    rpc GetRatePoll(RateRequest) returns (stream RateResponse);
}

As we can see above this is like the our schema where we define firstly the syntax and we want that to be proto3, any entity we would like to define in a proto file is called "message" so we have 2 message entities which are RateRequest which has a From and To basically 2 currencies and another message a RateResponse which returns the rate of conversion between these currencies.

That was for the entities but what about the services we want this currency proto file to have? there are 2 services in the service block defined, one is GetRate which takes in a RateRequest and returns a RateResponse and the other one is kind of interesting, GetRatePoll takes a RateRequest but returns a stream of data not an instantaneous response, once the data has all been transferred the cycle is complete. We define services buy the keyword rpc before it and take care that returns has an s and not just return like any programming language.

Now this file is language neutral, it's the same syntax and will never change across different languages, but what we need to do is actually compile the above file to match our language of use, since I'm using go and have installed protoc which is a requirement to be able to compile the file above as well as installing the go plugin for protobuf

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

. Now all we need to do is to run the following command assuming the .proto is located in the directory protos.

protoc --go_out=. --go_opt=paths=source_relative \               
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    protos/currency.proto

I specified the go_out option to be go because that's what I'm using.

This generates 2 files

currency_grpc.pb.go
currency.pb.go

These files are a language specific implementation to the proto file specified. They include all code necessary and you just use the code generated.

Now that we have successfully generated the code, we haven't yet implemented the actual services GetRate and GetRatePoll and to be able to do this if we take a look at our currency_grpc.pb.go file we will find an interface that has the 2 method signatures

type CurrencyServer interface {
    GetRate(context.Context, *RateRequest) (*RateResponse, error)
    GetRatePoll(*RateRequest, Currency_GetRatePollServer) error
    mustEmbedUnimplementedCurrencyServer()
}

Now let's implement GetRate firstly. What we'll do is create a class that has GetRate as a method and inject that class into the GRPC server which we haven't yet defined, since go doesn't have classes directly we will create a struct with a logger inside it and add methods to this struct.

type Currency struct {
    logger *Logger
}

And the GetRate method

func (c *Currency) GetRate(ctx context.Context, in *protos.RateRequest) (*protos.RateResponse, error) {
    c.logger.InfoLogger("Received a GRPC Request Requesting Currency Rate")
    return &protos.RateResponse{
        Rate: 0.5,
    }, nil
}

What we've done is just log the request and return a hard coded response with a rate of 0.5.

Also forgot to mention that I created a Logger struct that has different warnings based on how severe the log is

type Logger struct{}

func (*Logger) InfoLogger(message string) {
    log.Println("INFO: ", message)
}
func (*Logger) WarnLogger(message string) {
    log.Println("WARNING: ", message)
}
func (*Logger) ErrorLog(message string) {
    log.Panic("ERROR: ", message)
}

Now let's take a look at the other method GetRatePoll

func (c *Currency) GetRatePoll(in *protos.RateRequest, stream protos.Currency_GetRatePollServer) error {
    rs := protos.RateResponse{
        Rate: 0.84,
    }
    for {
        stream.Send(&rs)
        time.Sleep(5 * time.Second)
    }

}

All this does is create a response hard coded rate to 0.84 and keep sending the same response over and over with a 5 sec timeout. this is not bi directional streaming it's only one way as the server streams back a response to a single request. Obviously this will never end but its just for an example, otherwise maybe you'd want to read and send chunks of data until you finish the whole thing.

Initializing the server

Now all that's left is defining the GRPC server and running it so we can connect on it.


import (
    "context"
    protos "grpc/protos"
    "time"
)

func main() {
    server, err := net.Listen("tcp", ":9000")
    if err != nil {
        panic("Unable to listen on port 9000.")
    }
    logger := currency.Logger{}
    cs := currency.NewCurrency(&logger)
    grpc := grpc.NewServer()
    protos.RegisterCurrencyServer(grpc, cs)
    grpc.Serve(server)

}

In the main this is all the code required to initialize a GRPC server. We open a normal tcp connection on port 9000, initialize a logger which we send to the NewCurrency function which is a function that simply returns an instance of the currency server here's the implementation of it.


func NewCurrency(logger *Logger) *Currency {
    return &Currency{logger: logger}
}

It just takes a logger pointer and returns a new currency struct. A Factory pattern if you think of it.

We run grpc.NewServer() which initiates a grpc server.

Then we invoke the protos RegisterCurrencyServer which is found in the package grpc/protos Passing in the server and the currency struct.

Finally we run grpc.Serve and pass in the tcp server. We can register multiple servers of different context other than currency using RegisterCurrencyServer

Testing the server

Now client side GRPC is possible by implementing client methods from the generated GRPC code, however there's this useful tool called grpcCurl that is similar to the normal curl except it allows us to connect to GRPC endpoints. Here's the link for it.

From your terminal run

grpcurl -plaintext localhost:9000 Currency.GetRate

This will invoke the Currency.GetRate service that we defined and this is how it looks like

~|⇒ grpcurl -plaintext localhost:9000 Currency.GetRate
{
  "Rate": 0.5
}

Now invoking the Currency.GetRatePoll yields us an infinite stream of responses separated by 5 seconds each.

~|⇒ grpcurl -plaintext localhost:9000 Currency.GetRatePoll
{
  "Rate": 0.84
}
{
  "Rate": 0.84
}
{
  "Rate": 0.84
}
{
  "Rate": 0.84
}

And that's all for this topic. I enjoyed studying this and tried to explain it as much as I could, however this is just scratching the surface of GRPC and the examples shown were not a full follow on tutorial it was just a demo showcasing how everything works together.

Hope anyone found this useful and see you next article!

Did you find this article valuable?

Support Amr Elhewy by becoming a sponsor. Any amount is appreciated!