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 protomessage 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 ¤cyConversionServiceClient{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
- 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()) }
- 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
- 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!