dbgjerez / workshop-golang-grpc

An example gRPC which uses a Golang application with some examples

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Go Reference

Nowadays, modern architecture can combine different design patterns and languages.

Traditionally, microservices are often implemented in an HTTP API interface, which can be some performance issues in distributed overloaded systems.

gRPC (Remote Procedure Calls) is a modern high-performance framework, an Open Source project, which is used to connect a large number of microservices.

Installation

protoc

Download the binary to a temporal directory

curl -L https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip -o /tmp/protoc.zip

NOTE: We'll use the "v21.12" version of protoc, which is the latest today. You can change it for another version that you can find in the following link, even though it's not necessary: https://github.com/protocolbuffers/protobuf/releases

Unzip it

unzip /tmp/protoc.zip -d /tmp/protoc/

Install the binary

mv /tmp/protoc/bin/protoc /usr/local/bin/protoc

Verify the installation

protoc --version
libprotoc 3.21.12

protoc-gen-go and protoc-gen-go-grpc

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

NOTE: if you can execute protoc-gen-go you have to add it to your PATH: export GO_PATH=~/go and export PATH=$PATH:/$GO_PATH/bin

Generate protobufs

A significant advantage of using protobufs is the capacity to auto-generate the code in your preferred language, even in different languages.

In this example, we'll create a .proto file with the definition of the entity Comment.

Once we've created the file, we'll generate the source code. In this case, I have chosen the golang plugin.

protoc comment/grpc/*.proto \        
    --go_out=. \  
    --go_opt=paths=source_relative \
    --go-grpc_out=. \
    --go-grpc_opt=paths=source_relative

NOTE: I've installed the protoc-gen-go plugin, which allow to auto-generate golang code. If you prefer, you can install another language plugin.

After the plugin execution, we can inspect our code inside the comment/grpc folder.

Create a project

As a good practice, we shouldn't modify the autogenerated code, but we can import and override it.

In this example, the folder gprc contains the generated code. We'll create a new module. This module includes the auto-generated code and two applications: the grpc server and a grpc client used to test our application.

go mod init github.com/dbgjerez/workshop-golang-grpc/comment

It's essential to change dbgjerez for your own GitHub account.

We need to download the dependencies that our project need:

go mod tidy
go get google.golang.org/grpc
go get google.golang.org/grpc/reflection

Once we have our project, we'll create a folder called server and a main.go file inside it, where we'll implement our server application.

The DDD structure used to organize this project has a folder for handlers. A handler is a point to communicate with our application, for example, a REST endpoint or gRPC like this case. In addition, the application domain has its own folder for its definitions.

The main file contains all the necessary to initialize the application. The application initializes the server, registers the gRPC handlers and starts the server up.

The complete main function looks like the following block:

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  s := grpc.NewServer()
  c.RegisterCommentServiceServer(s, handler.NewCommentHandler())
  c.RegisterHealthServer(s, new(handler.HealthHandler))
  c.RegisterInfoServiceServer(s, handler.NewInfoHandler())
  reflection.Register(s)
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}

If we visualize the main.go file, we can watch the following points:

  • The port when the application is listening.
  • How the application starts the gRPC server.
  • The registration of the comment service.
  • The registration of the health check service.
  • The registration of the info service.

The line with reflection.Register(s) enables the exposition of the API, so you can call to know the different functions and endpoints that expose the application.

Play with the application

Now, we can start our application and test it:

go run main.go

We can use the grpcurl tool to test our server application. For example, I'm going to list the different endpoints with the following command:

grpcurl -plaintext localhost:50051 list
CommentService
HealthService
InfoService
grpc.reflection.v1alpha.ServerReflection

NOTE: change port 50051 for your application port.

Our application responds to different endpoints:

  • CommentService: application business logic
  • HealthService: the health check service endpoint.
  • InfoService: application information, such as the name, version and build time.
  • grpc.reflection.v1alpha.ServerReflection: the Reflection API exposes all the endpoint definitions

If we continue calling it, we can see the different methods that contain our endpoint:

grpcurl -plaintext localhost:50051 list CommentService  
CommentService.Retrieve

We've received two methods, also the same ones that we defined in the comment.proto file. So, it looks good. Now, I'll call the insert method:

grpcurl -plaintext localhost:50051 CommentService.Retrieve 
ERROR:
  Code: Unimplemented
  Message: method Insert not implemented

Our application is responding, so it runs ok, but we've received an error code. This error is because we have not implemented the different methods as we have used the auto-generated code, and we only have defined an empty struct for our server.

Implement the server code

Now, we can implement the business logic application without modifying the auto-generated code. I'll return a list of comments, but you can use whatever you want in your application.

As we defined the server struct, we only have to define the Retrieve method:

type CommentHandler struct {
  c.UnimplementedCommentServiceServer
  comments []*c.Comment
}

func (s *CommentHandler) Retrieve(ctx context.Context, rq *c.RetrieveRequest) (*c.Comments, error) {
  log.Printf("Request: %s", rq.String())
  return &c.Comments{Comments: s.FilterComments(rq.IdObject, rq.TypeObject)}, nil
}

NOTE: The CommentHandler struct has to contain the UninmplementedCommentServiceServer and whatever you want, as a base de date or something similar.

Finally, we'll implement the rest of the handlers for info and health endpoints.

Now, we'll run the application again and test it by calling the Retrieve endpoint:

grpcurl -plaintext localhost:50051 CommentService.Retrieve
{
  "comments": [
    {
      "idComment": "1",
      "idObject": 12,
      "typeObject": "film",
      "idUser": 20
    },
    {
      "idComment": "2",
      "idObject": 12,
      "typeObject": "film",
      "idUser": 20
    }
  ]
}

And now, we can see how the application responds to the comment that we return.

Create a client

A client is an application that usually consumes some services or applications easily.

When we executed the plugin, it generated all the necessary code to make the server and the client. In this way, we only have to focus on the business logic of our application.

  idObj   = flag.Int("idObj", -1, "The object id")
  typeObj = flag.String("type", "", "The server host")

  comments, err := client.Retrieve(ctx, &c.RetrieveRequest{IdObject: int32(*idObj), TypeObject: *typeObj})

The complete code is in the main.go file in the client folder.

Using container as runtime

Container runtimes offer us many advantages concerning portability, less overhead, decoupling, security, etc. In this case, we'll create a container to run our application.

Firstly, we need a file that contains the steps to generate our runtime container.

In addition, another good practice is to use a container to build the application. The Containerfile contains two steps, the first will build the image, and the second one is liable for the application execution.

We can check all the steps to open it.

Finally, the container can be built with the following instructions:

SERVICE_NAME=comment-service
VERSION=0.2
SERVICE_BUILD_TIME=$(date '+%Y/%m/%d %H:%M:%S')
podman build \
    --no-cache \
    --build-arg version=$VERSION \
    --build-arg serviceName=$SERVICE_NAME \
    --build-arg buildTime=$SERVICE_BUILD_TIME \
    -t quay.io/dborrego/$SERVICE_NAME:$VERSION \
    -f Containerfile

For example, you can use my container. I've deployed it in quay.io: https://quay.io/repository/dborrego/comment-service?tab=tags&tag=latest.

If you want to run it:

podman run \
	-p 8080:8080 \
	-d \
	--name grpc \
	quay.io/dborrego/comment-service:0.1

And now, then we can test again the application:

grpcurl -plaintext localhost:8080 list
CommentService
HealthService
InfoService
grpc.reflection.v1alpha.ServerReflection

Next steps

In this workshop, we've studied the easiest way to make many calls in the atomic model. Another very important advantage of gprc use is that you can implement a stream of calls both in the client and server.

In this example, you can see this example in the branch feature/client-stream

About

An example gRPC which uses a Golang application with some examples


Languages

Language:Go 86.1%Language:Dockerfile 13.9%