Sitemap

What is gRPC?

10 min readOct 15, 2024

--

gRPC is an open source framework used for service-to-service communication developed by Google. The abbreviation of RPC in its name, Remote Procedure Call, indicates that we can call a method on a remote server as if it were our own method.

When we look at a gRPC call, we see two basic structures. Server and Client. When we write our method, the models and the necessary definition of the method are made in the proto file. On the server side, the method is overridden and its content is filled. On the client side, this method is called to provide synchronous communication. We will discuss the details in the following sections.

How it Works

In the gRPC architecture, each client service contains a stub (automatically generated files) that is like an interface containing valid remote procedures. We call this a “stub”. After the client makes the call with the necessary parameters, the stub serializes the parameters using protobuf and saves the request to the local client-time library.

The operating system then calls the remote server using HTTP 2. The stub on the server receives the packets and decodes the parameters using protobuf. The server stub receives the response after calling the relevant function and sends it back to the client transport layer.

Finally, the client stub receives the returned response, resolves the parameters using protobuf again, and returns to the executed call.

Nowadays, when it comes to synchronous communication, the first method that comes to mind will always be REST. gRPC can of course be considered as an alternative to REST, but it would be wrong to think that we should remove all REST endpoints in the project and write gRPC methods instead.

Let’s explain with an example of microservice architecture. You have an online shopping system. In this system, we have a frontend application, and the backend structure consists of 4 different modules: order, payment, user and notification. For example, if a product is to be ordered, the following steps will occur:

  • The frontend application calls the “buyProduct” endpoint of the order service and triggers the ordering process.
  • The order service calls the “payProductPrice” endpoint of the payment service and starts the payment process.
  • After the successful response from the payment service, the order service throws an event asynchronously and ensures that the notification service sends an e-mail to the user.
  • The notification service calls the “getContactInfo” endpoint of the user service using the userId in the event and notifies the user of the order result via e-mail and push notification.

We decided to use gRPC in our project. Let’s see at which points we will perform the conversion and at which points we will continue with the same method.

In the first step, the “buyProduct” endpoint will continue to be used. Because we cannot use gRPC directly for calls on browsers. This prevents direct use, especially in web applications. For this reason, REST is still considered the best solution for external calls. In the second step, the “payProductPrice” endpoint can be converted to a gRPC method. An implementation can be made in such a way that the order service is the client and the payment service is the server. In the third step, we cannot talk about gRPC calls. Because gRPC can be used in synchronous communications. In the last step, we can convert the “getContactInfo” endpoint to a gRPC method so that the notification service is the client and the user service is the server.

Let’s give another example scenario:

  • To learn the number of sent mails on the frontend user detail page, the user service calls the “getSendedMailCount” endpoint.
  • The user service then calls the notification service’s “sendedMailCount” endpoint and gets the number of sent mails and returns to the frontend.

Here, the same REST usage is continued in the first step. In the second step, the “sendedMailCount” endpoint can be converted to a gRPC method, with the user service being the client and the notification service being the server.

There is a service that I want to draw your attention to in the two examples above. The User service became a gRPC server in the first scenario, while it became a gRPC client in the second scenario. At the same time, the use of REST continued in the second scenario. What I mean to say is that when starting to use gRPC, it is necessary not to have a perspective such as completely abandoning the use of REST and switching to gRPC. Just as both REST and gRPC can be used together in an application, an application can be both a server and a client at the same time.

gRPC vs REST

Although they have similarities, gRPC and REST differ from each other at many points. Let’s look at which aspects are similar and which aspects provide advantages to one another.

  • Both communicate over the HTTP protocol, but REST uses HTTP 1.1 while gRPC uses HTTP 2.
  • gRPC provides bidirectional stream communication. This means that both the client and the server can send and receive multiple requests and responses simultaneously over a single connection. REST does not offer this feature.
  • REST has a loose relationship. What does this mean? There is no contract between the client and the server. There is no limit for the data that goes in and out. But gRPC has a tight relationship. That is, the data between the client and the server is made within a certain contract. Any update made in one must be made in the other.
  • While data is transmitted in JSON format in REST communication, gRPC carries data in binary format. This causes a significant speed increase.
  • If you have a REST API, there is a 1:1 communication between the client and the server. A request is sent and a response is expected. In gRPC, there can be a 1:1 communication as well as a stream. You can make a gRPC call in the following 4 ways. Also, I am just touching on this article, but I will explain it in detail in the next article and explain how to implement it with examples.

1- Unary -> A single response is returned for a single request.
2- Client Stream -> The client sends multiple requests and the server returns a single response.
3- Server Stream -> The client sends a single request and the server returns multiple responses.
4- Bidirectional -> It is a combination of the previous two methods. The server returns multiple responses in response to multiple requests from the client.

REST ile kıyaslamalarımızı yaptık şimdi bir gRPC iletişimi nasıl hazırlarız ona bakarız.

Protobuf

Protobuf is an IDL (Interactive Data Language) used for gRPC. It is basically where you store data models and function contracts in the form of a proto file. Since it is a contract, both the client and the server must have the same proto file. You can see the contents of a sample proto file below.

syntax = "proto3";

package example;

option java_multiple_files = true;
option java_package = "com.example.grpc";

message TestRequest {
string name = 1;
string surname = 2;
}

message TestResponse {
string message = 1;
}

service TestService {
rpc sayHello(TestRequest) returns (TestResponse) {}
}

One of the most important features of microservice architecture is that it is modular and these modules can have different structures and technologies. In other words, while one service is written in Java, the other can be written in Python. As we mentioned before, the client and the server use the same proto file. If we assume that the client is Java and the server is Python, the proto file must also be language independent. Model and function definitions are used in the same way in all projects and code is generated according to that language.

If you pay attention, there are a few lines about Java. The lines written with the Option key are added specifically for Java applications. In this way, different option properties can be added for each language. Another thing I want to draw attention to is that when defining the fields in the models, an index is given for each. An index is assigned for each field starting from 1, and each index must be unique.

In the example below, you can see the use of timestamp, enum, list, nested model.

syntax = "proto3";

package example;

import "google/protobuf/timestamp.proto"

option java_multiple_files = true;
option java_package = "com.example.grpc";

message TestRequest {
string name = 1;
string surname = 2;
}

message TestResponse {
string message = 1;
google.protobuf.Timestamp creationDate = 2;
TestStatus testStatus = 3;
repeated string keyList = 4;
ExampleRes example = 5;

enum TestStatus {
ACTIVE = 0;
PASSIVE = 1;
}
}

message ExampleRes {
int32 count = 1;
bool up = 2;
}

service TestService {
rpc sayHello(TestRequest) returns (TestResponse) {}
}

You may say that I wrote validations for requests in my rest service. How can I do this in the proto file? You should use third-party libraries for this. For example, if you add the “io.envoyproxy.protoc-gen-validate:pgv-java-grpc” dependency for Java, you can do validation as follows.

message Person {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 50}];
int32 age = 2 [(validate.rules).int32 = {gte: 0, lte: 150}];
string email = 3 [(validate.rules).string = {email: true}];
}

Let’s also talk about nullable. You cannot set null to proto models by default. Your application will throw a null pointer exception. There is an approach for this. Let’s make the TestRequest in name field in the example above nullable.

syntax = "proto3";

package example;

import "google/protobuf/timestamp.proto"
import "google/protobuf/struct.proto"

option java_multiple_files = true;
option java_package = "com.example.grpc";

message TestRequest {
NullableStringField name = 1;
string surname = 2;
}

message TestResponse {
string message = 1;
google.protobuf.Timestamp creationDate = 2;

TestStatus testStatus = 3;
repeated string keyList = 4;
ExampleRes example = 5;

enum TestStatus {
ACTIVE = 0;
PASSIVE = 1;
}
}

message ExampleRes {
int32 count = 1;
bool up = 2;
}

message NullableStringField {
oneof kind {
google.protobuf.NullValue null = 1;
string value = 2;
}
}

service TestService {
rpc sayHello(TestRequest) returns (TestResponse) {}
}

For more detailed information about Protobuf, data types, best practices, etc., you can browse its page.

After examining the Proto sytanx, let’s now go through a real scenario. Let’s first see the “getContactInfo” service in the flow we mentioned earlier in REST form. In this article, I will give examples from Spring Boot.

Controller:

@GetMapping("/getContactInfo")
public UserInfo getUserContactInfo(@RequestParam String userId) {
return UserInfo.builder()
.name("Test user")
.age(30)
.email(test@example.com)
.phone(+9005554443322)
.build();
}

Model:

@Data
@Builder
public class UserInfo {
private String name;
private int age;
private String email;
private String phone;
}

Here we send userId as a request parameter to getContanctInfo API and the server fills in the UserInfo model and returns it. We will implement this endpoint with gRPC. First, we need to create the proto file. The proto file required for the model and function will be as follows.

syntax = "proto3";

package example;

option java_multiple_files = true;
option java_package = "com.example.grpc";

message UserRequest {
string userId = 1;
}

message UserInfo {
string name = 1;
int32 age = 2;
string email = 3;
string phone = 4;
}

service UserService {
rpc getContactInfo (UserRequest) returns (UserInfo) {}
}

We added this file to our server project. Then we build the project by adding the necessary dependencies and plugins and gRPC generates the necessary service and model classes from this proto file. I will not go into the details of this part, we will go into more detail in the next article when we create an end-to-end project together. Let’s not forget that since we have generated classes here, if a new field is to be added or removed or an update is to be made, we cannot do it directly from the classes. We need to update the proto file and rebuild it. Of course, the same update made on the server should be made on the client, we have talked about this before.

We added the proto file, built it and generated the necessary classes. Now let’s implement our method using them.

Server:

@GrpcService
public class UserInfoService extends UserServiceGrpc.UserServiceImplBase {

@Override
public void getContactInfo(UserRequest userRequest, StreamObserver<UserInfo> responseObserver) {
UserInfo userInfo = UserInfo.newBuilder()
.setName("Test user")
.setAge(30)
.setEmail(test@example.com)
.setPhone(+9005554443322)
.build();

responseObserver.onNext(userInfo);
responseObserver.onCompleted();
}
}

Let’s add the same proto file to the client application. We built it and now let’s implement it to call the gRPC function in the server application.

Client:

public class UserInfoClientService {

@GrpcClient("user.service")
private UserServiceGrpc.UserServiceBlockinStub userServiceBlockingStub;

public UserInfo getUserInfoFromGrpc(String userId) {
return userServiceBlockingStub.getContactInfo(UserRequest.newBuilder()
.setUserId(userId)
.build());
}
}

Yes, it is really that simple to convert a REST endpoint to a gRPC function. Its usage is also very simple. Of course, when advanced topics such as header, servlet context etc. are involved, there are bottlenecks and the complexity increases. These are the subjects of another article. Also, I did not include the parts about the changes in pom.xml and application.properties here. I do not want to go into too much detail about the Java implementation. As I said, we will discuss the entire system in detail in the next article.

Although gRPC is not widely used at the moment, it has started to replace REST in some places. It is seen as an effective and fast communication model, especially for microservice architecture.

See you in the next article…

--

--

Emre Akın
Emre Akın

Written by Emre Akın

Kafka, Java, EDA and more ...

No responses yet