Protocolos de comunicação

Mensagens sobre bytes, delimitadores, escaping, integridade e sincronização entre aplicações.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Receber os bytes de MOVE +5 não basta para executar uma ordem. O recetor precisa de reconhecer o início, reunir os campos e validar a mensagem completa. O protocolo define estas regras para ambos os lados.

Limites e significado das mensagens

Se envias um scancode e depois um pacote de rato, o recetor não sabe onde acaba um e começa outro apenas por ver bytes. Um cabeçalho pode indicar tipo e comprimento. Outra opção usa um delimitador de fim que não aparece sem codificação nos dados.

Define também unidades e ordem dos campos. Uma mensagem MOVE pode conter deslocamentos em unidades do protocolo ou uma posição absoluta em píxeis. Os dois sentidos não são equivalentes. Para campos de vários bytes, escolhe explicitamente uma ordem, como byte mais significativo primeiro, e reconstrói-a com máscaras e deslocamentos; nunca envies a representação crua de uma struct, porque o compilador pode inserir padding e as máquinas podem usar tipos ou ordens de bytes diferentes.

Um formato com escaping

Vamos construir um protocolo próprio pequeno, apenas para estudar framing e recuperação. Cada frame tem um delimitador 0x7E, um corpo e outro delimitador. O corpo contém tipo, sequência, comprimento, payload e um checksum XOR.

O checksum é o XOR dos bytes do corpo anteriores a ele. Deteta algumas alterações, mas inverter o mesmo bit em dois bytes conserva o XOR. Um checksum igual não prova que os dados chegaram intactos.

Se 0x7E ou 0x7D aparecer no corpo, transmite 0x7D seguido do byte original XOR 0x20. Assim, um delimitador nos dados torna-se 7D 5E e um escape torna-se 7D 5D. O recetor remove essa codificação antes de interpretar o corpo.

Exemplo com tipo 01, sequência 07, comprimento 02 e payload 7E 05:

Corpo antes de escaping: 01 07 02 7E 05 7F
Frame transmitido:      7E 01 07 02 7D 5E 05 7F 7E

O checksum vale 01 XOR 07 XOR 02 XOR 7E XOR 05 = 7F, por isso o frame ocupa nove bytes na UART. A 9600 bit/s em 8N1, leva pelo menos 9⋅10/9600=9,3759\cdot10/9600=9{,}375 ms.

Construir o frame

Este programa completo é C portátil. Reserva espaço para o pior caso de escaping e imprime exatamente os nove bytes do exemplo.

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

static size_t build_frame(uint8_t type, uint8_t sequence,
                          const uint8_t *payload, size_t length,
                          uint8_t *output, size_t capacity) {
  if (length > 255 || capacity < 2 * (length + 4) + 2) return 0;
  if (length != 0 && payload == NULL) return 0;
  if (output == NULL) return 0;
  uint8_t body[259];
  body[0] = type;
  body[1] = sequence;
  body[2] = (uint8_t)length;
  uint8_t checksum = type ^ sequence ^ (uint8_t)length;
  for (size_t i = 0; i < length; ++i) {
    body[3 + i] = payload[i];
    checksum ^= payload[i];
  }
  body[3 + length] = checksum;
  size_t used = 0;
  output[used++] = 0x7E;
  for (size_t i = 0; i < length + 4; ++i) {
    uint8_t byte = body[i];
    if (byte == 0x7E || byte == 0x7D) {
      output[used++] = 0x7D;
      output[used++] = byte ^ 0x20u;
    } else {
      output[used++] = byte;
    }
  }
  output[used++] = 0x7E;
  return used;
}

int main(void) {
  const uint8_t payload[] = {0x7E, 0x05};
  uint8_t output[32];
  size_t length = build_frame(1, 7, payload, sizeof payload,
                              output, sizeof output);
  if (length == 0) return 1;
  for (size_t i = 0; i < length; ++i)
    printf("%02X%s", (unsigned)output[i], i + 1 == length ? "\n" : " ");
  return 0;
}

O limite de payload é parte deste formato. Não aceites um comprimento recebido antes de verificar a capacidade do buffer. A construção só é válida quando os dois lados usam as mesmas regras de escaping e checksum.

Parser com estado

O recetor conserva estado entre bytes. Um modelo tem WAIT_START, BODY e ESCAPED:

Estado e entradaAção
WAIT_START, 7EComeçar um corpo vazio
WAIT_START, outro byteIgnorar
BODY, 7DPassar a ESCAPED
BODY, byte normalGuardar, se ainda houver capacidade
ESCAPED, 5E ou 5DGuardar byte XOR 20 e regressar a BODY
ESCAPED, 7EDescartar o corpo parcial e usar o delimitador como novo início
BODY, 7EValidar o corpo completo e preparar o próximo

A regra de erro também faz parte do contrato: neste formato, depois de 7D só aceitamos 5E ou 5D, e outro byte invalida o corpo e leva a WAIT_START, onde só um novo delimitador permite recomeçar. Se surge um delimitador com um escape incompleto, descarta o corpo parcial e usa o delimitador para recuperar o início.

Delimitadores consecutivos mantêm um corpo vazio à espera de dados, sem publicar mensagens vazias. Se o corpo excede a capacidade ou há erro UART, invalida a mensagem; um timeout impede ficar para sempre à espera de um corpo que o emissor abandonou.

Segue o frame anterior depois de a UART entregar cada byte. Aqui omitimos o delimitador inicial, que já colocou o parser em BODY:

EntradaEstadoCorpoSaída
01 07 02BODY01 07 02Nenhum
7DESCAPED01 07 02Nenhum
5EBODY01 07 02 7ENenhum
05 7FBODY01 07 02 7E 05 7FNenhum
7EBODY, novo corpo vazioCorpo anterior validadoUma mensagem aceite

O byte 5E só representa 7E porque o estado anterior era ESCAPED. O fecho é o primeiro momento em que se conhece a validade da mensagem inteira.

Uma chegada fragmentada não é um erro. O handler pode receber dois bytes agora e os restantes depois. A fila e o parser conservam-nos sem bloquear à espera da mensagem completa. Um delimitador pode aparecer depois de uma mensagem danificada e permitir recomeçar.

ACK, timeout e duplicados

Um ACK confirma que o recetor aceitou uma mensagem, segundo a regra definida. Não é uma prova de que o utilizador já viu o efeito no ecrã.

Num protocolo stop-and-wait, o emissor guarda a mensagem, envia-a e espera o ACK. Se o timeout termina, reenvia. Para impedir duplicação de efeitos, cada mensagem tem uma sequência:

  1. O emissor envia MOVE +5 com sequência 7.
  2. O recetor aplica o movimento uma vez e envia ACK 7.
  3. O ACK perde-se e o emissor reenvia a mesma mensagem.
  4. O recetor reconhece a sequência já aceite, envia novamente ACK 7 e não repete o movimento.

Sem o quarto passo, o cursor moveria +10, por isso define a sequência esperada, o tratamento de duplicados, o número máximo de tentativas e o comportamento quando a sessão falha. Um campo de sequência de oito bits dá a volta; a identificação da sessão e a janela de mensagens válidas têm de impedir que uma mensagem muito antiga pareça nova.

Para dados que representam um estado absoluto, aplicar duas vezes o mesmo valor pode ser inofensivo. Uma posição absoluta pode substituir um delta quando essa política é adequada. Continua a ser necessário rejeitar mensagens antigas que fariam o estado recuar.

Iniciar e sincronizar

Uma solução simples atribui papéis distintos: um lado inicia JOIN e o outro responde START quando está pronto. Se o recetor ainda não existe, o iniciador repete com timeout até ao limite escolhido. Ambos precisam de concordar sobre versão do protocolo e sessão.

Se ambos iniciarem ao mesmo tempo, define um desempate por identidade ou papel. Não esperes que atrasos aleatórios resolvam sempre uma regra que falta. Uma reconexão também deve descartar bytes e estado parcial da sessão anterior.

Numa aplicação centralizada, um computador calcula o estado e o outro envia entradas e apresenta o estado recebido. Na replicação, ambos calculam o estado. Para obter o mesmo resultado, precisam do mesmo estado inicial, das mesmas entradas na mesma ordem, dos mesmos instantes lógicos e de regras determinísticas. Receber as mesmas entradas por ordens diferentes pode produzir resultados diferentes.

Capacidade da porta série

Uma imagem de 1024 por 768 a 8 bpp tem 786432 bytes. A 115200 bit/s em 8N1, a capacidade ideal é 11520 bytes/s, por isso uma imagem levaria pelo menos:

78643211520≈68,27 s.\frac{786432}{11520}\approx68{,}27\ \text{s}.

Um frame por 68 segundos não serve para uma animação. Envia entradas, posições ou alterações de estado e desenha localmente. O cálculo continua otimista: ignora cabeçalhos, checksum, escaping, confirmações e retransmissões.

Exercícios

Calcular o custo do frame

No protocolo da página, o corpo antes de escaping é 02 01 02 7D 00 7C. Identifica os bytes que precisam de escaping e calcula quantos bytes seguem na linha, com um delimitador no início e outro no fim.

Pista
Há seis bytes no corpo antes de escaping.
Solução

O corpo cresce de seis para sete bytes. Os dois delimitadores elevam o total para nove. O byte 7D passa a 7D 5D; os restantes mantêm-se.

Erros frequentes

Contar apenas payload ou esquecer que o byte de escape original também tem de ser codificado.

Voltar à explicação
Impedir movimento duplicado

O recetor aplica MOVE +8 com sequência 14, mas o ACK perde-se. O emissor termina o timeout e reenvia. Que regra mantém o movimento em +8 em vez de +16?

Pista
O recetor deve lembrar mensagens já aceites.
Solução

O emissor reenvia com sequência 14. O recetor reconhece o duplicado da mensagem aceite, volta a confirmar 14 e não aplica de novo +8. O tratamento precisa de um contexto de sessão e janela válidos para não confundir mensagens antigas.

Confere a tua resposta:

  • Reconheci que o ACK pode perder-se após aplicar o movimento.
  • Mantive a mesma sequência no reenvio.
  • Reenviei ACK sem repetir o efeito.

Erros frequentes

Incrementar a sequência em cada tentativa ou enviar ACK apenas na primeira receção, o que mantém o emissor a repetir.

Voltar à explicação
Estimar um envio inviável

Uma imagem tem 921600 bytes. A porta transmite 115200 bit/s em 8N1, sem pausas. Qual é o tempo mínimo ideal para enviar uma imagem?

Pista
Cada byte útil ocupa dez bits.
Solução

O tempo é 921600/11520 = 80 segundos. Cabeçalhos e retransmissões aumentariam o tempo. Para uma aplicação interativa, envia alterações de estado e desenha localmente.

Erros frequentes

Dividir bytes diretamente por bit/s ou supor que o start e o stop não ocupam a linha.

Voltar à explicação
Recuperar depois de um escape incompleto

O parser começa em WAIT_START e recebe 7E 01 07 02 7D 7E, seguido de um frame válido completo. Que corpo pode publicar? Explica o estado imediatamente após o segundo 7E e por que não deve aplicar dados parciais.

Pista
7D pede um byte codificado, mas chega um delimitador.
Mais uma pista

O segundo 7E descarta o corpo incompleto e prepara um início.

Solução

Não publica o primeiro corpo. Ao chegar o segundo 7E no estado ESCAPED, descarta 01 07 02 e regressa a BODY com corpo vazio. O delimitador inicial do frame seguinte conserva esse corpo vazio. Só publica a mensagem seguinte depois de validar campos, comprimento e checksum no fecho. Aplicar campos da primeira mensagem antes disso deixaria efeitos de uma mensagem inválida.

Confere a tua resposta:

  • Descartei o primeiro corpo.
  • Recuperei BODY com corpo vazio.
  • Publiquei só a mensagem seguinte validada.

Erros frequentes

Guardar 7E XOR 20 como dado depois de um escape, ou atualizar a aplicação antes da validação completa.

Voltar à explicação
Erro não detetado pelo XOR

O corpo é 01 07 02 7E 05, seguido de checksum XOR 7F. Escolhe dois bytes do payload e altera-os de modo que o checksum continue a passar, sem mudar tipo, sequência ou comprimento.

Mostra o corpo alterado. Se o recetor o aceitar e enviar ACK, um timeout com retransmissão resolve este erro? Justifica.

Pista
Inverter o mesmo bit duas vezes anula-se no XOR total.
Solução

Inverte o bit 0 dos dois bytes do payload: 7E torna-se 7F e 05 torna-se 04. O novo corpo é 01 07 02 7F 04.

No original, 7E XOR 05 = 7B; no alterado, 7F XOR 04 = 7B. Com os mesmos campos anteriores, o checksum continua 7F, apesar de o payload mudar. Esta conta refere-se aos bytes do corpo depois de remover escaping.

Se o recetor aceitar a mensagem e confirmar, não há timeout que revele a corrupção. Retransmissão recupera perdas ou erros detetados, mas não torna esta verificação infalível. Uma verificação mais forte, como um CRC adequado ao formato, pode detetar mais padrões de erro; também tem limites.

Confere a tua resposta:

  • Construí duas alterações do mesmo bit em bytes distintos.
  • Mostrei o XOR 0x7F antes e depois.
  • Expliquei por que um ACK e um timeout não detetam essa corrupção.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.