The Math and Code Behind…
Facial alignment is a prereq…
3 years, 4 months ago
by Sabbir Ahmed
gRPC is google's high-performance RPC framework. Working with Remote Procedural Calls (RPC) has always been a headache before gRPC. Google has made it easier for developers and engineers with polyglot code generation support. Today we are going to have a look at the modern defacto standard for microservices and service-to-service communication protocol- gRPC.
This article is not going to cover the installation of the protobuf and go packages. They are pretty straightforward and you can find them easily at - Go Quick Start.
In this article, we will create a Basic Geometry service that will return us the geometric property of a shape ie, circle, rectangle, etc... So, Let's begin. I am a big fan of project structures. It helps me to understand what I am going to do and where I have to look for when it comes to resolving any bug/issue. So, I am going to start with our project's folder structure -
. ├── cmd │ ├── client │ └── server ├── internal ├── proto └── go.mod
Get the source code and follow along...
Almost every go project will have a cmd
folder. It may container cli
, server
, client
according to the project requirement. Internal will contain the factories, repositories, and services. Now we are ready to define the contract between server and client aka the proto. Create a new file at proto directory named geometry.proto and paste the following -
syntax = "proto3";
package geometry;
option go_package = "github.com/by-sabbir/grpc-service-example/proto";
These three lines of code are the setups. The first line denotes the version we are using to define the proto. The second line is the package name, we can be creative here. The 3rd line is the go-specific option, this is an amalgamation of the module name from the go.mod and the relative path of the proto directory.
Now, we are ready to declare the service and Request/Response contract - gRPC ensures the communication by setting up a pre-defined message structure so the server and client don't confuse themselves with the message structure. This may sound unnecessary for beginners but in my software development experience, this was one of the most frustrating points. And gRPC addressed this issue right at the beginning. An RPC service has three parts,
in Proto point 2 and 3 is part of the message struct and service is the service struct, dead simple. Let's code - extend the geometry.proto file and add the following lines -
message RectRequest {
float height = 1;
float width = 2;
}
message AreaResponse {
float result = 1;
}
message PermiterResponse {
float result = 1;
}
service GeometryService {
rpc Area (RectRequest) returns (AreaResponse);
rpc Perimeter (RectRequest) returns (PermiterResponse);
}
The proto itself is self-descriptive, we have a GeometryService with two functions - Area and Perimeter. Both RPC functions take a request object and spit out a response. Generally, the request and response messages are named with the same prefix, for example, if we had a service called Greet, we would have had a GreetRequest and a GreetResponse (Greet as prefix). But our service is a perfect example of making an exception.
In the beginning, we were bragging about the gRPC being polyglot, in this section, we will see it in action. We will generate some go code with protoc. Note that we must need the go plugins for it, check if you have already installed them at Go Quick Start.
The basic command is as follows -
❯ protoc --go_out=. --go-grpc_out=. proto/*.proto
This should generate the go interface codes as follows.
├── github.com └── by-sabbir └── grpc-service-example └── proto ├── geometry_grpc.pb.go └── geometry.pb.go
Oh! now we see the patterns right? This is the folder structure we assigned go_package in the third line of the geometry.proto file... the resemblance is uncanny (pun intended). But we don't need them here, we need them in the proto directory. If you are thinking of changing the go_package, you are not wrong. But there is a better approach. The protoc CLI tool allows us to assign modules and import paths for the generated code. let's update the command
❯ protoc -Iproto/ --go_out=. --go_opt=module=github.com/by-sabbir/grpc-service-example --go-grpc_out=. --go-grpc_opt=module=github.com/by-sabbir/grpc-service-example proto/*.proto
If I am not wrong you must be thinking who can remember the huge command! let me demystify that for you. But in practice, you will always use Makefile or equivalent to generate the codes for you. So the concise command should look like this
❯ protoc -I<proto path> --go_out=. --go_opt=module=<go module> --go-grpc_out=. --go-grpc_opt=module=<go module> proto/*.proto
For every _out, we will have _opt=module= and the module name from the go.mod file. We can delete the github.com folder as we have the generated code in the proto directory.
We have come a long way, let's create a new file in the cmd/server directory and name it main.go unsurprising stuff. And run the following command to get the dependencies-
❯ go mod tidy
Let's first copy the following code in the cmd/server/main.go from GitHub And the cmd/client/main.go from GitHub.
At this point, we are ready to test our applications,
❯ go run cmd/server/main.go
The output should be like below -
2022/09/26 16:18:17 tcp listener started at port: 5000
2022/09/26 16:18:21 invoked Area: height:10.1 width:20.5
2022/09/26 16:18:21 invoked Perimeter: height:10.1 width:20.5
If the code exists with status code 1, you should change the port at line 16 of the server/main.go, else run the client:
❯ go run cmd/client/main.go
If the output shows like below we are good to go to the next step
Area: 207.05
Perimeter: 61.2
Test with some values in the client code.
In go, it's common practice to start with a single file and then refactor to best practices. Currently, we have a monolithic server with no separation of concern. We started the project with growth factors in mind, we had an untouched folder named internal. Let's create a folder geometry and a file geometry.go, so the path looks like internal/geometry/geometry.go. This will be our Factory for the Geometry Service. Let's have a look at the geometry.go file
package geometry
import (
"context"
"log"
pb "github.com/by-sabbir/grpc-service-example/proto"
)
type Store interface {
Area(context.Context, *pb.RectRequest) (*pb.AreaResponse, error)
Perimeter(context.Context, *pb.RectRequest) (*pb.PermiterResponse, error)
}
type Server struct {
pb.GeometryServiceServer
}
func NewServer() *Server {
return &Server{}
}
func (s *Server) Area(ctx context.Context, in *pb.RectRequest) (*pb.AreaResponse, error) {
log.Println("invoked Area: ", in)
return &pb.AreaResponse{
Result: in.Height * in.Width,
}, nil
}
func (s *Server) Perimeter(ctx context.Context, in *pb.RectRequest) (*pb.PermiterResponse, error) {
log.Println("invoked Perimeter: ", in)
return &pb.PermiterResponse{
Result: 2 * (in.Height + in.Width),
}, nil
}
The Store interface will act as a Repository. if we need to expand the project and integrate it with a database we can create a new folder db in the internal directory and just implement the Store interface. And now our cmd/server/main.go file should look like the following,
package main import ( "fmt" "log" "net" "os" geomServer "github.com/by-sabbir/grpc-service-example/internal/geometry" pb "github.com/by-sabbir/grpc-service-example/proto" "google.golang.org/grpc" ) var ( host = "localhost" port = "5000" ) func main() { addr := fmt.Sprintf("%s:%s", host, port) lis, err := net.Listen("tcp", addr) if err != nil { log.Println("error starting tcp listener: ", err) os.Exit(1) } log.Println("tcp listener started at port: ", port) grpcServer := grpc.NewServer() geomServiceServer := geomServer.NewServer() // registering gemoetry service server into grpc server pb.RegisterGeometryServiceServer(grpcServer, geomServiceServer) if err := grpcServer.Serve(lis); err != nil { log.Println("error serving grpc: ", err) os.Exit(1) } }
As we can see now, the server is not burdened with the Area and Perimeter functions anymore. It's just a simple gRPC server implementation, concerns are separated.
Note: If you have any questions regarding this article please follow up on LinkedIn or Telegram. I will try my best to answer your queries.