Skip to main content
under engineered

gRPC - the new REST!

REST is the ubiquitous way of writing APIs for as long as I can remember. Today I want to introduce you to a new way of writing APIs.

Have you met gRPC?

gRPC is a relatively new way to write APIs and consume them as if you're just calling a function.

There are lots of pros on using gRPC as the way to write services over REST that I went ahead and created this big table of analysis for you all :)

gRPC vs REST #

pivot gRPC REST
payload size protobuf: smaller and binary json: bigger in size cuz text
underlying protocol http/2 http/1
comm bidirectional cuz http/2 and async one way, client to server only (no server push)
integration just call autogenerated methods use http clients and painstakingly create the request object manually
api spec protofiles and autogenerated code bug the backend dev, or hope for a swagger link and lastly trial and error
speed 20-25 times faster: less payload and less CPU to convert from binary format slow due to text based payloads and new connection for each request, no multi-plexing available

variants #

Did I tell you that streaming is a first class citizen in gRPC due to HTTP/2 protocol. That means there are four types of APIs you can write and use

  1. no streaming at all just like plain REST - it's called unary api in gRPC lingo
  2. client calling once and then server streaming
  3. client streaming and then the server responding once
  4. both client and server streaming continuously

types of api

defining the interface #

Everything starts from a proto file, which contains the definition of the APIs (methods) that the server will implement and clients can call.

I am going to implement a sum service that would take two numbers and return the result.

Below are the definition of the request and response types along with Sum service

syntax = 'proto3';

message SumRequest {
	int32 num1 = 1;
	int32 num2 = 2;
}

message SumResponse {
	int32 sum = 1;
}

service DemoService {
	rpc Sum(SumRequest) returns (SumResponse);
}

Once this is done, just running mvn clean install will generate the request, response and service classes automatically which will be used to add the logic of the API (no sweat!)

If you're following along, then take a peek at the /target folder and you'll find all the autogenerated code there :)

making the API ready #

Time to see how absolutely lazy it is to create the API

import io.grpc.stub.StreamObserver;

public class DemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
	@Override
	public void sum(SumRequest request, StreamObserver<SumResponse> responseObserver) {

	}
}

Adding the logic for sum service; simplest logic in the world :P


import io.grpc.stub.StreamObserver;

public class DemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
	@Override
	public void sum(SumRequest request, StreamObserver<SumResponse> responseObserver) {
		int num1 = request.getNum1();
		int num2 = request.getNum2();

		int result = num1 + num2;

		// creating the response payload
		SumResponse response = SumResponse.newBuilder().setSum(result).build();

		// sending the payload
		responseObserver.onNext(response);
		responseObserver.onCompleted();
	}
}

starting the server #

With a handful of boilerplate code we can start the server by writing a main program in a separate class. The first line shows creating the server and adding the service to it. A new instance of DemoServiceImpl.

public class Application {
	public static void main(String[] args) throws IOException, InterruptedException {
		Server server = ServerBuilder.forPort(3000)
				.addService(new DemoServiceImpl())
				.build();

		server.start();

		// boring boilerplate here
		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
			server.shutdown();
		}));
		server.awaitTermination();
	}
}

consuming the service #

Consuming the service consists of three parts

creating the channel #

This specifies the endpoint and port of the service running. gRPC is language agnostic, which means you can write your service in one language and call it using another. But here I'm just sticking with java.

creating the stub #

This is done using the auto-generated classes. Stub can be of two types:

calling the service #

This is just calling a method on the stub which corresponds to the actual service running


// step 1 - creating the channel
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 3000)
    .usePlaintext()
    .build();

// step 2 - creating the stub
DemoServiceGrpc.DemoServiceBlockingStub stub = DemoServiceGrpc.newBlockingStub(channel);

// request
SumRequest request = SumRequest.newBuilder()
    .setNum1(10)
    .setNum2(20)
    .build();

// step 3 - call the service
SumResponse response = stub.sum(request);

// print the result
System.out.println("Response from gRPC service: " + response.getSum());

this is the magic of gRPC, on the client you're calling just a method and it invokes the actual remote implementation

streaming both ways #

Let's see how streaming work both ways, and the perfect example of it would be a chat api, where client is sending messages over the same connection and server responding.

adding new definition in the proto file #


   message ChatRequest {
     string message = 1;
   }

   message ChatResponse {
     string reply = 1;
   }

   service DemoService {
     rpc Sum(SumRequest) returns (SumResponse);
     rpc Chat(stream ChatRequest) returns (stream ChatResponse);
   }

Do mvn clean compile to update the generated-code

adding the chat method in service #

Back in DemoServiceImpl.java, I magically have the chat method signature which looks like this

	@Override
	public StreamObserver<ChatRequest> chat(StreamObserver<ChatResponse> responseObserver) {

	}

So let's implement this

As soon as you try to return the StreamObserver<ChatRequest> object, you'd see this code generated by the IDE. All you've to do is now add logic inside the three methods generated for you.

return new StreamObserver<ChatRequest>() {
  @Override
  public void onNext(ChatRequest chatRequest) {
    // this will be called for every client message
    // notice the argument gives you the client request directly
  }

  @Override
  public void onError(Throwable throwable) {
    // when there's an error at client side
  }

  @Override
  public void onCompleted() {
    // client is done!
    // no more messages would be sent
  }
};

Let's add the logic to reply to this client and reply back using the method argument responseObserver

@Override
public StreamObserver<ChatRequest> chat(StreamObserver<ChatResponse> responseObserver) {
  return new StreamObserver<ChatRequest>() {
    @Override
    public void onNext(ChatRequest chatRequest) {
      // extract the message
      String message = chatRequest.getMessage();

      // create the response
      ChatResponse response = ChatResponse.newBuilder()
          .setReply("server says thanks for sending: " + message)
          .build();

      // send!
      responseObserver.onNext(response);
    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onCompleted() {
      responseObserver.onCompleted();
    }
  };
}

calling from client #

private void chat() throws InterruptedException {
  DemoServiceGrpc.DemoServiceStub stub = DemoServiceGrpc.newStub(channel);

  StreamObserver<ChatRequest> request = stub.chat(new StreamObserver<ChatResponse>() {
    @Override
    public void onNext(ChatResponse chatResponse) {
      // called everytime server replies
      System.out.println(chatResponse.getReply());
    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onCompleted() {
      // server is done now
    }
  });

  request.onNext(create("1st message from client"));
  request.onNext(create("2nd message from client"));
  request.onNext(create("3rd message from client"));
  request.onNext(create("4th message from client"));

  request.onCompleted();
}

The final output

server says thanks for sending: 1st message from client
server says thanks for sending: 2nd message from client
server says thanks for sending: 3rd message from client
server says thanks for sending: 4th message from client

code #

All the code for this post is available here.

discuss on twitter, because why not?