PTC29008: Projeto 2: Codificação de Mensagens

De MediaWiki do Campus São José
Revisão de 14h27min de 17 de setembro de 2020 por Msobral (discussão | contribs) (→‎Atividade)
Ir para navegação Ir para pesquisar

Próxima aula

Objetivos:

  • representar mensagens com sintaxe abstrata (ex: ABNF, ASN.1, ou outras)
  • codificar mensagens com sintaxe concreta

Cada mensagem que compõe o vocabulário de um protocolo carrega informações que dizem respeito a seu conteúdo (ou payload) e meta-dados do protocolo. Por exemplo, mensagens do protocolo TCP (chamadas de segmentos), como mostrado na figura a seguir, contêm diversas informações de controle que formam um cabeçalho, e um corpo de mensagem que contém os dados transportados pela mensagem.

PTC-Tcp-segment.gif


O TCP é um protocolo de transporte, e usa uma codificação binária em suas mensagens com representação big endian (todos protocolos de comunicação da Internet usam essa representação - ver RFC 1700). Já o protocolo de aplicação SMTP (Simple Mail Transfer Protocol) usa uma codificação textual em suas mensagens. O exemplo a seguir mostra a conversação entre um cliente (prefixo C:) e um servidor SMTP (prefixo S:).

S: 220 smtp.example.com ESMTP Postfix

C: HELO relay.example.org 
S: 250 Hello relay.example.org, I am glad to meet you

C: MAIL FROM:<bob@example.org>
S: 250 Ok

C: RCPT TO:<alice@example.com>
S: 250 Ok

C: RCPT TO:<theboss@example.com>
S: 250 Ok

C: DATA
S: 354 End data with <CR><LF>.<CR><LF>

C: From: "Bob Example" <bob@example.org>
C: To: "Alice Example" <alice@example.com>
C: Cc: theboss@example.com
C: Date: Tue, 15 January 2008 16:02:43 -0500
C: Subject: Test message
C: 
C: Hello Alice.
C: This is a test message with 5 header fields and 4 lines in the message body.
C: Your friend,
C: Bob
C: .
S: 250 Ok: queued as 12345

C: QUIT
S: 221 Bye


Outro protocolo de aplicação muito utilizado é o HTTP (Hypertext Transfer Protocol), que usa uma representação híbrida em suas mensagens (predominantemente textual, mas pode ter conteúdo binário).

Requisição (enviada pelo cliente) Resposta (enviada pelo servidor)
GET /~msobral/ptc/docs/ HTTP/1.1
Host: tele.sj.ifsc.edu.br
HTTP/1.1 200 OK
Date: Mon, 21 Sep 2015 17:04:04 GMT
Server: Apache
Vary: Accept-Encoding
Content-Length: 870
Content-Type: text/html;charset=UTF-8

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /~msobral/ptc/docs</title>
 </head>
 <body>
<h1>Index of /~msobral/ptc/docs</h1>
<table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a>
</th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a 
href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[DIR]"></td><td><a href="/~msobral
/ptc/">Parent Directory</a></td><td>&nbsp;</td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/layout.gif" alt="[   ]"></td><td><a 
href="state.pdf">state.pdf</a></td><td align="right">08-Sep-2015 16:47  </td><td 
align="right">130K</td><td>&nbsp;</td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>


Esses exemplos apresentam basicamente dois tipos de codificação:

  1. Textual: as informações são representadas em texto legível, o que facilita a interpretação por seres humanos porém gera mensagens mais longas. A representação em texto pode facilitar também o intercâmbio de informações entre computadores com diferentes arquiteturas e aplicações escritas com diferentes linguagens de programação. Por fim, um detalhe importante é a codificação de caractere adotada (ex: no caso do HTTP apresentado, usa-se codificação UTF-8).
  2. Binária: as informações são representadas por sequências de bits, segundo algum esquema de codificação, o que gera potencialmente mensagens mais curtas, porém de difícil legibilidade por seres humanos. A representação binária deve seguir regras estritas para o correto intercâmbio entre computadores de diferentes arquiteturas e programas escritos em diferentes linguagens de programação.


Hoje será estudado como cada tipo de representação pode ser especificado.

Sintaxe abstrata

Sintaxe abstrata é a especificação do conjunto de valores que compõem uma estrutura de dados, independente de linguagem de programação e arquitetura de hardware. Por exemplo, para representar um ativo em negociação na bolsa de valores identificaram-se como informações necessárias o nome do ativo, seu código de negociação, seu valor monetário e a data e horário desse valor. Assim pode-se especificar uma estrutura de dados que represente esse ativo, sendo ela composta de:

NOME: cadeia de caracteres com comprimento máximo de 16 caracteres
CODIGO: numero inteiro
VALOR: numero inteiro (quantidade de centavos)
DATA: string numerica com 8 caracteres (DDMMYYYY)
HORARIO: string numerica com 6 caracteres (HHMMSS)


A descrição acima é a sintaxe abstrata da estrutura de dados. Ela descreve que valores compõem a estrutura, e quais os tipos desses valores. No entanto, ela não especifica sua sintaxe concreta, que determina como essa estrutura de dados é representada, ou codificada, durante uma transmissão. Sua codificação pode ser feita de diferentes formas, sendo alguns exemplos:

  • Com uma representação textual XML
  • Com uma representação binária compacta little-endian ou big-endian, composta por números inteiros de 32 bits, caracteres ASCII ou UTF-8
  • Com uma representação binária do tipo TLV (Type-Length-Value)
  • Com uma representação textual em CSV ou JSON


Ambos conceitos são de grande importância no projeto de protocolos. A sintaxe abstrata especifica as estruturas de dados existentes nos intercâmbios realizados pelo protocolo. Essa especificação deve ser independente de arquitetura de computador ou linguagem de programação, podendo ser traduzida para uso em qualquer plataforma de hardware e/ou software, para que o protocolo possa ser usado nessas plataformas. A sintaxe concreta especifica como essas estruturas de dados devem ser codificadas para transmissão. Dessa forma, sistemas em comunicação podem estar em acordo quanto ao significado dos dados transmitidos. Dada sua importância, existem padrões para especificação de sintaxe abstrata e sintaxe concreta, sendo duas delas proeminentes:

... e algumas outras propostas por empresas ou grupos de desenvolvimento de software correndo por fora:

  • Protocol Buffers: linguagem de especificação de sintaxe abstrata criada pelo Google. Possui suporte a várias linguagens de programação, como Java, Python e C++. Há um projeto relacionado para uma biblioteca com pequeno footprint.
  • FlatBuffers: uma biblioteca para serialização eficiente de dados criada pelo Google. Possui suporte a várias linguagens de programação, apresentando menor footprint do que Protocol Buffers e menor tempo para codificação e decodificação.
  • MessagePack: define um formato de serialização binária para sintaxe concreta. Não possui uma notação para sintaxe abstrata: a serialização é feita diretamente a partir de valores representados na linguagem de programação alvo. Suporta muitas linguagens, tais como C, C++, Python e Java.

ASN.1


De acordo com a Introdução a ASN.1, Abstract Syntax Notation number One é um padrão que define um formalismo para a especificação de tipos abstratos de dados. ASN.1 não é uma linguagem de programação, e sim uma notação para especificar tipos de dados. ASN.1 tem muitos usos, alguns deles listados a seguir:


A notação fornece um certo número de tipos básicos predefinidos, tais como:

  • INTEGER: números inteiros
  • BOOLEAN: valores booleanos
  • IA5String, UniversalString, NumericString, PrintableString: cadeias de caracteres
  • BIT STRING: cadeias de bits
  • OBJECT IDENTIFIER: identificador de objeto


Ela torna possível a definição de tipos de dados, tais como:

  • SEQUENCE: estruturas de dados (struct)
  • SEQUENCE OF: listas
  • CHOICE: escolha entre valores


Uma definição em ASN.1 pode ser mapeada para uma estrutura de dados em linguagem de programação como C, C++, Java, Python, e outras. Assim, essa estrutura de dados pode ser usada em software que implementa um protocolo. A codificação e decodificação entre essa estrutura de dados e os dados transmitidos e recebidos deve ser feita por meio de algoritmos disponibilizados em bibliotecas. Com isso, os dados em transmissão podem ser representados de diferentes maneiras, como TLV, XML e outras, independente da especificação da estrutura de dados.

A conversão da especificação em ASN.1 para uma linguagem de programação específica deve ser realizada com um compilador ASN.1. Existem ferramentas como essa para diversas linguagens, como C, C++, Python e Java. Por exemplo, existe um compilador ASN.1 gratuito capaz de traduzir as especificações para linguagem C, além de prover uma biblioteca para codificação e decodificação.


O exemplo sobre a descrição de ativos pode ser usado para introduzir a linguagem ASN.1. A estrutura de dados Ativo pode ser especificada usando a declaração mostrada na coluna da esquerda. A estrutura de dados em linguagem C gerada pelo compilador ASN.1 está na coluna da direita:

ASN.1 Linguagem C
Module-Exemplo DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Ativo ::= SEQUENCE {
  nome PrintableString (SIZE(1..16)),
  codigo INTEGER,
  valor INTEGER,
  data NumericString(SIZE(8)),
  horario NumericString (SIZE(6))
}

END
/* Ativo */
typedef struct Ativo {
        PrintableString_t        nome;
        long     codigo;
        long     valor;
        NumericString_t  data;
        NumericString_t  horario;
        
        /* Context for parsing across buffer boundaries */
        asn_struct_ctx_t _asn_ctx;
} Ativo_t;

A documentação do compilador ASN.1 e uma demonstração de como escrever um programa que use uma API ASN.1 está aqui:


Existem compiladores ASN.1 para as linguagens de programação citadas, como estes:

Atividade

Usando pyAsn1 para codificar e decodificar as mensagens
  1. Crie um ambiente virtual para Python3, e em seguida ative-o:
    virtualenv -p python3 asn1
    . asn1/bin/activate
    
  2. Instale os módulos pyasn1 e asn1ate:
    pip3 install pyasn1
    pip3 install asn1ate
    
  3. Suponha esta especificação ASN.1 contida no arquivo Msg.asn1:
    Protocolo DEFINITIONS AUTOMATIC TAGS ::=
    BEGIN
    
    Mensagem ::= SEQUENCE {
      tipo INTEGER,
      id PrintableString (SIZE(1..16)),
      valor INTEGER,
    }
    
    END
  4. Compile a especificação ASN.1 do formato de mensagem (supondo que esteja no arquivo Msg.asn1):
    asn1ate Msg.asn1 > Msg.py
    
  5. O tipo de dados ASN.1 está contido em Msg.py. Use esse módulo Python para codificar ou decodificar uma mensagem ASN.1
    • Codificação: crie o arquivo codifica.py com este conteúdo:
      from pyasn1.codec.der.encoder import encode
      from Msg import Mensagem
      
      msg = Mensagem()
      msg['tipo'] = 1
      msg['valor'] =100
      msg['id'] = 'MSG1'
      
      # Codifica a mensagem
      data = encode(msg)
      
      # Mostra a mensagem codificada
      print(data)
      
      # Grava a mensagem em um arquivo
      open('msg.data','wb').write(data)
      
      ... e então execute-o:
      python3 codifica.py
      
    • Decodificação: crie o arquivo decodifica.py com este conteúdo:
      from pyasn1.codec.der.decoder import decode
      from Msg import Mensagem
      
      data = open('msg.data','rb').read()
      
      # Decodifica a mensagem
      msg, resto = decode(data, asn1Spec=Mensagem())
      
      # Mostra a mensagem
      print(msg)
      
      ... e então execute-o:
      python3 decodifica.py
      
  1. Seja um tipo de mensagem contendo estes dados:
    id: número inteiro
    nome: string composta por caracteres imprimíveis
    valores: lista de números inteiros
    timestamp: data e horário
    
    ... especifique-a com ASN.1.
  2. Compile sua especificação usando o compilador ASN.1 para Python
  3. Escreva dois programas de teste:
    • Um programa deve codificar mensagens, gravando-as em um arquivo
    • O outro programa deve decodificá-las, carregando os dados do arquivo e transformando-os em instâncias das mensagens.

Uso de ASN.1 para especificar e codificar mensagens de protocolo

Mensagens de um protocolo podem ser especificadas em ASN.1, e codificadas com alguns de seus codificadores padronizados (BER, DER, PER, XER, JER, ...). Uma mensagem nada mais é do que uma instância de algum tipo de dados especificado com ASN.1. Para transmiti-la, basta codificá-la e enviar o conteúdo resultante. Para exemplificar o uso de ASN.1 para esse propósito, a seguinte atividade deve ser realizada:

  • Transmissão e recepção de mensagens: escreva um programa transmissor e outro receptor, os quais se comunicam por uma conexão TCP. O transmissor envia uma sucessão de mensagens, as quais devem ser decodificadas e apresentadas pelo receptor. Use as mensagens do seu protocolo de aplicação.


Atividade

1. Crie dois programas: um deles gera mensagens de um protocolo hipotético, codifica-as com DER e envia-as por um socket TCP, e outro recebe mensagens codificadas de um socket TCP, decodifica-as e mostra-as na tela (apresenta primeiro o tipo de mensagem recebida seguido de seu conteúdo). Exemplo de especificação de mensagens:

Teste1 DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Mensagem ::= SEQUENCE {
  id INTEGER,
  nome PrintableString (SIZE(1..16))
}

END

2. Estenda seu programa para que as mensagens possam ser de dois tipos:

Mensagem DEFINITIONS AUTOMATIC TAGS ::=
BEGIN

Login ::= SEQUENCE {
  usuario PrintableString (SIZE(1..12)),
  senha PrintableString (SIZE(4..12))
}

Dados ::= SEQUENCE {
  seq INTEGER,
  x INTEGER,
  y INTEGER
}

Mensagem ::= SEQUENCE {
  id INTEGER,
  msg CHOICE {
    inicio Login,
    dados Dados
  }
}
 
END

ABNF


ABNF (Augmented Backus-Naur Form) é uma notação sintática definida pelo IETF para especificar mensagens de protocolos de aplicação da Internet. Ela é usada para especificar mensagens dos protocolos HTTP, SIP, SMTP, IMAP4, entre outros. ABNF possibilita especificar uma gramática para mensagens codificadas textualmente. Portanto, ela se aplica a protocolos cuja sintaxe concreta é uma representação em texto.


Um exemplo de especificação ABNF segue abaixo. Ela representa um endereço de e-mail no formato nome@algum.dominio:

email=trecho arroba 1*8(trecho ".") trecho
trecho=1*16ALPHA
arroba=%d64


Uma especificação ABNF é composta por uma ou mais regras. Cada regra possui um nome e uma declaração. No exemplo acima, há três regras: email, trecho e arroba. Cada regra é declarada da seguinte forma:

regra = elementos crlf

... sendo elementos formado por um ou mais nomes de regras ou valores terminais, e crlf corresponde à sequência \r\n (carriage return + newline). Um valor terminal corresponde a uma sequência de um ou mais caracteres (i.e. a uma string).


As sintaxes para composição de regras estão descritas de forma sucinta na própria RFC 5234.

Compilador ABNF


A geração de mensagens com base em uma gramática ABNF pode ser feita diretamente e de forma simples. Basicamente trata-se de gerar mensagens de texto compostas por valores devidamente representados. No entanto, o processo inverso é um pouco mais difícil. A identificação de mensagens recebidas, e a extração dos valores relevantes ali representados, evolve a utilização de um parser ABNF. Um parser é um programa que verifica um dado de entrada de acordo com uma gramática, decompondo esse dado de entrada em seus elementos de acordo com essa gramática.

Um parser ABNF deve ser capaz de seguir uma gramática ABNF qualquer, e usá-la para reconhecer e decompor mensagens. Usualmente se obtém um parser por meio de um gerador de parser, o qual gera um parser específico para uma dada gramática. O parser gerado é fornecido como código-fonte em alguma linguagem de programação. Existem alguns geradores de parser ABNF na Internet, tais como:

... porém eles não se mostraram apropriados para uso na disciplina (ao menos não com C++). Sendo assim, foi desenvolvido um gerador de parser próprio chamado de ABNF++. Em sua página na wiki há a documentação sobre sua utilização. Além dele existe também este parser Python3:

Atividade

  1. Experimente utilizar o gerador de parser para criar um parser para a gramática do exemplo apresentado no início da aula. Em seguida, teste-o com alguns endereços de e-mail.
  2. O reconhecimento de URI é uma tarefa mais complexa. Crie um parser a partir desta gramática, e teste-o com diferentes URI.
  3. Seu protocolo de aplicação poderia ser especificado com ABNF ? Compare a codificação das mensagens com ASN.1 e ABNF, considerando a facilidade para representação das informações e de utilização do esquema de codificação.

Protocol Buffers

Protocol Buffers é uma tecnologia criada pelo Google para especificar a sintaxe abstrata de tipos de dados e codificá-los de forma eficiente, e com isso guarda semelhanças com ASN.1. Um exemplo de especificação com Protocol Buffers é mostrada a seguir, onde se define o tipo Pessoa:

syntax = "proto2";
 
package exemplo;

message Pessoa {
  required string nome = 1;
  required int32 id = 2;
  optional string email = 3;
}

Assim como com ASN.1, deve-se compilar uma especificação para que gere código-fonte em alguma linguagem de programação. Atualmente existem compiladores para C++, Python, Java e Ruby. O código-fonte gerado contém os tipos de dados (ou classes) correspondentes aos tipos declarados na especificação, segundo uma API bem definida. Por fim, valores desses tipos de dados podem ser codificados e decodificados com um codec específico (ao contrário de ASN.1, não há opções de codecs).

Para conhecer Protocol Buffers, deve-se seguir seu tutorial de instalação e utilização:

  • Instalação: a instalação pode ser feita a partir do código-fonte, e isso é o recomendável para obter a versão mais recente do compilador e bibliotecas. Como a compilação do código-fonte é lenta, existem pacotes precompilados para Ubuntu, que são mais rápidos de instalar apesar de não serem a versão mais recente. Para instalar com pacotes do Ubuntu:
    sudo apt install protobuf-compiler libprotobuf-dev python-protobuf python3-pip
    sudo pip3 install protobuf
    
  • Introdução
  • Tutoriais para linguagens suportadas

Exemplos: C++ e Python

O exemplo usa esta especificação simplificada:

syntax = "proto2";

package exemplo;

message Ativo {
  required string nome = 1;
  required int32 id = 2;
  required int32 valor = 3;
  required int32 timestamp = 4;
}

Arquivo ex1.proto

A demonstração da compilação e uso dessa especificação para C++ está a seguir:

  1. Compile a especificação para que gere o código-fonte no diretório atual:
    protoc --cpp_out=. ex1.proto
    
    ... obtendo-se como resultado os arquivos ex1.pb.h e ex1.pb.cc
  2. Escreva um programa que demonstre o uso do tipo Ativo, incluindo sua codificação e decodificação:
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <time.h>
    #include "ex1.pb.h"
    using namespace std;
    
    int main() {
      // verifica a versão da biblioteca ProtocolBuffers
      GOOGLE_PROTOBUF_VERIFY_VERSION;
    
      // cria um objeto do tipo Ativo
      exemplo::Ativo msg;
      
      // define valores dos atributos do objeto
      msg.set_id(12345);
      msg.set_nome("PETR4");
      msg.set_timestamp(time(NULL));
      msg.set_valor(1915);
    
      // codifica o objeto, guardando o resultado numa string
      string data;
      msg.SerializeToString(&data);
    
      cout << "Mensagem codificada: " << data << endl;
    
      // cria outro objeto do tipo Ativo
      exemplo::Ativo copia;
    
      // decodifica o objeto a partir de uma string
      copia.ParseFromString(data);
    
      // Apresenta os valores dos atributos do objeto decodificado
      cout << "Mensagem decodificada:" << endl;
      cout << "Nome: " << copia.nome() << endl;
      cout << "Id: " << copia.id() << endl;
      cout << "Valor: " << copia.valor() << endl;
      cout << "Timestamp: " << copia.timestamp() << endl;
    
    }
    
  3. Compile o programa de demonstração:
    aluno@M1:~$ g++ -o ex1 ex1.cpp ex1.pb.cc -lprotobuf
    
  4. Execute o programa de demonstração:
    aluno@M1:~$ ./ex1
    Mensagem codificada: 
    PETR4�`▒� ����
    Mensagem decoficada:
    Nome: PETR4
    Id: 12345
    Valor: 1915
    Timestamp: 1526395361
    
  5. Compile a especificação para Python:
    aluno@M1:~$ protoc --python_out=. ex1.proto
    
  6. Escreva um programa de demonstração similar em Python2:
    #!/usr/bin/python3
     
    import sys,time
    import ex1_pb2
     
    msg = ex1_pb2.Ativo()
    msg.nome = 'PETR4'
    msg.id = 12345
    msg.valor = 195
    msg.timestamp = int(time.time())
     
    data = msg.SerializeToString()
     
    print('Mensagem codificada:', data)
     
    copia = ex1_pb2.Ativo()
    copia.ParseFromString(data)
     
    print('Mensagem decodificada:')
    print('Nome:', copia.nome)
    print('Id:', copia.id)
    print('Valor:', copia.valor)
    print('Timestamp:', copia.timestamp)
    
  7. Execute o programa feito em Python:
    aluno@M1:~$ python ex1.py
    Mensagem codificada: 
    PETR4�`▒� ����
    Mensagem decodificada:
    Nome: PETR4
    Id: 12345
    Valor: 195
    Timestamp: 1526395478
    

Atividade

  1. Modifique os programas de demonstração para que:
    1. O programa em C++ grave a mensagem codificada em um arquivo, e o programa Python a decodifique e mostre o objeto resultante
    2. O programa em Python grave a mensagem codificada em um arquivo, e o programa C++ a decodifique e mostre o objeto resultante
  2. Modifique a especificação para que o tipo Ativo tenha uma lista de valores com respectivos timestamp
    syntax = "proto2";
     
    package exemplo;
    
    message Valor {
      required int32 valor = 1;
      required int32 timestamp = 2;
    }
     
    message Ativo {
      required string nome = 1;
      required int32 id = 2;
      repeated Valor valores  = 3;
    }
    
  3. Modifique o exercício sobre transmissão de mensagens com sockets para usar Protocol BUffers.
  4. Estenda o exercício anterior para que seja possível enviar e receber dois tipos diferentes de mensagens (Dica: veja campos do tipo OneOf)
  5. Estenda o exercício anterior para que o transmissor envie mensagens sucessivas, e o receptor consiga separá-las. Note que Protocol Buffers não tem facilidades para resolver essa necessidade. Mas você pode resolvê-la usando as sugestões contidas no site do Protocol Buffers, ou, se possível, explorando o protocolo de transporte (ex: TCP é orientado a stream, mas UDP é orientado a datagrama).