Protobuf — Uma alternativa ao JSON e XML

Evandro F. Souza
Training Center
Published in
8 min readMay 31, 2018

A arquitetura SOA( Service-Oriented Architecture) possui uma bem merecida reputação entre os desenvolvedores. Talvez uma das muitas razões dessa reputação seja por ser uma abordagem sólida que alivia o crescimento doloroso e extrai muitas preocupações de quando desenvolvemos grandes aplicações. Normalmente, a comunicação destes serviços ocorre através de HTTP usando o formato JSON. Embora o JSON possua muitas vantagens óbvias como formato de intercâmbio de dados — ele é legível para humanos, bem compreendido e normalmente funciona bem — ele também tem seus problemas.

Para os casos no qual os dados não são consumidos diretamente por Javascript em navegadores, como por exemplo em serviços internos, pode ser que formatos estruturados — como o Protobuf apresentado neste post — seja uma melhor opção para codificar dados.

O objetivo deste post é entender o que é um Protocol buffers, quando é bom utilizar — ou não — e mostrar um breve exemplo de implementação usando Python.

O que é Protobuf?

Protobuf (sigla de Protocol buffers) é um mecanismo criado e usado pelo Google para serializar dados estruturados. É independente de linguagem ou plataforma.

Figura 1 — Implementando com Protobuf

Falando resumidamente, ele funciona da seguinte maneira:

  • Primeiro é definido como deseja que os dados sejam estruturados — em um arquivo de extensão .proto.
  • Em seguida, esta definição é compilada e o resultado é um código-fonte automaticamente gerado na linguagem desejada — no momento que escrevo este post, as linguagens compatíveis são C++, C#, Go, Java e Python.
  • Finalmente, código-fonte gerado é utilizado para gravar e ler os dados estruturados.
  • Sempre que houver mudança na estrutura dos dados, o ciclo se repetirá.

Agora, utilizando Python, vamos ver como funciona este processo na prática.

Como usar?

Primeiramente será necessário configurar o seu ambiente, para isto será necessário instalar o Protobuf, para então ter acesso ao protoc , que é compilador utilizado para gerar as classes stub.

Instalando o Protobuf

  • Acesse este link e baixe o pacote com o nome protoc.
  • Extraia o conteúdo do pacote em qualquer local e siga as instruções do README do pacote.

Dica para usuários linux: Rode os comandos abaixo, que servem para mover os arquivos para as pastas corretas e configurar permissão.

sudo mv protoc3/bin/* /usr/local/bin/sudo mv protoc3/include/* /usr/local/include/sudo chown [user] /usr/local/bin/protoc
sudo chown -R [user] /usr/local/include/google
  • Com tudo movido e configurado, rode o comando protoc --version e verifique se está tudo funcionando.

Após o ambiente preparado, vamos começar pelo inicio, como descrito acima, criando um arquivo .proto .

O arquivo .proto

Este arquivo possui uma sintaxe própria para estruturar os dados. Somente para exemplo, este tutorial irá salvar uma lista de filmes. Abaixo é possível visualizar o arquivo movies.proto .

Vamos analisar brevemente cada linha deste arquivo.

syntax="proto2";

No momento que escrevo este post, existem duas versões de sintaxes para Protobuf — proto2 e proto3. Conforme o nome explica, as diferença entre ambos é somente de sintaxes. Neste tutorial eu escolhi o proto2 pois eu achei melhor para os novatos — o meu caso :) — , pelo simples fato de possuir mais conteúdo para estudo. Caso esteja interessado em aprender um pouco mais da diferença entre as sintaxes, este é um ótimo artigo.

package movie;

O package é opcional, ele serve para evitar confrontos de nome entre protocolos de mensagens. A maneira que esta keyword afeta o código gerado dependerá da linguagem escolhida. Por exemplo, se for Java o valor será utilizado como referencia para criar um Java package. No caso deste exemplo, o valor do package será ignorado, isso pois utilizaremos Python, e os módulos do Python não organizados de acordo com sua localização no sistema de arquivos.

message Movie {
enum Role {
CHARACTER = 0;
SCREENPLAY = 1;
DIRECTOR = 2;
}
message Person {
required string name = 1;
required Role role = 2;
}
required string name = 1;
required double releaseDate = 2;
optional string overview = 3;
repeated Person featuredCrew = 4;
}
message MovieList {
repeated Movie movies = 1;
}

O message é utilizado para definir um novo tipo de mensagem. No nosso exemplo definimos três novos tipos: Movie, Person e MovieList. Cada mensagem deve possuir um ou mais campos e estes campos podem ser de tipos escalares(int, double, float, string e etc) ou tipo pré-definidos(É o caso do campo featureCrewe role). Quando falamos de tipos pré-definidos, podemos falar também sobre o enum, que é um conceito já amplamente explorado em diversas linguagens.

Conforme é possível notar, cada campo deve ser enumerado e este número é único por mensagem. Estes números servem para identificar o campo e não devem ser modificados uma vez que a mensagem já começou a ser utilizada.

Por fim, possuímos as keywords que especificam as regras de cada campo, são eles:

  • required : Este campo é obrigatório e não pode ser repetido mais que uma vez.
  • optional : Este campo é opcional e não pode ser repetido mais que uma vez.
  • repeated : Este campo pode ser repetido quantas vezes quiser( incluindo zero).

Gerando a classe stub para Python

O arquivo .proto descrito acima foi salvo em um diretório chamado proto_files agora vamos rodar o protoc e gerar a classe stub :

SRC_DIR=`pwd`
DST_DIR=`pwd`
protoc -I=$SRC_DIR --python_out=$DST_DIR/classes $SRC_DIR/proto_files/movies.proto

O argumento -I é o diretório no qual o arquivo .proto está localizado.

O argumento --python_out deve contar uma pasta existente no qual a classe será gerada. Existe um argumento out para cada linguagem disponível. Rode o comando protoc --help para mais detalhes.

O último argumento é o arquivo .proto no qual será interpretado pelo compilador. No caso do exemplo, salvamos o nosso arquivo com o nome movies.proto.

Após rodado o comando, será gerado um arquivo chamado movies_pb2.py dentro do diretório classes. Para não deixar o post muito extenso, eu não vou colocar a classe gerada aqui — pois ela realmente é grande — , mas caso queira visualizar pode acessar este link.

Com a classe gerada, como devemos utilizar ela? quais os métodos e atributos que estão disponíveis? É isso que vamos ver agora…

Utilizando a classe stub: Lendo e escrevendo os dados

Esta classe nos permite codificar(serializar) a mensagem. Seria o processo de preparar os dados para serem transportados. Abaixo é possivel visualizar um trecho de código que mostra isso:

O código é bem simples, temos um dict com os dados do filme. Para os campos que são do tipo repeated no arquivo .proto, precisamos chamar o método .add(). No nosso exemplo seriam os campos movies da mensagem MovieList e o featureCrew da mensagem Movie. E para serializar os dados, utilizamos o método SerializeToString(). A mensagem é no formato binário, dê uma olhada como é o conteúdo dela.

\n\xb7\x01\n\x08Deadpool\x11\xfc\xc8C_\xcc\xc3\xd6A\x1alWisecracking mercenary Deadpool battles the evil and powerful Cable and other bad guys to save a boy\'s life."\x11\n\rRyan Reynolds\x10\x00"\x10\n\x0cDavid Leitch\x10\x02"\x0f\n\x0bRhett Reese\x10\x01

No exemplo eu utilizei um dict propositalmente, pois ele lembra um formato JSON e serve para comparar — mesmo que no olhômetro — o quão leve é este formato de mensagem comparando com JSON( mesmo tirando todos os espaços e etc). Aliás, se quiser dar uma olhada em um benchmark, tem artigo é interessante

Bom, agora vamos entender como decodificar(deserializar) a mensagem.

O processo de decodificação é tão simples que nem temos muito o que analisar. É chamado o método ParseFromString e ele preenche a classe stub com os dados da string binária. Gostaria também de chamar atenção para o campo releaseDate, note que ele é do tipo double, isso ocorre pois o Protobuf não possui tipo para data, nestes casos o aconselhado é transportar o valor em formatotimestamp.

Agora que vimos como utilizar, vamos entender quais a vantagens e quando utilizar o Protobuf.

Quando utilizar?

É importante observar que, embora as mensagens JSON e Protobuf possam ser usadas de forma intercambiável, essas tecnologias foram projetadas com objetivos diferentes. JSON, que significa JavaScript Object Notation, é simplesmente um formato de mensagem que surgiu em subconjunto da linguagem de programação Javascript. As mensagens JSON são trocadas em formato de texto e, hoje em dia, são complemente independentes e suportadas por praticamente todas as linguagens de programação.

Protobuf, por outro lado, é mais que um formato de mensagem, é também um conjunto de regras e ferramentas para definir e trocar essas mensagens. Além disso, o Probuf possui mais tipos de dados que o JSON, como enumerados e métodos, e também é muito usado em RPCs( Remote Procedure Calls).

Abaixo cito cinco boas razões para utilizar Protobuf:

  1. Schemas. Existe toda uma preocupação em normalizar os dados e gravar eles corretamente nas nossas bases de dados. Contudo, por quê não existe tal cuidado quando transportamos nossos dados usando serviços de mensagem? O Protobuf resolve esta limitação permitindo que sejam definidos esquemas de dados que definem e garantem a integridade dos dados transportados entre diferentes serviços.
  2. Compatibilidade de versões anteriores. A maneira que os campos são definidos no schema( de maneira enumerada), evita a necessidade de verificação de compatibilidade entre versões, que é uma das principais razões do Protobuf ter sido projetado.
  3. Menos código duplicado. Geralmente os endpoints HTTP que se comunicam via JSON dependem de bibliotecas de terceiros ou códigos implementados especificamente para codificar e decodificar o JSON. Além do mais, muitas vezes as classes referentes ao JSON acabam expondo regras de negócio e dificultando ainda mais o trabalho manual de parser. O Protobuf resolve este problema ao gerar classes stub a partir dos schemas definidos. Deste modo,conforme os dados forem evoluindo e mudando, será somente necessário modificar o schema e regerar as classes stub.
  4. Validações e extensibilidade. As palavras chaves required , optional e repetead são extremamente poderosos. Elas permitem que seja codificado, a nível de schema, o formato dos dados estruturados e os detalhes de implementação sobre como as classes devem funcionar. Por exemplo, a biblioteca de Protobuf do Python irá mostrar uma exceção na tentativa de instanciar um objeto que possua um campo required vazio. É possível também alterar um campo de required para optional — ou vice-versa — simplesmente trocando a enumeração do campo. Conforme o projeto vai crescendo e surge a necessidade de modificar a estrutura de dados, este tipo de flexibilidade realmente se mostra importante.
  5. Interoperabilidade entre linguagens. Como os Protocol Buffers são implementados em várias linguagens, eles tornam a interoperabilidade entre aplicativos poliglotas em sua arquitetura muito mais simples. Independente da linguagem utilizada no serviço que for adicionado na arquitetura, utilizando o Protobuf somente é preciso ter em mãos o arquivo .proto e gerar a classe stub da linguagem do novo serviço.

E quando JSON é melhor?

Ainda há algumas vezes que o JSON é melhor que o Protobuf, como por exemplo nas situações em que:

  • É necessário que os dados sejam legíveis para humanos.
  • Os dados do serviço são consumidos diretamente por um web browser.
  • Sua aplicação server side é escrita em JavaScript.
  • Você não está preparado para vincular o modelo de dados a um esquema, por exemplo, talvez seus dados são dinâmicos.
  • A sua aplicação não consome tanta banda assim.

E provavelmente muitas outras situações. No final, como sempre, o que é importante é manter os tradeoffs em mente, afinal, não existe bala de prata.

E agora?

Protocol Buffers oferecem várias vantagens convincentes sobre o JSON, quando o assunto é transportar dados entre serviços internos. Contudo há casos que o JSON ainda pode ser preferível, como nos casos que os serviços são consumidos diretamente por um web browser. Neste post citamos brevemente, mas o Protobuf ganha ainda mais notoriedade quando utilizado em conjunto com o gRPC, que é um client application, criado pelo Google também, que permite que métodos sejam chamados entre aplicações diferentes( e linguagens diferentes). Mas este é um assunto para outro post :).

Se quiser trocar uma ideia ou entrar em contato comigo, pode me achar no Twitter (@e_ferreirasouza) , Linkedin ou deixar um comentário aqui.

Grande abraço e até a próxima!

--

--