MICROSERVICES, gRPC, AND GOLANG

MICROSERVICES, gRPC, AND GOLANG

Microservices - also known as microservice architecture - is an architectural style that structures an application as a collection of highly maintainable, tested and loosely coupled services.

A microservice is therefore a tiny service that can stand alone and communicate with other services over a connection. Each independently deployable microservice can be owned by a small team and written to address a particular business domain.

MICROSERVICES COMMUNICATION TECHNIQUES

There is always a need for a service or an application to talk to another application and this communication can occur in 2 different techniques: Synchronous Communication Techniques and Asynchronous Communication techniques.

Synchronous Communication Technique

This method involves a client sending a request message to the server and awaiting a response message. Synchronous communication can involve HTTP protocol and other protocols such as RPC. It involves exposing an interface of an application that other services can call. It happens in real-time. When a request is made to an endpoint, the caller remains locked or blocked in the interaction until a response is received.

Asynchronous Communication Technique

This communication technique eliminates the need to wait for a response from another service. Communication is processed by using a broker known as an event broker such as Kafka, and RabbitMQ. Information passage happens independent of time. Messages from the requests are placed in an ordered message queue such that even if the receiver is unavailable at the request time, the message remains in the queue to be processed later. The request and the response occur independently from each other.

gRPC

gRPC uses synchronous techniques for communication primarily. gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework that can run in any environment https://grpc.io/. It efficiently connects services in and across data centres. gRPC allows the client and the server to communicate by making remote calls.

The gRPC Concept

The concept of gRPC is built around the following:

  • Protocol Buffer: Protocol Buffer, also known as Protobuf, is used in gRPC as the Interface Definition Language (IDL). It is Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. Protobuf can easily be compared to JSON but is smaller, faster and more efficient.

Protobuf is defined in a .proto file. gRPC services and messages are defined in the .proto files and the code is generated from the defined language in the .proto file. You can read more on protocol buffers

“You define gRPC services in ordinary protocol buffer format, with RPC method parameters and return types specified as protocol buffer messages. Since the service definition is an extension to the protocol buffer specification, a special gRPC plug-in is used to generate code from your proto file.” - Excerpt From gRPC Up & Running by Kasun Indrasiri & Danesh Kuruppu

  • Communication Patterns: There are four fundamental approaches to communication in gRPC; Simple (Unary) RPC, Server, Server-Streaming RPC, Client-Streaming RPC and Bi-directional Streaming RPC
  1. Simple (Unary) RPC: A client calls a remote function of a gRPC service, a request is sent to the server and the server sends back a response. This is the simplest pattern of a gRPC.

  2. Server-Streaming RPC: In this type, the client a single request to the server and the server responds with a series of responses. The client continues to read from the stream until there are no more messages and at this point, the server marks the end of the stream by sending the status details.

  3. Client-Streaming RPC: This can be explained as the opposite of the server-streaming RPC. In this case, the client sends multiple requests and the server in response sends back a single response to the client.

  4. Bi-directional Streaming RPC: The client sends requests to the server as a stream and the server responds with a stream of messages. The communication is based on the gRPC logic. The two streams operate independently. The client and server can read and write in whatever order they like. For example, the server may send a response after receiving one or more messages from the streams or wait till it receives all the messages.

  • HTTP/2: In gRPC, the network communication between the client and the server occurs over HTTP/2. HTTP/2 provides support for all HTTP/1.1 functions but more efficiently, it addressed and overcame some of the HTTP/1.1 limitations. Applications written with this protocol are simpler, faster, and more powerful. HTTP/2 allows persistent connections by implementing streams. Streams can be defined as connected small units of messages called frames. Upon creating a channel in gRPC, an HTTP/2 connection is created with the server which allows multiple streams over one connection.

GO IMPLEMENTATION OF SIMPLE (Unary) RPC

  • Install the Protobuf compiler

brew install protobuf

Note: The command above is macOS specific, to install for other OS, follow this link here

  • Install the protocol compiler plugins for Go

protoc-gen-go

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

Note: You can also find the commands here

Here I will be explaining how to create a gRPC client connection and a gRPC server connection using golang.

  • I will create a .proto file in mygrpc directory. The .proto file is where we use our protobuf to define our services and our messages.
Server-side

                                    /proto/user.proto

// the proto version
syntax = "proto3";
package grpcUser;
// define where you want the generated code to be
// we want our generated code to be inside 
// the "pb" directory that is within the "proto" directory
option go_package = "/proto/pb";

message UserRequest {
  string first_name = 1;
  string last_name = 2;
  string address = 3;
  bool verified = 4;
}

message GetUserRequest {
  bool verified = 1;
}

message Response {
  int32 code = 1;
  string message = 2;
}

service UserService {
  rpc AddUser(UserRequest) returns (Response);
  rpc GetUser(GetUserRequest) returns (UserRequest);
}

Generate the code

  • I need to compile the .proto file before I can work with the service and messages. To compile the .proto file, I can run one of these commands:

  • If the paths=import flag is specified, the output file is placed in a directory named after the Go package's import path: the package definition in our code option go_package = "/proto/pb".

    • My proto/user.proto has a Go import path of /proto/pb and it will result in an output file at /proto/pb/user.pb.go

    • This is also the default output mode even if a paths flag is not specified.


// without a path flag
protoc --go_out=. --go-grpc_out=. proto/*.proto

// with a path flag
 protoc --go_opt=paths=import --go_out=. --go-grpc_out=. proto/*.proto
  • If the paths=source_relative flag is specified, the output file is placed in the same relative directory as the input file.

    • My proto/user.proto results in an output file at protos/user.pb.go.

protoc --go_opt=paths=source_relative --go_out=. --go-grpc_out=. proto/*.proto

For a more organised structure, I will be use this: protoc --go_out=. --go-grpc_out=. proto/*.proto

Note: "Because you want Go code, you use the --go_out option." See here

"The protocol buffer compiler produces Go output when invoked with the go_out flag. The argument to the go_out flag is the directory where you want the compiler to write your Go output. The compiler creates a single source file for each .proto file input. The name of the output file is created by replacing the .proto extension with .pb.go. Where in the output directory the generated .pb.go file is placed depends on the compiler flags as explained above in the Generate the code section." See here and here

Build logic

The generated codes are found in /proto/pb/user.pb.go and /proto/pb/user_grpc.pb.go. Now that the go codes have been generated above, I will work on a simple logic to add a user and retrieve a user's details

                                      /server/server.go
package server

import (
    "context"
    "grpcUser/proto/pb"
)

// embed UmimplementedUserServiceServer from the generated code for future compatibility
type Server struct {
    pb.UnimplementedUserServiceServer
    Users *pb.UserRequests
}

func NewServer(users *pb.UserRequests) *Server {
    return &Server{Users: users}
}

var users []*pb.UserRequest

func (s *Server) AddUser(ctx context.Context, req *pb.UserRequest) (*pb.Response, error) {
    users = append(users, req)
    s.Users = &pb.UserRequests{
        Users: users,
    }
    return &pb.Response{Code: 200, Message: "user saved successfully"}, nil
}

func (s *Server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserRequests, error) {
    for _, user := range s.Users.Users {
        if user.Verified == req.Verified {
            users = append(users, user)
        }
    }
    return &pb.UserRequests{Users: users}, nil
}

Note: Here is why we embed pb.UnimplementedUserServiceServer. You might also see in some places where the command below is used instead of the ones listed above. The notable difference here is "--go_out=plugins=grpc:." which specifies the plugin to load without specifying "--go-grpc_out=.". This will only generate a single file "user.pb.go". --go_out=plugins=grpc:.


protoc --go_opt=paths=source_relative --go_out=plugins=grpc:. proto/*.proto

Using this command above, you can also make use of the generated "UserServiceServer interface" in the "user.pb.go" by implementing it instead of embedding the "pb.UnimplementedUserServiceServer". This can be preferable if you would like to know if you have fully implemented all the methods in your gRPC service rather than waiting till run time.

Let's register the service and receive incoming requests

                                                    main.go

package main

import (
    "google.golang.org/grpc"
    "grpcUser/proto/pb"
    "grpcUser/server"
    "log"
    "net"
)

const (
    port = ":8080"
)

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    reflection.Register(s)
    serve := server.NewServer(&pb.UserRequests{})

    pb.RegisterUserServiceServer(s, serve)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
Code Explanation
  • It listens on port 8080

  • A new gRPC instance was created with grpc.NewServer

  • Register reflection service on gRPC server because grpcurl works better with reflection. Read more

  • The function NewServer() in the server package is called and the parameter passed in

  • The user service implementation(serve)is registered to the gRPC server.

  • Bind the lis(net.Listen) and gRPC server so it communicates on port 8080

Run the application

  • Run using

go run main.go
  • I will be using BloomRPC to send my requests. Install BloomRPC for mac using this command
brew install --cask bloomrpc

Screenshot 2022-07-05 at 15.04.34 1.png

Screenshot 2022-07-05 at 15.26.36.png

  • I can also use grpcurl by installing it
brew install grpcurl
  • You can check your services and see all the methods it implements
grpcurl -plaintext localhost:8080 describe

Screenshot 2022-07-05 at 17.16.42.png

  • Make a call to the services like this:
grpcurl -plaintext -d '{"first_name": "John", "last_name": "Doe", "address": "Somewhere", "verified": true}' localhost:8080 packageName.ServiceName/MethodName

Screenshot 2022-07-06 at 09.58.06.png

Get the repository on GitHub.

If you enjoyed this article, share it with your friends and colleagues! Thank you for reading and Stay tuned for more on gRPC!

REFERENCES