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.
- Creating a project
- Writing and compiling the protobuf
- Initializing the server and client
- Implementing the API
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