Microservices Communication using gRPC Protocol

Microservices Communication using gRPC Protocol


Today, we'll explore the gRPC protocol, a technology that has achieved remarkable success by opting for HTTP/2 over HTTP/1.1 and embracing protocol buffers in place of XML and JSON.

Microservices inter-process communication

In a microservices-based software system, inter-process communication is vital for applications to exchange information. Microservices can interact with each other through two primary communication mechanisms:

  1. Synchronous Communication (Request-Reply): In this mode, one service initiates communication by invoking an API provided by another service and then awaits a response. For effective use, the API should have well-defined semantics and support versioning.
  2. Asynchronous Communication: In contrast, asynchronous communication involves a service dispatching a message without expecting an immediate response. Multiple services can potentially handle and process these messages.

The most prevalent approach for implementing request-reply communication is often through RESTful services, typically utilizing JSON payloads over HTTP. However, RESTful services come with some drawbacks, including being bulky, less efficient, and constraining for certain scenarios. The bulkiness of RESTful services primarily stems from their reliance on JSON, which employs a text-based encoding format that consumes more space compared to binary formats.

Why gRPC for microservices communication?

In order to address the limitations commonly found in RESTful services, there is a growing need for a contemporary inter-process communication system that offers scalability and superior efficiency. Enter gRPC, an advanced open-source RPC framework.

The gRPC framework leverages a binary encoding format called protocol buffers and operates atop HTTP/2. It boasts strong typing, supports multiple programming languages, and facilitates bidirectional streaming for both client and server sides.

gRPC benefits

gRPC offers a refreshed take on the old RPC design method by making it interoperable, modern, and efficient using such technologies as Protocol Buffers and HTTP/2. The following benefits make it a solid candidate for replacing REST in some operations.

Lightweight messages. Depending on the type of call, gRPC-specific messages can be up to 30 percent smaller in size than JSON messages.

High performance. By different evaluations, gRPC is 5, 7, and even 8 times faster than REST+JSON communication.

Built-in code generation. gRPC has automated code generation in different programming languages including Java, C++, Python, Go, Dart, Objective-C, Ruby, and more.

More connection options. While REST focuses on request-response architecture, gRPC provides support for data streaming with event-driven architectures: server-side streaming, client-side streaming, and bidirectional streaming.

Here are the main types of gRPC:

  1. gRPC Unary RPC: Unary RPC is the simplest form of gRPC. In a unary RPC, the client sends a single request to the server and receives a single response in return. This is similar to a traditional synchronous function call. Unary RPC is suitable for scenarios where a single request and response are sufficient.
  2. gRPC Server Streaming RPC: Server Streaming RPC allows the client to send a single request to the server and receive a stream of responses in return. This is useful when the server needs to send a sequence of data to the client, such as real-time updates or a large set of results.
  3. gRPC Client Streaming RPC: Client Streaming RPC is the opposite of Server Streaming RPC. In this case, the client sends a stream of requests to the server and receives a single response in return. This is useful when the client has a continuous flow of data to send to the server, and the server processes it and responds when it's done.
  4. gRPC Bidirectional Streaming RPC: Bidirectional Streaming RPC allows both the client and server to send a stream of messages to each other. This creates a full-duplex communication channel where both sides can send and receive data independently. Bidirectional Streaming RPC is suitable for scenarios requiring interactive and continuous communication.

Example using Golang

This is a basic gRPC server and client written in Go. It is based on the gRPC Quickstart and gRPC Basics: Go tutorials .

We are going to implemented a simple gRPC server and client with the following functionality:

  • simple Unary RPC
  • server-side streaming RPC
  • client-side streaming RPC
  • bidirectional streaming RPC


Setting up a gRPC-Go project

  • Create a new directory for your project and cd into it

mkdir GO-grpc-demo 
cd GO-grpc-demo
mkdir client server proto        

  • Installing the gRPC Go plugin

go get google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
export PATH="$PATH:$(go env GOPATH)/bin"        

  • Initialise a Go module. Replace prabhatpankaj with your git username

go mod init github.com/prabhatpankaj/GO-grpc-demo
go mod tidy        

  • Create the proto file with the required services and messages in the proto directory

// proto/greet.proto

// for syntax highlighting we use proto3 version

syntax="proto3";

// path to our current folder, where the generated files will be placed
option go_package = "./proto";

// package name for our proto file
package greet_service;

// defining all the Services for the gRPC
service GreetService {
    // simple Unary RPC
    rpc SayHello(NoParam) returns (HelloResponse);

    // server streaming RPC
    rpc SayHelloServerStreaming(NamesList) returns (stream HelloResponse);

    // client streaming RPC
    rpc SayHelloClientStreaming(stream HelloRequest) returns (MessagesList);

    // bidirectional streaming RPC
    rpc SayHelloBidirectionalStreaming(stream HelloRequest) returns (stream HelloResponse);
}

// when you are not passing any params still you will need to add a type
// NoParam acts as a type for no parameters given.
message NoParam {};

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

message NamesList {
    repeated string names = 1;
}

message MessagesList {
    repeated string messages = 1;
}
        

  • Generate .pb.go files from the proto filedepending on what path you mention in your greet.proto file, We will run this

protoc --go_out=. --go-grpc_out=. proto/greet.proto
        

This will generate greet_grpc.pb.go and greet.pb.go as

  • create server/main.go

package main

import (
	"fmt"
	"log"
	"net"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
)

const (
	port = "8080"
)

// this is the struct to be created, pb is imported upstairs
type helloServer struct {
	pb.GreetServiceServer
}

func main() {
	//listen on the port
	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", port))
	if err != nil {
		log.Fatalf("Failed to start server %v", err)
	}
	// create a new gRPC server
	grpcServer := grpc.NewServer()
	// register the greet service
	pb.RegisterGreetServiceServer(grpcServer, &helloServer{})
	log.Printf("Server started at %v", lis.Addr())
	//list is the port, the grpc server needs to start there
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("Failed to start: %v", err)
	}
}
        

  • create client/main.go

package main

import (
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	port = ":8080"
)

func main() {
	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewGreetServiceClient(conn)

	names := &pb.NamesList{
		Names: []string{"Akhil", "Alice", "Bob"},
	}
        
        // need to write grpc type function here


}
        

Simple Unary RPC

  • need to create unary.go in both client and server folder as

client/unary.go

package main

import (
	"context"
	"log"
	"time"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

// callSayHello will need to be called from main.go to make the call to the server
func callSayHello(client pb.GreetServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	res, err := client.SayHello(ctx, &pb.NoParam{})
	if err != nil {
		log.Fatalf("Could not greet: %v", err)
	}
	log.Printf("%s", res.Message)
}
        

  • server/unary.go

package main

import (
	"context"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func (s *helloServer) SayHello(ctx context.Context, req *pb.NoParam) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{
		Message: "Hello unary",
	}, nil
}
        

  • add function callSayHello() in client/main.go

package main

import (
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	port = ":8080"
)

func main() {
	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewGreetServiceClient(conn)

	callSayHello(client)
}
        

  • run go run server/main.go

  • run go run client/main.go

hello unary is coming from server/unary.go as simple request/response .

server-side streaming RPC

  • need to create server_stream.go in both client and server folder as

client/server_stream.go

package main

import (
	"context"
	"io"
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func callSayHelloServerStream(client pb.GreetServiceClient, names *pb.NamesList) {
	log.Printf("Streaming started")
	stream, err := client.SayHelloServerStreaming(context.Background(), names)
	if err != nil {
		log.Fatalf("Could not send names: %v", err)
	}

	for {
		message, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("Error while streaming %v", err)
		}
		log.Println(message)
	}

	log.Printf("Streaming finished")
}
        

  • server/server_stream.go

package main

import (
	"log"
	"time"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func (s *helloServer) SayHelloServerStreaming(req *pb.NamesList, stream pb.GreetService_SayHelloServerStreamingServer) error {
	log.Printf("Got request with names : %v", req.Names)
	for _, name := range req.Names {
		res := &pb.HelloResponse{
			Message: "Hello " + name,
		}
		if err := stream.Send(res); err != nil {
			return err
		}
		// 2 second delay to simulate a long running process
		time.Sleep(2 * time.Second)
	}
	return nil
}
        

  • add function callSayHelloServerStream() in client/main.go

package main

import (
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	port = ":8080"
)

func main() {
	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewGreetServiceClient(conn)

	names := &pb.NamesList{
		Names: []string{"name 1", "name 2", "name 3"},
	}

	// callSayHello(client)
	callSayHelloServerStream(client, names)
}
        

Here i am sending Names: []string{"name 1", "name 2", "name 3"} from client and in return getting stream of data from server as below.

client-side streaming RPC

  • need to create client_stream.go in both client and server folder as

client/client_stream.go

package main

import (
	"context"
	"log"
	"time"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func callSayHelloClientStream(client pb.GreetServiceClient, names *pb.NamesList) {
	log.Printf("Client Streaming started")
	stream, err := client.SayHelloClientStreaming(context.Background())
	if err != nil {
		log.Fatalf("Could not send names: %v", err)
	}

	for _, name := range names.Names {
		req := &pb.HelloRequest{
			Name: name,
		}
		if err := stream.Send(req); err != nil {
			log.Fatalf("Error while sending %v", err)
		}
		log.Printf("Sent request with name: %s", name)
		time.Sleep(2 * time.Second)
	}

	res, err := stream.CloseAndRecv()
	log.Printf("Client Streaming finished")
	if err != nil {
		log.Fatalf("Error while receiving %v", err)
	}
	log.Printf("%v", res.Messages)
}
        

  • server/client_stream.go

package main

import (
	"io"
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func (s *helloServer) SayHelloClientStreaming(stream pb.GreetService_SayHelloClientStreamingServer) error {
	var messages []string
	for {
		req, err := stream.Recv()
		if err == io.EOF {
			return stream.SendAndClose(&pb.MessagesList{Messages: messages})
		}
		if err != nil {
			return err
		}
		log.Printf("Got request with name : %v", req.Name)
		messages = append(messages, "Hello "+req.Name)
	}
}
        

  • add function callSayHelloClientStream() in client/main.go

package main

import (
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	port = ":8080"
)

func main() {
	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewGreetServiceClient(conn)

	names := &pb.NamesList{
		Names: []string{"name 1", "name 2", "name 3"},
	}

	// callSayHello(client)
	// callSayHelloServerStream(client, names)
	callSayHelloClientStream(client, names)
}
        

Here i am sending Names: []string{"name 1", "name 2", "name 3"} from client as stream . Once Client streaming finished , it returns a single response message as below.

bidirectional streaming RPC

  • need to create bi_stream.go in both client and server folder as

client/bi_stream.go

package main

import (
	"context"
	"io"
	"log"
	"time"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func callSayHelloBidirectionalStream(client pb.GreetServiceClient, names *pb.NamesList) {
	log.Printf("Bidirectional Streaming started")
	stream, err := client.SayHelloBidirectionalStreaming(context.Background())
	if err != nil {
		log.Fatalf("Could not send names: %v", err)
	}

	waitc := make(chan struct{})

	go func() {
		for {
			message, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Fatalf("Error while streaming %v", err)
			}
			log.Println(message)
		}
		close(waitc)
	}()

	for _, name := range names.Names {
		req := &pb.HelloRequest{
			Name: name,
		}
		if err := stream.Send(req); err != nil {
			log.Fatalf("Error while sending %v", err)
		}
		time.Sleep(2 * time.Second)
	}

	stream.CloseSend()
	<-waitc
	log.Printf("Bidirectional Streaming finished")
}
        

  • server/bi_stream.go

package main

import (
	"io"
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
)

func (s *helloServer) SayHelloBidirectionalStreaming(stream pb.GreetService_SayHelloBidirectionalStreamingServer) error {
	for {
		req, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		log.Printf("Got request with name : %v", req.Name)
		res := &pb.HelloResponse{
			Message: "Hello " + req.Name,
		}
		if err := stream.Send(res); err != nil {
			return err
		}
	}
}
        

  • add function callSayHelloBidirectionalStream() in client/main.go

package main

import (
	"log"

	pb "github.com/prabhatpankaj/GO-grpc-demo/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	port = ":8080"
)

func main() {
	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewGreetServiceClient(conn)

	names := &pb.NamesList{
		Names: []string{"name 1", "name 2", "name 3"},
	}

	// callSayHello(client)
	// callSayHelloServerStream(client, names)
	// callSayHelloClientStream(client, names)
	callSayHelloBidirectionalStream(client, names)
}
        

Here i am sending Names: []string{"name 1", "name 2", "name 3"} from client.

It started streaming from client and server as bidirectional stream as below.

Final Code can be downloaded from Github Repo


References:

https://grpc.io/

https://grpc.io/docs/languages/go/quickstart/

https://grpc.io/docs/languages/go/basics/

https://www.youtube.com/watch?v=a6G5-LUlFO4&t=832s


要查看或添加评论,请登录

社区洞察

其他会员也浏览了