gRPC communication with golang

gRPC communication with golang

what is gRPC?

To understand gRPC first we need to know what RPC is.RPC
(Remote Procedure Call) is used to create Inter-Process-Communication between services where services can communicate with each other remotely. And gRPC is an open-source high-performance RPC framework that was implemented by Google. It is widely used in a microservices architecture.

Why gRPC?

gRPC is way faster than any RPC or REST API. It uses HTTP/2 for high-scale performance and by default, gRPC uses a protocol buffer for data serialization which makes the payload faster than using any other JSON or XML to format the data cause the protocol buffer uses binary as payload (smaller and faster). gRPC also provides bidirectional streaming.

Types of gRPC Communication

there are four types of gRPC communication

  • Unary

    where the client sends a request to the server and gets a single response back.

  • Server Streaming

    where the client sends a request to the server and gets a stream of responses from the server. It's similar when we request to watch a video on youtube and then youtube sends chunks of video as a response.

  • Client Streaming

    where the client writes a stream of messages and sends them to the server and the server returns the response. when we upload data to a server.

  • Bidirectional Streaming

    where the client and server both send messages to each other. when a server gets a request from the client and sends back a response then the client sends another request according to the response.

Getting started with GRPC in Golang

The first step is to install the Go plugins for the protocol compiler:

Install the protocol compiler plugins for Go using the following commands:

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.22

Install the protocol compiler plugins for Go using the following commands:

export PATH="$PATH:$(go env GOPATH)/bin"

Now create a folder named "proto" and then create a file named "greet.proto". Next, we need to configure our ".proto" file then protoc command will generate the code based on the desired language. A schema for a particular use of protocol buffers associates data types with field names, using integers to identify each field. here's the code for greet.proto

// version of protocol buffer 
syntax="proto3";

// declaring go import path
option go_package = "./proto";

// importing this package for empty type declaration
import "google/protobuf/empty.proto";

package greet_service;


service GreetService{

   // unary communication
   rpc SayHello(google.protobuf.Empty) returns (HelloResponse); 

   // server streaming communication
   rpc SayHelloServerStreaming(NameList) returns (stream HelloResponse);

   // client streaming communication
   rpc SayHelloClientStreaming(stream HelloRequest) returns (MessageList);

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

}

message HelloRequest{
    string name = 1;
}

message NameList{
    // "repeated" represents array here
    repeated string message = 1;
}

message HelloResponse{
   string message = 1;
}

message MessageList{
    repeated string messages = 1;
}

run protoc --go_out=. --go-grpc_out=. proto/greet.proto at the root of the directory this command will generate client and server stubs in the proto folder.

Server-side

create a server folder and main.go file

//                            server/main.go
package main

import (
    "log"
    "net"

    pb "github.com/IRSHIT033/go-grpc/proto"

    "google.golang.org/grpc"
)
// define the port
const (
    port = ":8080"
)
// struct that represents the rpc server
type helloServer struct {
    // protocol buffer generated server struct
    pb.GreetServiceServer
}

func main() {
    // listen on the port
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("Failed to start the server %v", err)
    }
    // creating new gRPC server
    grpcServer := grpc.NewServer()
    // register the server
    pb.RegisterGreetServiceServer(grpcServer, &helloServer{})
    log.Printf("server started at %v", lis.Addr())
    // now gRPC server accepts the request from the client
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("Failed to start: %v", err)
    }

}

Client-side

create a client folder and main.go file

//                            client/main.go
package main

import (
    "log"

    pb "github.com/IRSHIT033/go-grpc/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

const (
    port = ":8080"
)

func main() {
    // creates a client connection to the server
    conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect : %v", err)
    }
    defer conn.Close()
    // created gRPC client 
    client := pb.NewGreetServiceClient(conn)

}

both server and client setup is done

Unary Communication

this code will create a unary communication in grpc between client and server.

// server/unary.go

// emptypb.Empty for empty type struct
// same as the rpc function in the proto
func (s *helloServer) SayHello(ctx context.Context, in *emptypb.Empty) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{
        Message: "hello",
    }, nil
}
// client/unary.go
func callSayHello(client pb.GreetServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    // emptypb.Empty for empty type struct
    resp, err := client.SayHello(ctx, &emptypb.Empty{})
    if err != nil {
        log.Fatalf("could not greet : %v", err)
    }
    log.Printf("%s", resp.Message)
}

and then call the callSayHello(client pb.GreetServiceClient) in the client/main.go

Client-Side-Streaming

// server/client_stream.go
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.MessageList{Messages: messages})
        }
        if err != nil {
            return nil
        }
        log.Printf("Got request with names: %v", req.Name)
        messages = append(messages, "Hello ", req.Name)
    }
}
// client/client_stream.go
func callSayHelloClientStream(client pb.GreetServiceClient, names *pb.NameList) {
    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.Message {
        req := &pb.HelloRequest{
            Name: name,
        }
        if err := stream.Send(req); err != nil {
            log.Fatalf("error while sending %v", err)
        }
        log.Printf("send the request with name: %s", name)
        time.Sleep(2 * time.Second)
    }

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

and then call the callSayHelloClientStream(client pb.GreetServiceClient, names *pb.NameList) in the client/main.go

Server-Side-Streaming


// server/server_stream.go

func (s *helloServer) SayHelloServerStreaming(req *pb.NameList, stream pb.GreetService_SayHelloServerStreamingServer) error {
    log.Printf("got request with names : %v", req.Message)
    for _, name := range req.Message {
        res := &pb.HelloResponse{
            Message: "Hello " + name,
        }
        if err := stream.Send(res); err != nil {
            return err
        }
        time.Sleep(2 * time.Second)
    }
    return nil
}

// client/server_stream.go

func callSayHelloServerStream(client pb.GreetServiceClient, names *pb.NameList) {
    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 running %v", err)
        }
        log.Println(message)
    }
    log.Println("streaming finished")
}

and then call the callSayHelloServerStream(client pb.GreetServiceClient, names *pb.NameList) in the client/main.go

Bidirectional-Streaming


// server/bidirectional_stream.go

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 the request with name:%v", req.Name)
        res := &pb.HelloResponse{
            Message: "Hello" + req.Name,
        }
        if err := stream.Send(res); err != nil {
            return err
        }
    }
}

// client/bidirectional_stream.go

func callSayHelloBidirectionalStream(client pb.GreetServiceClient, names *pb.NameList) {
    log.Printf("Bidirectional streaming started")
    stream, err := client.SayHelloBidirectionalStreaming(context.Background())
    if err != nil {
        log.Fatalf("could not send names : %v", err)
    }
   // creating a channel
    waitc := make(chan struct{})
   // creating a go routine function receives data stream from server
    go func() {
        for {
            message, err := stream.Recv()
            if err == io.EOF {
    // get out infinite loop when there's not any response from the server
                break
            }
            if err != nil {
                log.Fatalf("error while running %v", err)
            }
            log.Println(message)
        }
    // closing the channel
        close(waitc)
    }()

    for _, name := range names.Message {
        req := &pb.HelloRequest{
            Name: name,
        }
        // sends requests to the server
        if err := stream.Send(req); err != nil {
            log.Fatalf("Error while sending %v", err)
        }
        // using sleep for for the delay to
        time.Sleep(2 * time.Second)
    }
    stream.CloseSend()
    // block until the channel is closed  
    <-waitc
    log.Printf("Bidirectional streaming finished")
}

and then call the callSayHelloBidirectionalStream(client pb.GreetServiceClient, names *pb.NameList) in the client/main.go

here's the GitHub code:- GRPC_GO_project