Author Avatar

Anuj Verma

0

Share post:

In this post we are going to build a simple gRPC client and server application. I was recently trying to set this up in one of my projects and was struggling to do it. Hence I decided to write a post which focuses only on the exact steps to setup gRPC communication between server and client written in Golang.

I will not cover the basics of gRPC and why we should use it. If you are new to it you can read about it here. Before we start here are the pre-requisites:

Create project directory and initialise Go modules

Don’t worry if you are unaware with some of the files in below tree structure, for now just create empty files / directories using the given commands.

$ mkdir exchange-app
$ cd exchange-app
$ go mod init bank/exchange-app
$ mkdir proto
$ touch server.go client.go proto/currency_conversion.proto

$ tree .
.
├── client.go
├── go.mod
├── proto
│   └── currency_conversion.proto
└── server.go

Install gRPC and protoc-gen-go

$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go

Install protobuf

# Linux 
$ apt install -y protobuf-compiler
$ protoc --version 

# Mac OS
$ brew install protobuf
$ protoc --version 

For other operating systems, please refer this link.

Let’s setup the client and server communication

Define a service in a .proto file

The first step is to complete the currency_conversion.proto file which defines the gRPC service and the request and response messages using protocol buffers. Now open the currency_conversion.proto file in a text editor and paste the below content in it.

// currency_conversion.proto
// this file will be used to generate the grpc client and server interfaces in Go

syntax = "proto3";

package proto;

message CurrencyConversionRequest {
    string from = 1;
    string to = 2;
    float value = 3;
}

message CurrencyConversionResponse {
    float converted_value = 1;
}

service CurrencyConversionService {
    rpc Convert(CurrencyConversionRequest) returns (CurrencyConversionResponse) {}
}

Generate server and client code using the protocol buffer compiler

From the project root directory, run the below command to generate the gRPC client and server interfaces in Go which can be used as a Go package in the client and server code.

$ protoc -I proto/ proto/currency_conversion.proto --go_out=plugins=grpc:proto/

After executing the above command, if we again look at the tree structure of the current directory, we can see that a currency_conversion.pb.go file is generated.

$ tree .
.
├── client.go
├── go.mod
├── proto
│   ├── currency_conversion.pb.go // generated file
│   └── currency_conversion.proto
└── server.go

The generated file contains the following contents:

  • Golang struct type for each message defined in currency_conversion.proto file. It also has helper methods to get value of each field defined in the struct. As an example, look at the generated code for proto message CurrencyConversionRequest
type CurrencyConversionRequest struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    From  string  `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"`
    To    string  `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"`
    Value float32 `protobuf:"fixed32,3,opt,name=value,proto3" json:"value,omitempty"`
}

func (x *CurrencyConversionRequest) GetFrom() string {
    if x != nil {
        return x.From
    }
    return ""
}

func (x *CurrencyConversionRequest) GetTo() string {
    if x != nil {
        return x.To
    }
    return ""
}

func (x *CurrencyConversionRequest) GetValue() float32 {
    if x != nil {
        return x.Value
    }
    return 0
}
  • An interface type which clients will use to call the methods implemented by the grpc server
type CurrencyConversionServiceClient interface {
    Convert(ctx context.Context, in *CurrencyConversionRequest, opts ...grpc.CallOption) (*CurrencyConversionResponse, error)
}

type currencyConversionServiceClient struct {
    cc grpc.ClientConnInterface
}

func NewCurrencyConversionServiceClient(cc grpc.ClientConnInterface) CurrencyConversionServiceClient {
    return &currencyConversionServiceClient{cc}
}

func (c *currencyConversionServiceClient) Convert(ctx context.Context, in *CurrencyConversionRequest, opts ...grpc.CallOption) (*CurrencyConversionResponse, error) {
    out := new(CurrencyConversionResponse)
    err := c.cc.Invoke(ctx, "/currency_conversion.CurrencyConversionService/Convert", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}
  • An interface type which the servers will implement to serve the client requests
// CurrencyConversionServiceServer is the server API for CurrencyConversionService service.
type CurrencyConversionServiceServer interface {
    Convert(context.Context, *CurrencyConversionRequest) (*CurrencyConversionResponse, error)
}

// this is the function servers will use to register their struct object which implements the grpc service methods
func RegisterCurrencyConversionServiceServer(s *grpc.Server, srv CurrencyConversionServiceServer) {
    s.RegisterService(&_CurrencyConversionService_serviceDesc, srv)
}

Finish the server.go file

Now let’s implement the server.go

package main

import (
	"bank/exchange-app/proto"
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"
)

// myGRPCServer implements CurrencyConversionServiceServer interface
type myGRPCServer struct {
}

// actual implementation of the Convert method
func (s *myGRPCServer) Convert(ctx context.Context, request *proto.CurrencyConversionRequest) (*proto.CurrencyConversionResponse, error) {
	fmt.Printf("request received to convert currency %+v \n", request)

	// NOTE: here you can write the logic to convert the currency

	return &proto.CurrencyConversionResponse{
		ConvertedValue: 74.02,
	}, nil
}

func main() {
	fmt.Println("starting gRPC server application")

	// start a listener on the port you want to start the grpc server
	lis, err := net.Listen("tcp", ":9000")
	if err != nil {
		fmt.Println(err)
		return
	}

	// create a grpc server object
	srv := grpc.NewServer()

	// pass the address of the struct which implements the CurrencyConversionServiceServer interface
	proto.RegisterCurrencyConversionServiceServer(srv, &myGRPCServer{})

	// start the grpc server
	err = srv.Serve(lis)
	if err != nil {
		return
	}

	return
}

But what have we done here ? Let’s see what steps can be followed to write the server.go code

  1. Create a struct (give it any name you want). It should implement the CurrencyConversionServiceServer interface
// myGRPCServer implements CurrencyConversionServiceServer interface
type myGRPCServer struct {
}

// actual implementation of the Convert method
func (s * myGRPCServer) Convert(ctx context.Context, request *proto.CurrencyConversionRequest) (*proto.CurrencyConversionResponse, error) {
    fmt.Printf("request received to convert currency %+v \n", request)

    // NOTE: here you can write the logic to convert the currency

    return &proto.CurrencyConversionResponse{
        ConvertedValue: 74.02,
    }, nil
}

2. Start a listener on 9000 port (you can use port of your choice)

lis, err := net.Listen("tcp", ":9000")
if err != nil {
    fmt.Println(err)
    return
}

3. Using google.golang.org/grpc package’s NewServer method, create a new grpc server object. It creates a gRPC server object which has no service registered and has not started to accept requests yet

 srv := grpc.NewServer()

4. Register the defined struct to inform grpc about the object which implements the grpc server

proto.RegisterCurrencyConversionServiceServer(srv, & myGRPCServer{})

5. Start the gRPC server now which will listen for incoming requests at port 9000

// start the grpc server
err = srv.Serve(lis)
if err != nil {
    return
}

Finish the client.go file

Here is the implemented grpc client.go file and you can find the breakdown of each step after it.

package main

import (
	"context"
	"fmt"

	"bank/exchange-app/proto"

	"google.golang.org/grpc"
)

func main() {
	fmt.Println("starting grpc client application")

	// use grpc.Dial to create a grpc connection to running grpc server
	conn, err := grpc.Dial(":9000", grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		fmt.Println(err)
		return
	}

	defer conn.Close()

	// use the NewCurrencyConversionServiceClient function defined in generated .pb.go file
	// to generate a grpc client object
	// this object can be used to call methods implemented in grpc server
	client := proto.NewCurrencyConversionServiceClient(conn)

	// create the grpc request
	request := &proto.CurrencyConversionRequest{
		From:  "INR",
		To:    "USD",
		Value: 1,
	}

	// call the grpc server method Convert using the client
	response, err := client.Convert(context.Background(), request)
	if err != nil {
		fmt.Println(err)
		return
	}

	// print the response
	fmt.Println("converted currency value is: ", response.GetConvertedValue())
}
  1. Dial a connection to the server
// use grpc.Dial to create a grpc connection to running grpc server
conn, err := grpc.Dial(":9000", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
    fmt.Println(err)
    return
}

2. Create a grpc client object using the connection object. This object will be used to call server methods

client := proto.NewCurrencyConversionServiceClient(conn)

3. Create the grpc request and call the server methods

// create the grpc request
request := &proto.CurrencyConversionRequest{
    From:  "INR",
    To:    "USD",
    Value: 1,
}

// call the grpc server method Convert using the client
response, err := client.Convert(context.Background(), request)
if err != nil {
    fmt.Println(err)
    return
}

And that’s it.

Time to test

  1. Run the GRPC server
$ go run server.go
starting gRPC server application

2. Run the client

$ go run client.go
starting grpc client application
converted currency value is:  74.02

When you run the client code, server will also show the log message

request received to convert currency from:"INR" to:"USD" value:1

That was it! I hope you enjoyed the post and learned how we can establish gRPC communication between client and server written in Golang. If you enjoyed the post, please like and share. Any feedbacks or suggestions will encourage me to write more. Thanks for reading!

10 must know Vim commands for beginners
Java Beans and POJOs are not same

Leave a Reply