# Protocolos de comunicação

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

Página: https://resumos.rgo.pt/cadeiras/lc/protocolos/

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\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 entrada | Ação |
| --- | --- |
| `WAIT_START`, `7E` | Começar um corpo vazio |
| `WAIT_START`, outro byte | Ignorar |
| `BODY`, `7D` | Passar a `ESCAPED` |
| `BODY`, byte normal | Guardar, se ainda houver capacidade |
| `ESCAPED`, `5E` ou `5D` | Guardar byte XOR `20` e regressar a `BODY` |
| `ESCAPED`, `7E` | Descartar o corpo parcial e usar o delimitador como novo início |
| `BODY`, `7E` | Validar 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.

Ideia-chave

O parser monta uma **mensagem candidata**, não uma ordem já aceite. No fecho, confere tipo, número de campos, comprimento e checksum. Só depois publica um evento da aplicação. Não atualizes a posição com o primeiro campo antes de saber se o resto da mensagem é válido.

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

| Entrada | Estado | Corpo | Saída |
| --- | --- | --- | --- |
| `01 07 02` | `BODY` | `01 07 02` | Nenhum |
| `7D` | `ESCAPED` | `01 07 02` | Nenhum |
| `5E` | `BODY` | `01 07 02 7E` | Nenhum |
| `05 7F` | `BODY` | `01 07 02 7E 05 7F` | Nenhum |
| `7E` | `BODY`, novo corpo vazio | Corpo anterior validado | Uma 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:

$$
\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](https://resumos.rgo.pt/cadeiras/lc/protocolos/#um-formato-com-escaping)

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](https://resumos.rgo.pt/cadeiras/lc/protocolos/#ack-timeout-e-duplicados)

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](https://resumos.rgo.pt/cadeiras/lc/protocolos/#capacidade-da-porta-s%C3%A9rie)

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](https://resumos.rgo.pt/cadeiras/lc/protocolos/#parser-com-estado)

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](https://resumos.rgo.pt/cadeiras/lc/protocolos/#um-formato-com-escaping)
