Introdução6.1
Nas aulas anteriores, focamos principalmente na Camada Física. Dessa forma, aprendemos que, no nível mais baixo, a internet não passa de pulsos de luz, variações de voltagem ou ondas de rádio. Para a Camada Física, não existem "arquivos", "imagens" ou "mensagens de texto". Existe apenas um fluxo contínuo e caótico de bits (0 e 1).
Imagine tentar ler um livro onde todas as letras estão escritas em uma única linha infinita, sem espaços, sem parágrafos e sem pontuação. Seria impossível saber onde termina uma frase e começa outra. A rede física é exatamente assim.
O objetivo deste laboratório é construir a "inteligência" que organiza esse caos. Vamos implementar um protocolo de enlace capaz de:
- Enquadrar (Framing): Definir onde uma mensagem começa e termina usando marcadores especiais.
- Proteger (Error Detection): Usar matemática para garantir que a mensagem não foi alterada por ruído elétrico.
- Endereçar: Garantir que sabemos quem enviou e para quem é a mensagem.
Definindo o Quadro6.2
Na computação de redes, nunca enviamos dados "soltos". Nós os encapsulamos os bits que irão trafegar nas camadas físicas em uma estrutura chamada Quadro. Pense no Quadro como um Envelope de Correio.
- O papel com a carta escrita é o seu dado útil (Payload).
- O envelope contém as informações vitais para o carteiro (Cabeçalho e Rodapé).
Para estruturarmos a comunicação, adotamos o modelo visualizado na figura abaixo, dividindo o quadro em três seções estratégicas: Cabeçalho (Header), Carga Útil (Payload) e Rodapé (Trailer). Esta organização não é uma mera simplificação educativa, mas reflete o padrão industrial de protocolos reais (como Ethernet e Wi-Fi). A disposição dos campos segue uma lógica de eficiência de hardware: o Cabeçalho vem primeiro para que os roteadores identifiquem o destino e tomem decisões de encaminhamento imediatamente; o Payload carrega a mensagem isolada; e o Rodapé contendo a verificação de erro (CRC) é posicionado ao final para permitir que o transmissor calcule a integridade em tempo real enquanto os dados trafegam, anexando o resultado apenas no último instante.
Vamos definir a estrutura do nosso quadro na linguagem C. Diferente de linguagens de alto nível, em C precisamos ser precisos com o tamanho da memória. Usaremos uint8_t (da biblioteca <stdint.h>) para garantir que nossas variáveis tenham exatamente 8 bits (1 byte), independentemente do computador.
Abra seu editor e crie o arquivo main.c. Vamos começar definindo nossas "regras do jogo" (Constantes) e nossas estruturas.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h> // Importante: Garante que uint8_t tenha 8 bits exatos
// === CONSTANTES DO PROTOCOLO ===
// Usaremos valores hexadecimais padrão para marcar início/fim
#define FLAG 0x7E // 01111110 em binário (Sinal de "Início/Fim")
#define ESC 0x7D // 01111101 em binário (Sinal de "Escape")
#define MAX_SIZE 100 // Tamanho máximo da mensagem (payload)
// === ESTRUTURA DO QUADRO ===
// Esta struct representa a organização lógica dos dados na memória
typedef struct {
uint8_t origem; // ID do nó que enviou (0-255)
uint8_t destino; // ID do nó que deve receber (0-255)
uint8_t tamanho; // Quantos bytes de dados úteis temos?
uint8_t dados[MAX_SIZE]; // O buffer da mensagem
uint8_t crc; // Byte de verificação de erro
} Quadro;
// Protótipos das funções que implementaremos
uint8_t calcula_crc8(uint8_t *buffer, int len);
int realizar_stuffing(uint8_t *entrada, int tam_entrada, uint8_t *saida);
int remover_stuffing(uint8_t *entrada, int tam_entrada, uint8_t *saida);
Por que usamos uint8_t e não char ou int? Em protocolos de rede, cada bit conta. Um int pode ter 16, 32 ou 64 bits dependendo do processador. O uint8_t nos dá controle absoluto sobre o fluxo de bytes.
Cálculo de integridade (CRC-8)6.3
A Camada Física é um ambiente hostil. Motores elétricos, cabos dobrados ou interferência de rádio podem causar um Bit Flip (um bit 0 virar 1). Se enviarmos "Pagar 100" e um bit mudar, pode virar "Pagar 900". Para evitar catástrofes, usamos o CRC (Cyclic Redundancy Check).
Imagine que todos os bits da sua mensagem formam um número gigante. Nós dividimos esse número por um divisor pré-definido (Polinômio). O que nos interessa não é o resultado da divisão, mas o RESTO.
- O transmissor calcula o resto e o envia junto (no campo CRC).
- O receptor faz a mesma conta. Se o resto dele bater com o resto enviado, a mensagem está íntegra.
Vamos implementar o algoritmo bit-a-bit. Não se preocupe em decorar a matemática dos polinômios agora, foque na lógica do loop: "Para cada bit, se for 1, aplicamos o XOR".
// Implementação do CRC-8 (Polinômio 0x07)
// Retorna o "checksum" de um buffer de dados
uint8_t calcula_crc8(uint8_t *buffer, int len) {
uint8_t crc = 0x00; // Começamos zerado
uint8_t polinomio = 0x07; // Valor matemático padrão para misturar bits
// Passamos por cada BYTE da mensagem
for (int i = 0; i < len; i++) {
crc ^= buffer[i]; // XOR inicial com o dado atual
// Agora processamos cada BIT desse byte
for (int j = 0; j < 8; j++) {
// Se o bit mais à esquerda for 1 (máscara 0x80 = 10000000)
if (crc & 0x80) {
// Desloca para esquerda e aplica XOR com polinômio
crc = (crc << 1) ^ polinomio;
} else {
// Se for 0, apenas desloca
crc = (crc << 1);
}
}
}
return crc;
}
Enquadramento e Byte Stuffing6.4
Esta é a parte mais importante do laboratório. Precisamos enviar nossos dados pela "física". Decidimos que todo quadro começa e termina com o byte FLAG (0x7E).
Entretanto, isso nos causa um problema, e se o usuário quiser enviar uma foto ou um texto que, por acaso, contenha o byte 0x7E no meio? Dessa forma o receptor vai apenas achar que o quadro acabou antes da hora.
A Solução (Byte Stuffing): Usamos um caractere de escape (ESC = 0x7D). A regra é simples:
- Se aparecer uma
FLAGnos dados -> Inserimos[ESC] [FLAG]. - Se aparecer um
ESCnos dados -> Inserimos[ESC] [ESC].
Uma visualização disso pode ser vista na tabela abaixo:
| Dado Original | Transmitido na Rede |
|---|---|
[ A ] [ B ] | [FLAG] [ A ] [ B ] [FLAG] |
[ A ] [FLAG] [ B ] | [FLAG] [ A ] [ESC] [FLAG] [ B ] [FLAG] |
[ A ] [ESC] [ B ] | [FLAG] [ A ] [ESC] [ESC] [ B ] [FLAG] |
Portanto, adicione esta função ao seu código para realizar a tarefa. Ela pega os dados brutos e cria um "buffer seguro" para transmissão.
// Recebe dados brutos -> Retorna dados codificados com Flags e Escapes
// Retorna o tamanho do buffer de saída
int realizar_stuffing(uint8_t *entrada, int tam_entrada, uint8_t *saida) {
int j = 0; // Cursor do buffer de saída
// 1. Abre o Quadro
saida[j++] = FLAG;
// 2. Processa o recheio (Payload + Cabeçalhos)
for (int i = 0; i < tam_entrada; i++) {
uint8_t byte_atual = entrada[i];
// Se for um caractere especial, precisamos escapar!
if (byte_atual == FLAG || byte_atual == ESC) {
saida[j++] = ESC; // Avisa: "O próximo é dado, não comando"
saida[j++] = byte_atual; // Copia o dado
} else {
saida[j++] = byte_atual; // Copia normal
}
}
// 3. Fecha o Quadro
saida[j++] = FLAG;
return j; // Retorna o tamanho total para enviarmos na rede
}
Em contrapartida, o receptor deve ser capaz de remover essa proteção para ler a mensagem original. Portanto, adicione a função abaixo:
// Recebe dados da rede -> Remove Escapes e Flags -> Retorna dados limpos
int remover_stuffing(uint8_t *entrada, int tam_entrada, uint8_t *saida) {
int j = 0; // Cursor de saída (dados limpos)
int i = 0; // Cursor de entrada (rede)
// Pula a FLAG inicial se houver
if (tam_entrada > 0 && entrada[0] == FLAG) i++;
while (i < tam_entrada) {
uint8_t byte_atual = entrada[i];
// Se achou FLAG de fim, terminamos
if (byte_atual == FLAG && i == tam_entrada - 1) break;
// LÓGICA DO DESTUFFING
if (byte_atual == ESC) {
i++; // Pula o ESC
// E grava o próximo byte obrigatoriamente (mesmo que seja uma FLAG)
if (i < tam_entrada) {
saida[j++] = entrada[i];
}
} else {
// Byte comum
saida[j++] = byte_atual;
}
i++;
}
return j; // Tamanho real da mensagem recuperada
}
Simulação6.5
Agora vamos juntar todas as peças num fluxo lógico de execução. Nosso main vai simular a vida de um pacote: Nascer (Usuário) -> Ser Protegido (Enlace) -> Viajar (Rede) -> Ser Recebido (Destino).
int main() {
char mensagem_usuario[MAX_SIZE];
int id_destino;
// =============================================================
// 1. CAMADA DE APLICAÇÃO (Input do Usuário)
// =============================================================
printf("--- SIMULADOR DE ENLACE ---\n");
printf("Digite sua mensagem: ");
fgets(mensagem_usuario, MAX_SIZE, stdin);
mensagem_usuario[strcspn(mensagem_usuario, "\n")] = 0; // Remove \n
printf("Digite o ID do Destino (Ex: 2): ");
scanf("%d", &id_destino);
// =============================================================
// 2. ENCAPSULAMENTO (Montando o Quadro)
// =============================================================
Quadro q_envio;
q_envio.origem = 1; // Vamos fingir que somos o nó 1
q_envio.destino = (uint8_t)id_destino;
q_envio.tamanho = strlen(mensagem_usuario);
memcpy(q_envio.dados, mensagem_usuario, q_envio.tamanho);
// Passo crucial: Calcular o CRC sobre os dados antes de enviar
q_envio.crc = calcula_crc8(q_envio.dados, q_envio.tamanho);
printf("\n[TRANSMISSOR] Quadro montado.\n");
printf(" > Payload: %s\n", q_envio.dados);
printf(" > CRC Calculado: 0x%02X\n", q_envio.crc);
// =============================================================
// 3. SERIALIZAÇÃO E STUFFING (Preparando para o fio)
// =============================================================
// Primeiro, transformamos a struct em um array de bytes bruto
uint8_t buffer_bruto[MAX_SIZE + 10];
int k = 0;
buffer_bruto[k++] = q_envio.origem;
buffer_bruto[k++] = q_envio.destino;
buffer_bruto[k++] = q_envio.tamanho;
for(int i=0; i<q_envio.tamanho; i++) buffer_bruto[k++] = q_envio.dados[i];
buffer_bruto[k++] = q_envio.crc;
// Agora aplicamos o Stuffing
uint8_t buffer_rede[MAX_SIZE * 2]; // Dobro do tamanho por segurança
int tam_rede = realizar_stuffing(buffer_bruto, k, buffer_rede);
printf("\n[REDE FÍSICA] Enviando bits pelo fio...\n > Bytes: ");
for(int i=0; i<tam_rede; i++) printf("%02X ", buffer_rede[i]);
printf("\n");
// =============================================================
// 4. SIMULAÇÃO DE ERRO (Opcional)
// =============================================================
char simular_erro;
printf("\n[DEUS] Deseja interferir no sinal e corromper um bit? (s/n): ");
scanf(" %c", &simular_erro);
if (simular_erro == 's' || simular_erro == 'S') {
// Inverte bits no meio do buffer (corrompe o payload)
buffer_rede[tam_rede/2] ^= 0xFF;
printf("[ERRO] Raio cósmico atingiu o cabo! Dados alterados.\n");
}
// =============================================================
// 5. RECEPÇÃO E VALIDAÇÃO
// =============================================================
uint8_t buffer_recebido[MAX_SIZE + 10];
int tam_rec = remover_stuffing(buffer_rede, tam_rede, buffer_recebido);
// Reconstrói a struct a partir dos bytes
Quadro q_rec;
int cursor = 0;
q_rec.origem = buffer_recebido[cursor++];
q_rec.destino = buffer_recebido[cursor++];
q_rec.tamanho = buffer_recebido[cursor++];
for(int i=0; i<q_rec.tamanho; i++) q_rec.dados[i] = buffer_recebido[cursor++];
uint8_t crc_recebido = buffer_recebido[cursor++]; // O CRC que veio no pacote
// VALIDAÇÃO FINAL
printf("\n[RECEPTOR] Quadro recebido de %d.\n", q_rec.origem);
// O Receptor calcula o CRC baseado no que ELE ouviu
uint8_t crc_novo = calcula_crc8(q_rec.dados, q_rec.tamanho);
if (crc_novo == crc_recebido) {
q_rec.dados[q_rec.tamanho] = '\0'; // Finaliza string para imprimir
printf("[SUCESSO] Integridade verificada. Mensagem: \"%s\"\n", q_rec.dados);
} else {
printf("[FALHA] Erro de CRC! Esperado 0x%02X, calculado 0x%02X.\n", crc_recebido, crc_novo);
printf("[AÇÃO] Quadro descartado silenciosamente.\n");
}
return 0;
}
Executando e Testando6.6
- Compile o código:
gcc main.c -o simulador - Execute:
./simulador
- Cenário de Teste 1: Sucesso
- Tente enviar a mensagem
Ola. Não solicite erro. - Você verá que o CRC calculado pelo transmissor será idêntico ao recalculado pelo receptor.
- Tente enviar a mensagem
- Cenário de Teste 2: Byte Stuffing
- Tente enviar a mensagem:
Ola ~ Mundo. - O caractere
~é a nossa FLAG (0x7E). - Observe na saída
[REDE FÍSICA]que o código substituirá o7Epor7D 7E. O receptor, porém, mostrará a mensagem limpa no final. Isso prova que o enquadramento funcionou.
- Tente enviar a mensagem:
- Cenário de Teste 3: Detecção de Erro
- Envie qualquer mensagem e responda 's' (sim) para simular erro.
- O programa irá alterar um byte aleatório durante a "transmissão". O receptor notará que o CRC calculado não bate com o recebido e rejeitará o pacote.
Próximos passos6.7
Este conteúdo ainda não foi finalizado. Assim que estiver completo, este aviso será atualizado com o link correspondente.