PTC29008: Projeto 2: Codificação de Mensagens
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.
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> </td><td align="right"> - </td><td> </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> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>
|
Esses exemplos apresentam basicamente dois tipos de codificação:
- 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).
- 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:
- ASN.1 (Abstract Syntax Notation One): ASN.1 é uma linguagem de especificação de sintaxe abstrata definida pelo ITU-T. A sintaxe concreta está dissociada dessa linguagem, podendo ser realizada de diferentes formas, tais como BER (Binary Encoding Rules), PER (Packed Encoding Rules), XER (XML Encoding Rules) e outras. A notação ASN.1 não impõe uma escolha sobre qual codificação utilizar, ficando assim de livre escolha pelo desenvolvedor.
- ABNF (Augmented Backus-Naur Form): ABNF é uma notação definida pelo IETF para expressar tanto a sintaxe abstrata quanto sua codificação textual, sendo portanto voltada para estruturas de dados representadas em modo de texto. Assim, ABNF impõe também a codificação a ser usada nas mensagens.
... 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.
- 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
- Site oficial sobre ASN.1
- Introdução a ASN.1
- Um exemplo sobre ASN.1
- ASN.1: Communication between heterogeneous systems (livro gratuito)
- ASN.1 Complete (livro gratuito)
- Compilador ASN.1 ... e esse mesmo projeto no github
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 e outras (Python, Perl, ...). 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++ e Java (e mesmo Python). Em particular, existe um compilador ASN.1 gratuito capaz de traduzir as especificações para linguagem C, além de prover uma bilbioteca 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:
De forma alternativa, existem compiladores ASN.1 para outras linguagens de programação, como estes:
Atividade
- Seja um tipo de mensagem contendo estes dados: ... especifique-a com ASN.1.
id: número inteiro nome: string composta por caracteres imprimíveis valores: lista de números inteiros timestamp: data e horário
- Compile sua especificação usando o Compilador ASN.1
- 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.
- Modifique os dois programas do item anterior para que eles se comuniquem por meio de sockets (UDP ou TCP). Dica: use ostringstream para codificar mensagens, e istringstream para decodificá-las. Note que o codificador grava em um objeto ostream, e o decodificador recupera mensagens a partir de um objeto istream.
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.
Revisão sobre sockets TCP
- Um tutorial resumido sobre sockets
- Outro tutorial mais completo (cópia no servidor de Tele) (versão original)
O uso de sockets TCP usando a API de sockets pode ser resumida nos seguintes passos:
Cliente:
- Criar um socket (socket)
- Vinculá-lo a um endereço e port (bind)
- Conectá-lo a um servidor (connect)
- Enviar e receber dados (send, recv)
- Encerrá-lo (close)
Servidor:
- Criar um socket (socket)
- Vinculá-lo a um endereço e port (bind)
- Ativá-lo para que possa receber conexões (listen)
- Receber conexão (accept) ... isso cria um novo socket automaticamente, através do qual se pode:
- Encerrar o socket principal (close) ... isso impede a recepção de novas conexões, mas não encerra as conexões já aceitas e em andamento.
A API de sockets em sua forma mais elementar é fornecida para linguagem C. Outras linguagens oferecem versões de mais alto nível dessa API, tais como:
- Java: Java Sockets
- C++: Boost Asio, Socket++ (API simplificada)
- Python: socket library, asyncio, socketserver, Twisted,
Exemplos existem nos tutoriais indicados nos links no início desta seção. Porém para facilitar o uso de sockets foram criadas algumas classes C++:
As classes são TCPClientSocket e TCPServerSocket. A descrição de suas interfaces está em TCPBaseSocket.h (ver comentários). Objetos dessas classes funcionam como sockets, porém com simplificações (não são necessários todos os passos da API de sockets do sistema operacional). Existe um exemplo de utilização nos arquivos client.cpp e server.cpp:
Exemplo de cliente |
---|
#include <cstdlib>
#include <iostream>
#include "TCPBaseSocket.h"
using namespace std;
/*
*
*/
int main(int argc, char** argv) {
TCPClientSocket client;
// conecta no servidor da wiki (pode-se usar o nome DNS ou
// o endereço IP do servidor)
client.connect("wiki.sj.ifsc.edu.br", 80);
// faz uma requisição HTTP
client.send("GET / HTTP/1.1\n\n");
// recebe e mostra a resposta do servidor
string resp = client.recv(10240);
cout << resp << endl;
return 0;
}
|
Exemplo de servidor |
---|
#include <cstdlib>
#include <iostream>
#include "TCPBaseSocket.h"
using namespace std;
/*
*
*/
int main(int argc, char** argv) {
// cria um socket servidor que recebe conexões no port 8000
TCPServerSocket server(8000);
// fica eternamente recebendo novas conexões
// e dados de conexões existentes
while (true) {
try {
// aguarda uma nova conexão ou dados em
// uma conexão existente
Connection & sock = server.wait(0);
string addr;
unsigned short port;
// obtém o IP e port do socket da outra ponta da
// conexão
sock.get_peer(addr, port);
// se for nova conexão, apenas mostra IP e port da outra ponta
if (sock.isNew()) {
cout << "Nova conexão: " << addr << ':' << port << endl;
} else {
// tenta receber até 1024 bytes no socket retornado
// por "wait"
string data = sock.recv(1024);
// conseguiu ler algo desse socket ...
if (data.size()) {
// ...mostra-os na tela e envia-os de volta
// para a outra ponta da conexão
cout << "recebeu de " << addr << ':' << port;
cout << ": " << data << endl;
data = "recebido: " + data;
sock.send(data);
}
}
} catch (TCPServerSocket::DisconnectedException e) {
// esta exceção informa que uma conexão foi encerrada
// o socket correspondente foi invalidado automaticamente
cout << e.what() << ": " << e.get_addr() << ':';
cout << e.get_port()<< endl;
}
}
return 0;
}
|
Diagrama de classe da biblioteca Socket++ |
---|
O diagrama de classes a seguir mostra as classes envolvidas: |
... mas quem quiser tentar (ou conhecer) algo mais completo, pode experimentar a biblioteca Boost.Asio.
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
... e de utilização:
demo.cc |
---|
#include <parser_Teste1.h>
#include <iostream>
#include <sstream>
using namespace std;
int main() {
TMensagem pkt;
pkt.set_id(1234);
pkt.set_nome("manoel");
// verifica se os valores contidos na estrutura de dados respeitam
// a especificação
pkt.check_constraints();
// mostra a estrutura de dados na tela
cout << "Estrutura de dados em memória (antes de codificação XER):" << endl;
pkt.show();
// cria o codificador
ostringstream out;
TMensagem::XerSerializer encoder(out);
// codifica a estrutura de dados
encoder.serialize(pkt);
cout << endl;
cout << out.str() << endl;
// cria o decodificador
istringstream inp(out.str());
TMensagem::XerDeserializer decoder(inp);
// tenta decodificar uma estrutura de dados
TMensagem * other = decoder.deserialize();
cout << endl;
if (other) {
cout << "Estrutura de dados obtida da decodificação XER:" << endl;
other->show();
} else cerr << "Erro: não consegui decodificar a estrutura de dados ..." << endl;
// devem-se destruir explicitamente as estruturas de dados obtidas
// do decodificador
delete other;
}
|
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
... exemplo de utilização:
TMensagem pkt;
// define o valor do campo "id"
pkt.set_id(8);
// Acessa o campo choice, que será referenciado
// pela variável "msg" (note o operador &)
TMensagem::Choice_msg & msg = pkt.get_msg();
// Dentro de "msg" acessa-se "inicio": isso faz
// com que o campo choice contenha um valor do tipo
// "Login" (não pode mais ser mudado).
TLogin login = msg1.get_inicio();
// define os valores dos campos de "inicio"
login.set_usuario("aluno");
login.set_senha("blabla...");