gRPC - Unary gRPC



We will now look at various types of communication that the gRPC framework supports. We will use an example of Bookstore where the client can search and place an order for book delivery.

Let us see unary gRPC communication where we let the client search for a title and return randomly one of the book matching the title queried for.

.proto file

First let us define the bookstore.proto file in common_proto_files −

syntax = "proto3";
option java_package = "com.tp.bookstore";

service BookStore {
   rpc first (BookSearch) returns (Book) {}
}
message BookSearch {
   string name = 1;
   string author = 2;
   string genre = 3;
}
message Book {
   string name = 1;
   string author = 2;
   int32 price = 3;
}

Let us now take a closer look at each of the lines in the above block.

syntax = "proto3";

Syntax

The "syntax" here represents the version of Protobuf we are using. We are using the latest version 3 and the schema thus can use all the syntax which is valid for version 3.

package tutorial;

The package here is used for conflict resolution if, say, we have multiple classes/members with the same name.

option java_package = "com.tp.bookstore";

This argument is specific to Java, i.e., the package where to auto-generate the code from the .proto file.

service BookStore {
   rpc first (BookSearch) returns (Book) {}
}

This represents the name of the service "BookStore" and the function name "first" which can be called. The "first" function takes in the input of type "BookSearch" and returns the output of type "Book". So, effectively, we let the client search for a title and return one of the book matching the title queried for.

Now let us look at these types.

message BookSearch {
   string name = 1;
   string author = 2;
   string genre = 3;
}

In the above block, we have defined the BookSearch which contains the attributes like name, author and genre. The client is supposed to send the object of type of "BookSearch" to the server.

message Book {
   string name = 1;
   string author = 2;
   int32 price = 3;
}

Here, we have also defined that, given a "BookSearch", the server would return the "Book" which contains book attributes along with the price of the book. The server is supposed to send the object of type of "Book" to the client.

Note that we already had the Maven setup done for auto-generating our class files as well as our RPC code. So, now we can simply compile our project −

mvn clean install

This should auto-generate the source code required for us to use gRPC. The source code would be placed under −

Protobuf class code: target/generated-sources/protobuf/java/com.tp.bookstore
Protobuf gRPC code: target/generated-sources/protobuf/grpc-java/com.tp.bookstore

Setting up gRPC Server

Now that we have defined the proto file which contains the function definition, let us setup a server which can call these functions.

Let us write our server code to serve the above function and save it in com.tp.bookstore.BookeStoreServerUnary.java

Example

package com.tp.bookstore;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import com.tp.bookstore.BookStoreOuterClass.Book;
import com.tp.bookstore.BookStoreOuterClass.BookSearch;
public class BookeStoreServerUnary {
   private static final Logger logger = Logger.getLogger(BookeStoreServerUnary.class.getName());
 
   static Map<String, Book> bookMap = new HashMap<>();
   static {
      bookMap.put("Great Gatsby", Book.newBuilder().setName("Great Gatsby")
         .setAuthor("Scott Fitzgerald")
         .setPrice(300).build());
      bookMap.put("To Kill MockingBird", Book.newBuilder().setName("To Kill MockingBird")
         .setAuthor("Harper Lee")
         .setPrice(400).build());
      bookMap.put("Passage to India", Book.newBuilder().setName("Passage to India")
         .setAuthor("E.M.Forster")
         .setPrice(500).build());
      bookMap.put("The Side of Paradise", Book.newBuilder().setName("The Side of Paradise")
         .setAuthor("Scott Fitzgerald")
         .setPrice(600).build());
      bookMap.put("Go Set a Watchman", Book.newBuilder().setName("Go Set a Watchman")
         .setAuthor("Harper Lee")
         .setPrice(700).build());
   }
   private Server server;
   private void start() throws IOException {
      int port = 50051;
      server = ServerBuilder.forPort(port)
         .addService(new BookStoreImpl()).build().start();
 
      logger.info("Server started, listening on " + port);
 
      Runtime.getRuntime().addShutdownHook(new Thread() {
         @Override
         public void run() {
            System.err.println("Shutting down gRPC server");
            try {
               server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
               e.printStackTrace(System.err);
            }
         }
      });
   }
   public static void main(String[] args) throws IOException, InterruptedException {
      final BookeStoreServerUnary greetServer = new BookeStoreServerUnary();
      greetServer.start();
      greetServer.server.awaitTermination();
   }
   static class BookStoreImpl extends BookStoreGrpc.BookStoreImplBase {
      @Override
      public void first(BookSearch searchQuery, StreamObserver<Book> responseObserver) {
         logger.info("Searching for book with title: " + searchQuery.getName());
         List<String> matchingBookTitles = bookMap.keySet().stream().filter(title ->
            title.startsWith(searchQuery.getName().trim())).collect(Collectors.toList());

         Book foundBook = null;
         if(matchingBookTitles.size() > 0) {
            foundBook = bookMap.get(matchingBookTitles.get(0));
         }
         responseObserver.onNext(foundBook);
         responseObserver.onCompleted();
      }
   }
}

The above code starts a gRPC server at a specified port and serves the functions and services which we had written in our proto file. Let us walk through the above code −

  • Starting from the main method, we create a gRPC server at a specified port.

  • But before starting the server, we assign the server the service which we want to run, i.e., in our case, the BookStore service.

  • For this purpose, we need to pass the service instance to the server, so we go ahead and create a service instance, i.e., in our case, the BookStoreImpl

  • The service instance need to provide an implementation of the method/function which is present in the .proto file, i.e., in our case, the first method.

  • The method expects an object of type as defined in the .proto file, i.e.,for us the BookSearch

  • The method searches for the book in the available bookMap and then returns the Book by calling the onNext() method. Once done, the server announces that it is done with the output by calling onCompleted()

  • Finally, we also have a shutdown hook to ensure clean shutting down of the server when we are done executing our code.

Setting up gRPC Client

Now that we have written the code for the server, let us setup a client which can call these functions.

Let us write our client code to call the above function and save it in com.tp.bookstore.BookStoreClientUnaryBlocking.java

Example

package com.tp.bookstore;

import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.tp.bookstore.BookStoreOuterClass.Book;
import com.tp.bookstore.BookStoreOuterClass.BookSearch;
import com.tp.greeting.GreeterGrpc;
import com.tp.greeting.Greeting.ServerOutput;
import com.tp.greeting.Greeting.ClientInput;

public class BookStoreClientUnaryBlocking {
   private static final Logger logger = Logger.getLogger(BookStoreClientUnaryBlocking.class.getName());
   private final BookStoreGrpc.BookStoreBlockingStub blockingStub;
	
   public BookStoreClientUnaryBlocking(Channel channel) {
      blockingStub = BookStoreGrpc.newBlockingStub(channel);
   }
   public void getBook(String bookName) {
      logger.info("Querying for book with title: " + bookName);
      BookSearch request = BookSearch.newBuilder().setName(bookName).build();
 
   Book response; 
   try {
      response = blockingStub.first(request);
      } catch (StatusRuntimeException e) {
         logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
         return;
      }
      logger.info("Got following book from server: " + response);
   }
   public static void main(String[] args) throws Exception {
      String bookName = args[0];
      String serverAddress = "localhost:50051";
	  
      ManagedChannel channel = ManagedChannelBuilder.forTarget(serverAddress)
         .usePlaintext()
         .build();
 
      try {
         BookStoreClientUnaryBlocking client = new 
         BookStoreClientUnaryBlocking(channel);
         client.getBook(bookName);
      } finally {
         channel.shutdownNow().awaitTermination(5, 
         TimeUnit.SECONDS);
      }
   }
}

The above code starts a gRPC server at a specified port and serves the functions and services which we had written in our proto file. Let us walk through the above code −

  • Starting from the main method, we accept one argument, i.e., the title of the book we want to search for.

  • We setup a Channel for gRPC communication with our server.

  • And then, we create a blocking stub using the channel. This is where we choose the service "BookStore" whose functions we plan to call. A "stub" is nothing but a wrapper which hides the complexity of the remote call from the caller.

  • Then, we simply create the expected input defined in the .proto file,i.e., in our case BookSearch and we add the title name we want the server to search for.

  • We ultimately make the call and await the result from the server.

  • Finally, we close the channel to avoid any resource leak.

So, that is our client code.

Client Server Call

To sum up, what we want to do is the following −

  • Start the gRPC server.

  • The Client queries the Server for a book with a given name/title.

  • The Server searches the book in its store.

  • The Server then responds with the book and its other attributes.

Now, that we have defined our proto file, written our server and the client code, let us proceed to execute this code and see things in action.

For running the code, fire up two shells. Start the server on the first shell by executing the following command −

java -cp .\target\grpc-point-1.0.jar 
com.tp.bookstore.BookeStoreServerUnary

We would see the following output −

Output

Jul 03, 2021 7:21:58 PM 
com.tp.bookstore.BookeStoreServerUnary start
INFO: Server started, listening on 50051

The above output means the server has started.

Now, let us start the client.

java -cp .\target\grpc-point-1.0.jar 
com.tp.bookstore.BookStoreClientUnaryBlocking "To Kill"

We would see the following output −

Output

Jul 03, 2021 7:22:03 PM 
com.tp.bookstore.BookStoreClientUnaryBlocking getBook
INFO: Querying for book with title: To Kill

Jul 03, 2021 7:22:04 PM 
com.tp.bookstore.BookStoreClientUnaryBlocking getBook
INFO: Got following book from server: name: "To Kill 

MockingBird"
author: "Harper Lee"
price: 400

So, as we see, the client was able to get the book details by querying the server with the name of the book.

Advertisements