Posts Entendendo protocol buffers — protobuf
Post
Cancel

Entendendo Protocol Buffers — Protobuf

Uma imagem grande onde está escrito

Introdução

Protocol Buffers (protobuf) é um método de serializar dados estruturados que é principalmente útil para comunicação entre serviços ou até mesmo para guardar dados. Ele foi desenhado pelo Google no começo de 2001 (mas apenas publicamente liberado em 2008) para ser menor e mais rápido que o XML. As mensagens protobuf são serializadas em um formato binary wire que é muito compacto, aumentando a performance.

Detalhes

Este protocolo envolve uma linguagem de descrição de interface específica para modelar a estrutura de dados definido em um arquivo .proto Um programa de qualquer linguagem suportada consegue gerar através de um compilador código fonte nativo utilizando o contrato afim de criar ou interpretar streams de bytes que representam os dados estruturados. Protocol buffers normalmente serve como base para remote procedure call (RPC) que é muito usada para comunicação de aplicações entre diferentes máquinas, o mais comumente utilizado é o gRPC. Protobuf é similar ao Apache Thrift usado pelo Facebook, Ion criado pela Amazon ou o Microsoft Bonds Protocol.

Exemplo

Neste exemplo nós iremos criar dois projetos:

  • Uma aplicação console Java que irá utilizar uma especificação .proto de um cliente (customer.proto) para gerar um arquivo com um cliente hard coded

  • Uma aplicação console C# que irá ler o arquivo do cliente hard coded gerado pela aplicação java e exibir os dados no console

Resumo

Caso você apenas queira ler o código e ir descobrindo e aprendendo por conta própria, disponibilizei repositório com estas duas aplicações. Primeiramente siga as instruções do arquivo README_pt.md do projeto Java no repositório:

https://github.com/danielpadua/protobuf-example-java

Após gerar o arquivo serializado com protobuf, siga as instruções do arquivo README_pt.md do projeto C# no repositório:

https://github.com/danielpadua/protobuf-example-csharp

Contrato Protobuf

Primeiro de tudo, vamos criar uma estrutura que irá representar um cliente. Os dados que precisamos representar são:

  • Identificação única (ID)
  • Foto
  • Nome
  • Data de nascimento
  • Data/Hora de criação
  • Data/Hora da última atualização

Portanto, será necessário criar um arquivo .proto como este:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

package danielpadua.protobufexample.contracts;

option java_multiple_files = true;
option java_outer_classname = "CustomerProto";
option java_package = "dev.danielpadua.protobufexamplejava.contracts";
option csharp_namespace = "DanielPadua.ProtobufExampleDotnet.Contracts";

import "google/protobuf/timestamp.proto";
import "money.proto";
import "date.proto";

message Customer {
    int64 id = 1;
    bytes photo = 2;
    string name = 3;
    google.type.Date birthdate = 4;
    google.type.Money balance = 5;
    google.protobuf.Timestamp createdAt = 6;
    google.protobuf.Timestamp updatedAt = 7;
}

Alguns pontos sobre o código acima:

  • O tipo Timestamp é um “Well Known Type” que foi introduzido no proto3, você pode utilizar estes tipos apenas importando-os nas primeiras linhas do arquivo proto

  • Os tipos Date e Money são “Google Common Type”, diferentes do “Well Known Type” não é possível usá-los apenas realizando a importação. É necessário copiar o conteúdo destes arquivos do repositório do google e colar no seu projeto, qualquer que seja a linguagem que está utilizando.

Existem outros tipos, você pode ler a documentação aqui. Dos Well Known Types aqui e do Google Common Types aqui.

Este arquivo .proto será um recurso em comum entre os projetos C# e Java, mas para simplificar o exemplo, irei recriá-lo nos repositórios de ambos projetos. O ideal para projetos grandes e complexos seria ter um repositório separado como um ponto único para os projetos que irão utilizá-lo.

Aplicação Java Console

Com isto explicado, vamos começar a criar uma aplicação Java console. Para este exemplo irei utilizar a OpenJDK 15, IntelliJ IDEA CE e o Maven como build tool.

  1. Abra o IntelliJ IDEA CE and escolha criar um novo projeto

Criando um novo projetoCriando um novo projeto

  1. Escolha Maven no painel esquerdo, selecione sua Java JDK no canto superior direito. Como disse anteriormente, irei utilizar a OpenJDK 15 que havia instalado anteriormente

Detalhes do novo projetoDetalhes do novo projeto

  1. Preencha os próximos campos como desejar, por exemplo:

Configurações maven do projetoConfigurações maven do projeto

Projeto criado. Vamos começar adicionando o encoding do código fonte, informar ao compilador maven que estaremos utilizando a JDK 15 e adicionando a dependência protobuf-java feita pelo Google. Adicione as linhas a seguir no arquivo pom.xml dentro da tag “projeto”, logo após a tag “version”:

1
2
3
4
5
6
7
8
9
10
11
12
13
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>15</maven.compiler.source>
    <maven.compiler.target>15</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.13.0</version>
    </dependency>
</dependencies>

Agora, vamos incluir os arquivos .proto definidos na seção acima. Mas antes crie um diretório “proto” dentro de src/main:

Criando o diretório dos arquivos protobufCriando o diretório dos arquivos protobuf

Depois adicione um novo arquivo chamado: customer.proto e coloque o código mencionado na seção acima:

Erro ao importar outros arquivos protobufErro ao importar outros arquivos protobuf

Os imports do money.proto e date.proto irão mostrar erro porque ainda não os criamos. Você pode criá-los repetindo o procedimento acima adicionando o código do money.proto e date.proto diretamente do repositório do Google.

customer.proto sem erroscustomer.proto sem erros

Ok, contratos protobuf criados. Agora, para gerarmos o código fonte nativo (classes java) a partir do contrato, nós precisamos usar o executável protoc para compilar os arquivos .proto apontando a linguagem desejada de saída. Existem duas maneiras principais de fazer isto:

  • Manualmente, baixando o protoc na sua máquina e executando-o. Caso você deseje ir por este caminho, leia o guia de instalação do protoc aqui

  • Automaticamente, adicionando geração de código via protoc no build do seu projeto maven. Existem alguns plugins maven para isto, mas irei utilizar o protoc-jar-maven-plugin, que envolve o executável do protoc como um jar assim ele pode ser executado em qualquer SO e então compilar seus arquivos .proto.

Você pode começar a utilizá-lo apenas adicionando as linhas abaixo no seu pom.xml abaixo da tag “project”, logo após a tag “dependencies”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<build>
    <plugins>
        <plugin>
            <groupId>com.github.os72</groupId>
            <artifactId>protoc-jar-maven-plugin</artifactId>
            <version>3.11.4</version>
            <executions>
                <execution>
                    <id>protoc.main</id>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>run</goal>
                    </goals>
                    <configuration>
                        <protocVersion>3.13.0</protocVersion>
                        <addSources>main</addSources>
                        <includeMavenTypes>direct</includeMavenTypes>
                        <includeStdTypes>true</includeStdTypes>
                        <includeDirectories>
                            <include>src/main/proto</include>
                        </includeDirectories>
                        <inputDirectories>
                            <include>src/main/proto</include>
                        </inputDirectories>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Sempre verifique se existem novas versões estáveis antes de adicionar dependências ou plugins em seu pom.xml.

Nós estamos adicionando a estrutura de diretório onde estamos armazenando os arquivo .proto, assim o plugin saberá onde eles estão para compilá-los.

Neste ponto é provável que você queira executar o compilador protoc para gerar as classes java que representam os contratos protobuf, então, clique na tab maven e depois no botão run maven goal e escreva: mvn clean install

Executando maven clean installExecutando maven clean install

Caso a compilação tenha sucesso, a mensagem a seguir irá ser exibida:

Um build de sucessoUm build de sucesso

Próximo passo é criar um pacote dev.danielpadua.protobufexamplejava e dentro do mesmo uma classe principal para nossa aplicação console:

Nosso método mainNosso método main

Caso você digite “Customer”, o autocomplete do IntelliJ irá aparecer e te sugerir importar a classe gerada pelo protoc via plugin:

Sucesso ao auto-completar classes geradasSucesso ao auto-completar classes geradas

Então, deu tudo certo. Agora nós podemos escrever o código para gerar o cliente hard coded e gravá-lo no diretório que você desejar (dentro do método main):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Date birthdate = Utils.toGoogleDate(LocalDate.of(1990, 4, 30));
Money balance = Utils.toGoogleMoney(BigDecimal.valueOf(9000.53));
Timestamp createdUpdateAt = Utils.toGoogleTimestampUTC(LocalDateTime.now());
String fullPath = "/Users/danielpadua/protobuf/protobuf-customer";

try (FileOutputStream fos = new FileOutputStream(fullPath)) {
    Customer daniel = Customer.newBuilder()
            .setId(1)
            .setPhoto(ByteString.EMPTY)
            .setName("Daniel")
            .setBirthdate(birthdate)
            .setBalance(balance)
            .setCreatedAt(createdUpdateAt)
            .setUpdatedAt(createdUpdateAt)
            .build();

    daniel.writeTo(fos);
    System.out.println("protobuf-customer created successfully");
} catch (FileNotFoundException e) {
    System.out.println(format("could not find file {0}", fullPath));
} catch (IOException e) {
    System.out.println(format("error while reading file {0}. exception: {1}", fullPath, e.getMessage()));
}

Note que o código acima faz uso de uma classe Utils para converter os tipos: Java LocalDate para Google Date, Java BigDecimal para Google Money e Java LocalDateTime para Google Timestamp. Você pode incluir minha classe Utils no seu projeto copiando o código a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package dev.danielpadua.protobufexamplejava;

import com.google.protobuf.Timestamp;
import com.google.type.Date;
import com.google.type.Money;

import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

public class Utils {
    protected static Timestamp toGoogleTimestampUTC(final LocalDateTime localDateTime) {
        return Timestamp.newBuilder()
                .setSeconds(localDateTime.toEpochSecond(ZoneOffset.UTC))
                .setNanos(localDateTime.getNano())
                .build();
    }

    protected static LocalDateTime fromGoogleTimestampUTC(final Timestamp googleTimestamp) {
        return Instant.ofEpochSecond(googleTimestamp.getSeconds(), googleTimestamp.getNanos())
                .atOffset(ZoneOffset.UTC)
                .toLocalDateTime();
    }

    protected static Date toGoogleDate(final LocalDate localDate) {
        return Date.newBuilder()
                .setYear(localDate.getYear())
                .setMonth(localDate.getMonth().getValue())
                .setDay(localDate.getDayOfMonth())
                .build();
    }

    protected static LocalDate fromGoogleDate(final Date googleDate) {
        return LocalDate.of(googleDate.getYear(), googleDate.getMonth(), googleDate.getDay());
    }

    protected static Money toGoogleMoney(final BigDecimal decimal) {
        return Money.newBuilder()
                .setCurrencyCode("USD")
                .setUnits(decimal.longValue())
                .setNanos(decimal.remainder(BigDecimal.ONE).movePointRight(decimal.scale()).intValue())
                .build();
    }

    protected static BigDecimal fromGoogleMoney(final Money googleMoney) {
        return new BigDecimal(googleMoney.getUnits())
                .add(new BigDecimal(googleMoney.getNanos(), new MathContext(9)));
    }
}

Caso você tenha o problema a seguir no IntelliJ, você pode simplesmente utilizar a sugestão sugerida: “Set language level to 8 — Lambdas, type annotations etc”:

Language level no IntelliJ IDEALanguage level no IntelliJ IDEA

Agora podemos executar a aplicação. Clique no botão “Add Configuration” localizado no canto superior direito, ao lado do botão de build. Clique no botão com o sinal de soma e selecione “Application”:

Criando uma run configurationCriando uma run configuration

Então preencha o nome da configuração e selecione a classe principal a ser executada:

Últimos campos para preencher antes de executarÚltimos campos para preencher antes de executar

Depois, clique no botão executar ou debugar caso você queira:

Vamos rodar!Vamos rodar!

Um resultado de sucesso deve exibir a mensagem a seguir:

Arquivo protobuf criado com sucessoArquivo protobuf criado com sucesso

Agora verifique o diretório que você definiu como saída para o arquivo:

Sucesso!Sucesso!

Nós implementamos com sucesso uma simples aplicação console Java que cria uma mensagem protobuf utilizando uma estrutura definida em um arquivo .proto, através de um processo de compilação automatizado e integrado ao build maven. Agora para provar que é útil para comunicação entre projetos de diferentes linguagens, iremos criar uma aplicação console C# para ler este arquivo e mostrar os dados no console.

Aplicação Console C#

Para o examplo do C# irei utilizar: .NET 5, Visual Studio Code e o pacote Grpc.AspNetCore que contém um compilador de protobuf é embutido ao dotnet build.

Abra o Visual Studio Code, abra o terminal integrado, navegue até o diretório onde você deseja guardar o projeto e execute os comandos a seguir, linha a linha:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Cria um diretório para a solução
mkdir protobuf-example-csharp
# Navega até a solução
cd protobuf-example-csharp
# Cria o projeto principal
dotnet new console -o src/DanielPadua.ProtobufExampleCsharp
# Cria o projeto de testes
dotnet new xunit -o tests/DanielPadua.ProtobufExampleCsharp.Tests
# Cria a solution na raíz
dotnet new sln -n DanielPadua.ProtobufExampleCsharp
# Adiciona os projetos na solution
dotnet sln add src/DanielPadua.ProtobufExampleCsharp/DanielPadua.ProtobufExampleCsharp.csproj
dotnet sln add tests/DanielPadua.ProtobufExampleCsharp.Tests/DanielPadua.ProtobufExampleCsharp.Tests.csproj

Feito, projeto criado, agora vamos abri-lo no Visual Studio Code:

Selecione a pasta raíz (a mesma que você está no terminal)Selecione a pasta raíz (a mesma que você está no terminal)

A estrutura do projeto deve ser como esta:

Adicione os assets para fazer build e debugAdicione os assets para fazer build e debug

Clique no “Yes” na mensagem no canto inferior direito, para que o Omnisharp crie a pasta .vscode com os assets para executar/debugar o projeto. Selecione “DanielPadua.ProtobufExampleCsharp”:

Escolha o projeto para gerar os assetsEscolha o projeto para gerar os assets

Abra o terminal novamente no nível src/DanielPadua.ProtobufExampleCsharp e execute:

1
dotnet add package Grpc.AspNetCore

Agora vamos incluir os contratos .proto. Crie um diretório abaixo da raíz do projeto principal e nomeie-o como: “Protos” e crie os arquivos .proto listados na seção acima:

Arquivos protobuf criadosArquivos protobuf criados

Adicione os arquivos .proto no .csproj para que o protoc compile quando o dotnet build rodar:

Adicionando o caminho dos arquivos protobuf para o plugin compilarAdicionando o caminho dos arquivos protobuf para o plugin compilar

Agora vamos compilar o projeto para gerar as classes C# a partir dos contratos .proto. No terminal, execute:

1
dotnet build

A mensagem de um build de sucesso deve ser algo como isto:

Dotnet build okDotnet build ok

Próximo passo é substituir o “Hello World” no método Main pelas linhas a seguir:

1
2
3
4
5
    var fullpath = @"/Users/danielpadua/protobuf/protobuf-customer";
    using var inputStream = File.OpenRead(fullpath);
    Customer c = Customer.Parser.ParseFrom(inputStream);
    Console.WriteLine("Customer from protobuf-example-java:");
    Console.WriteLine(c.ToString());

Agora você deve tentar importar o namespace “Contracts”, pressione ctrl+. (windows, linux) or cmd+. (macOs) para abrir o autocomplete, e então a opção de importar irá aparecer:

Intellisense e a geração de código protobuf funcionandoIntellisense e a geração de código protobuf funcionando

Caso a opção de importar não aparece, tente reiniciar o Omnisharp usando: ctrl+shift+p (windows, linux) or cmd+shift+p (macOs), digite: restart omnisharpe aperte o enter:

Espere por um momento antes de tentar importar novamenteEspere por um momento antes de tentar importar novamente

Infelizmente a integração do Omnisharp com o compilador de protobuf não é perfeita, mas funciona.

Garanta que você está lendo o mesmo diretório e arquivo que o projeto Java gerou, e então rode sua aplicação console C# apertando o botão de executar do Visual Studio Code (caso você tenha configurado os assets de execução/debug corretamente) ou simplesmente rodando a linha de comando: dotnet run estando no diretório raíz do projeto principal:

Executando do terminalExecutando do terminal

Executando do Visual Studio CodeExecutando do Visual Studio Code

E, nós conseguimos. Recebemos e interpretamos uma mensagem serializada em protobuf gerada por uma aplicação Java, dentro de uma aplicação C#.

Conclusão

Protobuf foi feito para ser mais rápido, mais leve e consequentemente mais performático do que outros protocolos. Então, te convido a fazer uma breve pesquisa como: “protobuf vs json performance” ou alguma outra, para ver benchmarks e outros cases de sucesso mundo afora.

Neste artigo espero ter dado um bom mergulho para aqueles que, como eu há um tempo atrás, não haviam nem ouvido falar sobre protobuf e sempre utilizaram JSON ou XML para serialização.

Até a próxima!

Referencias

Esse post está licenciado sob CC BY 4.0 pelo autor.