PolarSPARC |
Bhaskar S | 11/29/2020 |
Overview
gRPC is a distributed, heterogeneous, high performance, high throughput, modern, open source, remote procedure call (RPC) framework from Google with the following features:
Is platform neutral
Is language neutral with official support for C++, C#, Go, Java, JavaScript, Python, Ruby, etc
Uses ProtoBuf for defining message formats and service interfaces
Leverages HTTP/2 for efficient network communication
Provides support for both synchronous and asynchronous styles of communication
Has pluggable support for load balancing, health checking, and authentication
In short, with gRPC, services deployed on distributed systems can communicate with each other in an efficient and secure manner.
The following diagram illustrates the high-level architecture of gRPC (along with some circled number annotations):
The circled number annotations indicate the steps to implement, build, and deploy a gRPC based service and are as follows:
Create a .proto file with the definitions for the request message Req, the response message Res, and the service interface Service
Compile the .proto file to generate the gRPC code for the server gRPC Server and the gRPC code for the client gRPC Stub for the chosen language
Extend the gRPC Server code to implement and build the server side or the service provider
Extend the gRPC Stub code to implement and build the cide side or the service consumer
We will demonstrate gRPC using the Go and the Java 11 programming languages.
Installation and Setup
The installation is on a Ubuntu 20.04 LTS based Linux desktop.
We will also assume that the logged in user-id is alice with the home directory located at /home/alice.
We need to install the packages for the Go programming language called golang, the Java 11 programming language called openjdk-11-jdk, the Maven build management tool for Java called maven, and the protobuf compiler called protobuf-compiler from the Ubuntu repository.
To install the mentioned packages, execute the following commands:
$ sudo apt-get update
$ sudo apt-get install golang openjdk-11-jdk maven protobuf-compiler -y
For the Go language, we will create a directory called go under the home directory of the logged in user and set the GOPATH environment variable by executing the following commands:
$ cd $HOME
$ mkdir go
$ export GOPATH=$HOME/go
To setup the directory structure and Go dependencies for the demonstrations, execute the following commands:
$ cd $GOPATH
$ mkdir -p src/polarsparc.com/grpc
$ cd $GOPATH/src/polarsparc.com/grpc
$ go mod init polarsparc.com/grpc
$ GO111MODULE=on go get -u google.golang.org/grpc
$ GO111MODULE=on go get github.com/golang/protobuf/protoc-gen-go
To setup the Java directory structure for the demonstrations, execute the following commands:
$ cd $HOME
$ mkdir -p java/grpc
$ cd $HOME/java/grpc
$ mkdir -p src/main/java src/main/proto src/test/java target
$ mkdir -p src/main/java/com/polarsparc src/test/java/com/polarsparc
For the Java language, we will leverage Maven to manage the build as well as the package dependencies.
The following is the Maven pom.xml file located in the directory $HOME/java/grpc:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.polarsparc.grpc</groupId> <artifactId>gRPC</artifactId> <version>1.0</version> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.33.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.33.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.33.1</version> </dependency> <!-- Needed for Java 9 and above --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>annotations-api</artifactId> <version>6.0.53</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.6.3</version> <scope>test</scope> </dependency> </dependencies> <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.6.2</version> </extension> </extensions> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.6.1</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.14.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.33.1:exe:${os.detected.classifier}</pluginArtifact> <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Hands-on with gRPC
One of the prerequisites for gRPC is have a fundamental understanding of Protocol Buffers.
gRPC supports four types of communication patterns which are as follows:
Unary :: the client sends a single request to the server and the server responds with a single response
Server Streaming :: the client sends a single request to the server and the server responds with a sequence of responses
Client Streaming :: the client sends a sequence of requests to the server and the server responds with a single response
Bidirectional Streaming :: the client sends a sequence of requests to the server and the server with a sequence of responses (with the requests and responses operating independently)
Unary RPC
The following diagram illustrates the high-level architecture of Unary communication pattern:
For the Unary RPC demonstration, we will implement a simple Greet service, where the client sends a name in the request and the server responds back with an appropriate greeting message based on the time of the day.
We will first demonstrate the Greet service using the Go programming language.
In the $GOPATH directory, create the project directory hierarchy by executing the following commands:
$ cd $GOPATH/src/polarsparc.com/grpc
$ mkdir -p unary unary/greetpb unary/server unary/client
The following are the contents of the file greet.proto located in the directory $GOPATH/src/polarsparc.com/grpc/unary/greetpb as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ syntax = "proto3"; package unary; option go_package = "polarsparc.com/grpc/unary/greetpb"; message GreetRequest { string name = 1; } message GreetResponse { string message = 1; } service GreetService { rpc greet(GreetRequest) returns (GreetResponse); }
The request message is defined as GreetRequest and the response message is defined as GreetResponse. The service interface is defined as GreetService with an RPC method greet that takes in a GreetRequest as an input and returns a GreetResponse.
To compile the greet.proto file, execute the following commands:
$ cd $GOPATH/src/polarsparc.com/grpc/unary
$ protoc greetpb/greet.proto --go_out=plugins=grpc:$GOPATH/src
On success, this will generate the Go code file called greet.pb.go located in the directory $GOPATH/src/polarsparc.com/grpc/unary/greetpb.
From the file greet.pb.go, we see the GreetServiceServer interface, as shown below, that the server needs to implements:
. . . type GreetServiceServer interface { Greet(context.Context, *GreetRequest) (*GreetResponse, error) } . . .
The following are the contents of the file server.go for the Unary RPC server that implements the GreetServiceServer interface and is located in the directory $GOPATH/src/polarsparc.com/grpc/unary/server as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ package main import ( "context" "google.golang.org/grpc" "log" "net" "polarsparc.com/grpc/unary/greetpb" // [1] "time" ) type server struct {} // [2] func (s *server) Greet(_ context.Context, req *greetpb.GreetRequest) (*greetpb.GreetResponse, error) { // [3] log.Printf("Received a Greet request with req: %v\n", req) name := req.GetName() message := getMessage(name) res := &greetpb.GreetResponse{ Message: message, } return res, nil } const ( addr = "127.0.0.1:20001" ) func main() { log.Printf("Ready to start the Greet server on %s", addr) lis, err := net.Listen("tcp", addr) if err != nil { log.Fatalf("Failed to create listener on %s", addr) } srv := grpc.NewServer() // [4] greetpb.RegisterGreetServiceServer(srv, &server{}) // [5] if err = srv.Serve(lis); err != nil { log.Fatalf("Failed to start server: %v", err) } } func getMessage(name string) string { hour := time.Now().Hour() msg := "Hello, " + name + ", " if hour < 12 { msg = msg + "Good Morning !!!" } else if hour < 16 { msg = msg + "Good Afternoon !!!" } else if hour < 21 { msg = msg + "Good Evening !!!" } else { msg = msg + "Good Night !!!" } return msg }
The following are brief descriptions for some of the Go type(s)/method(s) used in the code above:
[1] :: import the code from the package polarsparc.com/grpc/unary/greetpb generated by the protoc compiler
[2] :: an abstract type used for associating the service method(s) as receivers
[3] :: receiver method that implements the service method Greet. It takes two input arguments - a Context object and a GreetRequest object, and returns a GreetResponse object. A Context is used to wrap request specific values such as an authentication token, a timeout value for the RPC call, etc. The objects GreetRequest and GreetResponse are the Go types associated with the corresponding message types from the greet.proto file
[4] :: create an instance of the gRPC server
[5] :: register an instance of the server object (that implements the service method Greet) with the gRPC server
The following are the contents of the file client.go that implements the Unary RPC client for the GreetService located in the directory $GOPATH/src/polarsparc.com/grpc/unary/client as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ package main import ( "golang.org/x/net/context" "google.golang.org/grpc" "log" "polarsparc.com/grpc/unary/greetpb" ) const ( addr = "127.0.0.1:20001" ) func main() { log.Println("Ready to start the Greet client...") conn, err := grpc.Dial(addr, grpc.WithInsecure()) if err != nil { log.Fatalf("Failed to connect to %s", addr) } defer conn.Close() cl := greetpb.NewGreetServiceClient(conn) // [1] req := &greetpb.GreetRequest{ // [2] Name: "Alice", } res, err := cl.Greet(context.Background(), req) // [3] if err != nil { log.Fatalf("Failed to send Greet request to %s [%v]", addr, err) } log.Printf("%s\n", res.Message) }
The following are brief descriptions for some of the Go type(s)/method(s) used in the code above:
[1] :: create an instance of the gRPC client stub NewGreetServiceClient generated by the protoc compiler
[2] :: create an instance of the request object GreetRequest
[3] :: invoke the gRPC method Greet using the client stub. The method context.Background() returns an empty Context object without any values set
The following diagram illustrates the contents of the directory $GOPATH/src/polarsparc.com/grpc:
Open two Terminal windows - one for the server and one for the client.
In the server Terminal, execute the following commands:
$ cd $GOPATH/src/polarsparc.com/grpc/unary/server
$ go run server.go
The following would be the typical output:
2020/11/28 20:48:59 Ready to start the Greet server on 127.0.0.1:20001
In the client Terminal, execute the following commands:
$ cd $GOPATH/src/polarsparc.com/grpc/unary/client
$ go run client.go
The following would be the typical output:
2020/11/28 20:50:55 Ready to start the Greet client... 2020/11/28 20:50:55 Hello, Alice, Good Evening !!!
AWESOME !!! We have successfully demonstrated the Unary gRPC communication style using the Go language.
The following are the contents of the file greet.proto located in the directory $HOME/java/grpc/src/main/proto as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ syntax = "proto3"; package unary; option java_multiple_files = true; option java_package = "com.polarsparc.gun"; message GreetRequest { string name = 1; } message GreetResponse { string message = 1; } service GreetService { rpc greet(GreetRequest) returns (GreetResponse); }
To compile the greet.proto file, execute the following commands:
$ cd $HOME/java/grpc
$ mvn compile
On success, this will generate some files in the directory $HOME/java/grpc/target/generated-sources/protobuf/java/com/polarsparc/gun.
The following diagram illustrates the contents of the directory $HOME/java/grpc/target/generated-sources :
The following are the contents of the Java program called GreetService.java that implements the Unary gRPC service GreetService located in the directory $HOME/java/grpc/src/main/java/com/polarsparc/gun/server as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ package com.polarsparc.gun.server; import com.polarsparc.gun.GreetRequest; import com.polarsparc.gun.GreetResponse; import com.polarsparc.gun.GreetServiceGrpc; import io.grpc.stub.StreamObserver; import java.time.LocalTime; public class GreetService extends GreetServiceGrpc.GreetServiceImplBase { // [1] @Override public void greet(GreetRequest request, StreamObserver<GreetResponse> responseObserver) { // [2] String message = getMessage(request.getName()); GreetResponse response = GreetResponse.newBuilder() .setMessage(message) .build(); responseObserver.onNext(response); // [3] responseObserver.onCompleted(); // [4] } private static String getMessage(String name) { LocalTime lt = LocalTime.now(); int hour = lt.getHour(); StringBuilder sb = new StringBuilder("Hello ").append(name).append(", "); if (hour < 12) { sb.append("Good Morning !!!"); } else if (hour < 16) { sb.append("Good Afternoon !!!"); } else if (hour < 21) { sb.append("Good Evening !!!"); } else { sb.append("Good Night !!!"); } return sb.toString(); } }
The following are brief descriptions for some of the Java class(es)/method(s) used in the code above:
[1] :: extend the base class GreetServiceGrpc.GreetServiceImplBase generated by the maven protobuf compiler plugin
[2] :: override the service method Greet which takes two input arguments - a GreetRequest object and a StreamObserver object. The StreamObserver object is used for sending the GreetResponse object
[3] :: method onNext on the StreamObserver object is used for sending the response object of type GreetResponse
[4] :: method onCompleted on the StreamObserver signals the succesful completion of sending a response
The following are the contents of the Java program called GreetServer.java that implements the Unary RPC service GreetService located in the directory $HOME/java/grpc/src/main/java/com/polarsparc/gun/server as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ package com.polarsparc.gun.server; import io.grpc.Server; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import java.io.IOException; import java.net.InetSocketAddress; public class GreetServer { public static void main(String[] args) { Server server = NettyServerBuilder // [1] .forAddress(new InetSocketAddress("127.0.0.1", 20001)) .addService(new GreetService()) // [2] .build(); try { server.start(); // [3] } catch (IOException e) { e.printStackTrace(); } System.out.print("Started the gRPC GreetService on 127.0.0.1:20001 ...\n"); try { server.awaitTermination(); // [4] } catch (InterruptedException e) { e.printStackTrace(); } } }
The following are brief descriptions for some of the Java class(es)/method(s) used in the code above:
[1] :: create an instance of the gRPC server NettyServerBuilder on the specified ip address and port
[2] :: register an instance of the GreetService object (that implements the service method Greet) with the gRPC server
[3] :: start the gRPC server
[4] :: wait till the gRPC server terminates
The following are the contents of the Java program called GreetClientTest.java that implements the Unary RPC client for GreetService located in the directory $HOME/java/grpc/src/test/java/com/polarsparc/gun/client as shown below:
/* @Author: Bhaskar S @Blog: https://www.polarsparc.com @Date: 28 Nov 2020 */ package com.polarsparc.gun.client; import com.polarsparc.gun.GreetRequest; import com.polarsparc.gun.GreetResponse; import com.polarsparc.gun.GreetServiceGrpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class GreetClientTest { private GreetServiceGrpc.GreetServiceBlockingStub stub; @BeforeAll public void setup() { ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", 20001) // [1] .usePlaintext() // [2] .build(); this.stub = GreetServiceGrpc.newBlockingStub(channel); // [3] } @Test public void greetTest() { GreetRequest request = GreetRequest.newBuilder() // [4] .setName("Bob") .build(); GreetResponse response = this.stub.greet(request); // [5] System.out.printf("%s\n", response.getMessage()); } }
The following are brief descriptions for some of the Java class(es)/method(s) used in the code above:
[1] :: create an instance of the object ManagedChannel that represents a virtual gRPC connection to the service endpoint on the specified ip address and port
[2] :: indicate that we are using an unsecured communication channel
[3] :: create an instance of the gRPC client stub GreetServiceBlockingStub generated by the protoc compiler
[4] :: create an instance of the request object GreetRequest
[5] :: invoke the gRPC method Greet using the client stub
The following diagram illustrates the contents of the directory $HOME/java/grpc:
Open two Terminal windows - one for the server and one for the client.
In the server Terminal, execute the following commands:
$ cd $HOME/java/grpc
$ mvn exec:java -Dexec.mainClass=com.polarsparc.gun.server.GreetServer
The following would be the typical output:
Started the gRPC GreetService on 127.0.0.1:20001 ...
In the client Terminal, execute the following commands:
$ cd $HOME/java/grpc
$ mvn test -Dtest=com.polarsparc.gun.client.GreetClientTest
Without the maven plugin maven-surefire-plugin defined in the pom.xml file, the maven command mvn test ... will NOT execute any of the tests
The following would be the typical output:
Hello Bob, Good Night !!!
One could also test with the Go server running and using the Java client and vice versa.
Without the same package unary; defined in the greet.proto file, the Go server and the Java client or vice versa will not be able communicate with each other
References