Iniciando com gRPC

Luiz Felipe da Silva
mobicareofficial
Published in
5 min readJan 4, 2021

O que é gRPC?

Antes de falar sobre gRPC, precisamos saber o que é o RPC. Para não misturar os assuntos e conceitos, RPC é a definição de um protocolo para executar procedimentos em outros computadores em rede, e não cabe ao RPC especificar como a mensagem é enviada de um processo para o outro.

O gRPC é um framework do Google que implementa RPC, e tem inúmeras vantagens para a escolha do gRPC.

Principais vantagens

  • Desenvolvimento de API Contract-first, com Protocol Buffer por padrão;
  • Disponível em mais de 10 linguagens de programação;
  • Suporte a chamadas streaming do cliente para o servidor, do servidor para cliente e bidirecional (falarei mais deste assunto);
  • Reduz a utilização da rede, latência, pois os dados são trafegados em binário;
  • Utiliza o protocolo HTTP2.

Protocol Buffer

Protocol buffer ou protobuf é um método criado pelo Google de serialização de dados estruturados, agnóstico de linguagem. A transferência de dados chega a ser até 6x mais rápida que um JSON. O gRPC utiliza o arquivo com extensão .proto para criar o código base, garantindo o Contract-first.

A serialização/deserialização faz um uso menos intensivo da CPU pelo fato das mensagens estarem em formato binário, ou seja, mais próximo de como o computador representa os dados.

Cliente e o servidor em qualquer linguagem

Atualmente, o Google tem disponibilizado o gRPC em mais de 10 linguagens. Podendo utilizar qualquer uma no cliente e no servidor.

Principais vantagens do HTTP2

Citarei as principais vantagens, mas não entrarei em detalhes, já existe bastante conteúdo na internet falando sobre isso.

  • Fluxo multiplexados
  • Compressão do cabeçalho
  • Protocolo binário

Demonstração de como o http2 é mais rápido que o http1.1. Se você tiver uma internet muito rápida, pode experimentar colocar no navegador no modo low-end mobile

Mão na massa

Para este tutorial, farei um crud de usuários no mongodb, vou utilizar o servidor e o cliente no mesmo projeto em java.

Dependências que estou utilizando no servidor. Para outras configurações, vou deixar o link do projeto no github.

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation 'io.grpc:grpc-netty-shaded:1.33.1'
implementation 'io.grpc:grpc-protobuf:1.33.1'
implementation 'io.grpc:grpc-stub:1.33.1'
implementation "io.grpc:grpc-services:1.33.1" // reflection

testCompile group: 'junit', name: 'junit', version: '4.12'

compile group: 'org.mongodb', name: 'mongodb-driver-sync', version: '4.1.1'
}

SERVER

1. Precisamos definir o .proto que será o contrato da nossa API. Arquivo user.proto. Observe que a operação ListUser, o servidor irá mandar os usuários por stream na mesma conexão tcp, e o cliente irá ficar recebendo os usuários em tempo real. Para as operações CreateUser e DeleteUser será unário, para cada request, uma response.

Não vou fazer stream do cliente para o server e nem bidirecional, pois o artigo ficará gigante, mas a forma como fazemos não é muito diferente.

syntax = "proto3";

package user;

option java_package = "com.proto.user";
option java_multiple_files = true;

message User {
string id = 1;
string name = 2;
string email = 3;
}

message CreateUserRequest {
User user = 1;
}

message CreateUserResponse {
User user = 1;
}

message DeleteUserRequest {
string userId = 1;
}

message DeleteUserResponse {
string userId = 1;
}

message ListUserRequest {

}

message ListUserResponse {
User user = 1;
}

service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {};
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {}; // return NOT_FOUND if not found
rpc ListUser(ListUserRequest) returns (stream ListUserResponse) {};
}

2. Agora precisamos executar o plugin que gerará todo o código que o gRPC irá usar com base no que definimos no user.proto. O build do projeto faz este trabalho automaticamente. Não precisamos nos preocupar como os arquivos foram gerados, apenas com a implementação deles.

3. Feito isto, precisamos subir nosso servidor para receber requests, mas ainda não temos nenhum serviço implementado.

package br.com.crudgrpc.server;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;

public class UserServer {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("Server start");
Server server = ServerBuilder.forPort(50051)
.build();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.shutdown();
}));
server.awaitTermination();
}
}

4. Como definimos três operações no serviço UserService, iremos implementar apenas a lógica de como cada uma irá proceder, o gRPC irá criar tudo que precisamos para implementar.

Para isto, criei o arquivo UserServiceImpl com algumas configurações para conectar no mongodb e as operações. A primeira operação será a CreateUser, é uma request unária, ou seja, uma request, uma response.

@Override
public void createUser(CreateUserRequest request, StreamObserver<CreateUserResponse> responseObserver) {
System.out.println("Creating User");
User user = request.getUser();
Document doc = new Document("name", user.getName())
.append("email", user.getEmail());
collection.insertOne(doc);
String userID = doc.getObjectId("_id").toString();
System.out.println("Inserted user: " + userID);
CreateUserResponse response = CreateUserResponse.newBuilder()
.setUser(user.toBuilder().setId(userID).build())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}

5. Operação deletar usuário: esta operação tem um tratamento para usuário não encontrado, assim como fazemos nas rest apis.

@Override
public void deleteUser(DeleteUserRequest request, StreamObserver<DeleteUserResponse> responseObserver) {
String userID = request.getUserId();
DeleteResult result = null;
try{
result = collection.deleteOne(eq("_id", new ObjectId(userID)));
}catch (Exception e){
System.out.println("User not found");
responseObserver.onError(
Status.NOT_FOUND
.withDescription("User not found for id: " + userID)
.augmentDescription(e.getLocalizedMessage())
.asRuntimeException()
);
}
if(result.getDeletedCount() == 0){
System.out.println("User not found for id: " + userID);
responseObserver.onError(
Status.NOT_FOUND
.withDescription("User not found for id: " + userID)
.asRuntimeException()
);
}else{
System.out.println("User was deleted");
responseObserver.onNext(
DeleteUserResponse.newBuilder()
.setUserId(userID)
.build()
);
responseObserver.onCompleted();
}
}

6. Operação listar usuários: nesta operação, o cliente fica com uma conexão aberta recebendo os usuários por stream.

@Override
public void listUser(ListUserRequest request, StreamObserver<ListUserResponse> responseObserver) {
System.out.println("Streaming users");
collection.find().iterator().forEachRemaining(document -> responseObserver.onNext(
ListUserResponse.newBuilder()
.setUser(this.documentToUser(document))
.build()
));
responseObserver.onCompleted();
}

private User documentToUser(Document document){
return User.newBuilder()
.setName(document.getString("name"))
.setEmail(document.getString("email"))
.build();
}

7. Para finalizar o servidor, precisamos disponibilizar o serviço que implementamos para ele. Apenas adicionei a linha 11, abaixo da instância do server.

package br.com.crudgrpc.server;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;

public class UserServer {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("Server start");
Server server = ServerBuilder.forPort(50051)
.addService(new UserServiceImpl())
.build();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.shutdown();
}));
server.awaitTermination();
}
}

Client

Para o cliente, vou implementar as operações de criar, deletar e listar usuário.

1. Precisamos criar um channel para comunicar com o servidor.

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
UserServiceGrpc.UserServiceBlockingStub userClient = UserServiceGrpc.newBlockingStub(channel);

2. Criar novo usuário.

User user = User.newBuilder()
.setName("luiz")
.setEmail("teste@teste.com")
.build();
CreateUserResponse createUserResponse = userClient.createUser(
CreateUserRequest.newBuilder()
.setUser(user)
.build()
);
System.out.println(createUserResponse.toString());

3. Deletar usuário.

String userId = createUserResponse.getUser().getId();
DeleteUserResponse deleteUserResponse = userClient.deleteUser(
DeleteUserRequest.newBuilder().setUserId(userId).build()
);
System.out.println(deleteUserResponse.toString());

4. Listar usuários. Perceba que aqui estamos usando streaming para enviar os usuários do servidor para o cliente na mesma conexão TCP.

userClient.listUser(ListUserRequest.newBuilder().build()).forEachRemaining(
listUserResponse -> System.out.println(listUserResponse.getUser().toString())
);

Olá! Me chamo Luiz Felipe da Silva, sou desenvolvedor na Mobicare e movido a tecnologia. Estou sempre em busca de aprender e contribuir com novas tecnologias e desafios.

A Mobicare e a Akross combinam os Melhores Talentos, Tecnologias de Ponta, Práticas Agile e DevOps com Capacidades Operacionais avançadas para ajudar Operadoras Telecom e grandes empresas a gerarem novas receitas e a melhorarem a experiência dos seus próprios clientes.

Se você gosta de inovar, trabalhar com tecnologia de ponta e está sempre buscando conhecimento, somos um match perfeito!

Faça parte do nosso time. 😉

--

--