Como o protocolo funciona
O protocolo AMQP divide seus participantes entre "clientes", que podem ser tanto
"remetentes" quanto "assinantes", a depender da sua interação com
as
mensagens, e o "broker", entidade centralizada responsável por fazer a
distribuição das mensagens e gerenciamento das estruturas.
Uma das características de destaque do AMQP frente a outros protocolos pub/sub é o uso
de
exchanges, que determinam a maneira como as mensagens serão roteadas, e filas, que
são ligadas às exchanges e armazenam as mensagens a serem consumidas em
regime
First-In-First-Out. Um assinante seleciona uma exchange e cria nela uma fila ou
consome uma fila já existente.
Para publicar uma mensagem, o remetente seleciona uma exchange e uma chave de roteamento,
que
serve o propósito do tópico. As variações de comportamento por
exchange incluem rotear para filas que têm chave igual à chave de roteamento,
para
filas que correspondem parcialmente à chave de roteamento, para todas as filas na
exchange, e métodos de roteamento mais complexos baseados nos cabeçalhos da
mensagem.
Estruturação do protocolo
Lendo a especificação, temos que o protocolo estabelece cinco tipos de dado:
- Inteiros, podendo ser de 1 a 8 bytes, sempre sem sinal;
- Bits, ocupando 1 byte;
- Strings curtas, podendo ter até 255 bytes;
- Strings longas, de tamanho indefinido (ainda)
- Tabelas de campos, que carregam pares chave-valor com tipagem explícita.
Além disso, experimentando com o Wireshark e o amqp-tools, foram obtidas (e depois confirmadas pela leitura da especificação) as seguintes propriedades:
- Strings (curtas e longas) e tabelas de campos são prefixadas por um inteiro que indica seu tamanho em bytes.
- Dentro de uma tabela de símbolos, as chaves são strings curtas (prefixadas como
acima), seguidas de um caracter indicando o tipo do valor
- Bit/Booleano = 't'
- Strings curtas = 'S'
- Tabelas = 'F'
- Outros tipos não apareceram nos pacotes trocados, mas estão especificados. Inclusive, todos os tipos inteiros seguem um padrão de caractere minúsculo para signed, e caractere maiúsculo para unsigned. Exceto para short-short-(u)int, que é o contrário.
Estrutura de Pacotes
Os pacotes do protocolo são todos estruturados em uma maneira padronizada, o que facilita a
implementação de máquinas de estado para verificação e
classificação destes.
A estrutura geral dos pacotes é a seguinte:
- 1 byte: determina o tipo de pacote
- Método tem valor 1, Header tem valor 2, Body tem valor 3
- 2 bytes: determina o canal (não usei mais de um canal)
- 4 bytes: determina o comprimento do payload
- "comprimento" bytes: o payload em si
- 1 byte: 0xCE, valor de "fim" (de tabela ou de pacote)
Os tipos de pacote são divididos entre Método, Header, Body, os mais comuns, e alguns outros mais específicos, como Heartbeat.
A maior parte do overhead do protocolo se dá por trocas de pacotes do tipo Método, sendo estes divididos em várias classes, sendo as principais:
- Connection, lidando com dados relacionados à negociação dos parâmetros de comunicação entre cliente e broker;
- Channel, dedicado à abertura e fechamento de canais dentro da conexão;
- Queue, para criação e ligação (Bind) de Queues, que são usadas para entrega de mensagens;
- Basic, responsável pelo funcionamento da troca de mensagens em si.
Um pacote do tipo "Método" tem a seguinte estrutura: - 2 bytes: classe do método (10 para Connection, 20 para Channel, 50 para Queue, e 60 para Basic);
- 2 bytes: o método em si;
- "comprimento - 4" bytes: Uma lista de argumentos do método.
Os argumentos do método têm tipos específicos a depender do método. Por exemplo, o método Connection.Start, primeira mensagem do broker para um cliente, têm os seguntes argumentos:
- "Version-Major", inteiro de 1 byte;
- "Version-Minor", inteiro de 1 byte;
- "Server-Properties", tabela de campos;
- "Mechanisms", string longa;
- "Locales", string longa.
Esses argumentos e seus tipos estão também descritos na especificação, facilitando a implementação contida da interpretação desses pacotes, uma vez identificados.
Explicação dos Métodos
Cada método será descrito da seguinte forma:
- Nome do método (Cliente/Broker): Explicação do
método.
- Anotações de casos de erro ou de particularidades.
Classe Connection
Todas essas mensagens são enviadas pelo canal 0, reservado para essas comunicações.
- Start Broker: Primeira mensagem de fato estruturada, sinaliza para o cliente as capacidades e características do broker. Dentre elas, os mecanismos de autenticação e a linguagem (locale).
- Start-OK Cliente: Sinaliza para o broker as capacidades e
características do cliente, e seleciona um mecanismo de autenticação e um
locale para serem usados nessa conexão. Além disso, também faz a
autenticação.
- Caso o mecanismo enviado não seja um dos suportados pelo Broker, este fecha o socket automaticamente.
- Tune Broker: Estabelece limites para a quantidade de Canais, tamanho de quadro AMQP, e tempo com que as partes devem trocar sinais de "Heartbeat" para sinalizar que a conexão continua ativa
- Tune-OK Cliente: Negocia os limites da conexão ou confirma os já estabelecidos pelo broker.
- Open Broker: Sinaliza que a conexão foi aberta.
- Open-OK Cliente: Confirma a abertura da conexão.
- Close Broker/Cliente: Sinaliza desejo de fechamento de conexão, enviando código de resposta (como respostas HTTP), e mais informações sobre o erro.
- Close-OK Broker/Cliente: Confirma o fechamento da conexão.
Classe Channel
Todas as mensagens são enviadas pelo canal de interesse.
- Open Cliente: Sinaliza desejo de abertura do canal.
- Open-OK Broker: Confirma abertura do canal.
- Flow Broker/Cliente: Sinaliza que o fluxo de dados do canal deve ser interrompido/resumido.
- Flow-OK Broker/Cliente: Confirma o processamento do comando de Flow
- Close Broker/Cliente: Sinaliza desejo de fechamento do canal, enviando código de resposta (como respostas HTTP), e mais informações sobre o erro.
- Close-OK Broker/Cliente: Confirma fechamento do canal
Classe Queue
- Declare Cliente: Solicita a declaração de uma fila, especificando os atributos da fila.
- Declare-OK Broker: Confirma a criação da fila, fornecendo o nome da fila gerada se não foi providenciado.
- Bind Cliente: Solicita a ligação de uma fila específica a uma exchange, e estabelece a chave de roteamento para esse pareamento exchange-fila.
- Bind-OK Broker: Confirma a ligação entre uma exchange e uma fila.
- Unbind Cliente: Basicamente o método para reverter o método Bind.
- Unbind-OK Broker: Confirma a remoção da ligação entre uma exchange e uma fila.
- Purge Cliente: Pede a deleção de todas as mensagens de uma fila que não estão esperando uma mensagem de ACK.
- Purge-OK Broker: Confirma a deleção das mensagens.
- Delete Cliente: Solicita a deleção de uma fila de mensagens.
- Delete-OK Broker: Confirma a deleção da fila. Isso cancela todos os assinantes da fila.
Classe Basic
- QoS Cliente: Solicita uma determinada qualidade de serviço do Broker. Isso é especificado na quantidade de bytes ou mensagens a serem "pre-enviados" ao assinante.
- QoS-OK Broker: Confirma que o QoS será atendido pelo broker.
- Consume Cliente: Solicita que o broker envie mensagens que chegarem em uma fila específica para esse canal, criando um "Consumer" para aquela fila.
- Consume-OK Broker: Confirma que a ação de Consume foi processada pelo broker.
- Cancel Cliente: Cancela o consumo de uma fila.
- Cancel-OK Broker: Confirma que a ação de Cancel foi processada pelo broker.
- Publish Cliente: Publica uma mensagem para ser roteada em uma exchange
específica com uma chave de roteamento específica.
- Esse Método deve ser acompanhado de um pacote do tipo "Header", com o cabeçalho da mensagem, e "Body", com o corpo da mensagem.
- Pode ter a flag "Mandatory", para indicar que o remetente deve ser avisado caso a mensagem não possa ser roteada
- Pode ter a flag "Immediate", para indicar que, se a mensagem não puder ser entregue para um Consumer, o remetente deve ser avisado.
- Return Broker: Responde a um método de "Publish" com as flags de Mandatory e Immediate.
- Deliver Broker: Entrega uma mensagem publicada em uma fila.
- Get Cliente: Extrai uma mensagem de uma fila, sem criar um Consumer.
- Get-OK Broker: Retorna a mensagem da fila requisitada pelo método "Get".
- Get-Empty Broker: Responde a um método de "Get" que a fila requisitada está vazia.
- ACK Cliente: Confirma o recebimento de uma ou mais mensagens (ack individual ou cumulativo).
- Reject Cliente: Indica ao broker que a mensagem não pode ser processada, podendo ser re-enfileirada ou descartada.
- Recover Cliente: Solicita ao broker que reenvie todas as mensagens do canal que ainda não receberam ACK, podendo especificar que estas sejam re-enfileirada em vez disso.
- Recover-OK Broker: Confirma o processamento do Recover.
Um pouco sobre exchanges
Como foi mencionado no começo, o protocolo usa de Exchanges para determinar diferentes mecanismos de roteamento das mensagens publicadas. Essas exchanges podem ser definidas pela implementação de cada Broker, mas o protocolo determina quatro tipos de exchange padrão, sendo elas:
- A Exchange padrão (do tipo "amq.direct"), na qual a chave de roteamento da mensagem deve ser igual ao nome de uma fila para que a mensagem chegue;
- O tipo "amq.topic", no qual a chave de roteamento usada no Bind é um
padrão,
e a mensagem publicada será roteada para todas as filas às quais sua chave se
encaixa.
Os padrões consideram palavras separadas por pontos (por exemplo:
"chave.de.exemplo"), e os caracteres especiais para padrões são os
seguintes:
- "*" para encontrar exatamente uma palavra nessa posição ("chave.de.*" ou "*.de.exemplo");
- "#" para encontrar zero ou mais palavras nessa posição ("#.exemplo", "chave.#.exemplo" ou "chave.de.exemplo.#");
- O tipo "amq.fanout", no qual as mensagens enviadas na Exchange desse tipo são enviadas para todas as filas da exchange, ignorando a chave de roteamento;
- O tipo "amq.match", no qual o roteamento é feito olhando para os Headers das mensagens, ignorando a chave de roteamento.
Há um conjunto de métodos não explorados aqui dedicados à criação de Exchanges.
Analisando as capacidades do protocolo
A partir desse resumo do que o protocolo suporta, podemos estipular algumas capacidades e casos de uso.
A primeira e principal capacidade que salta é a possibilidade de implementar mensageiros. Um broker central e dois clientes diferentes, cada um consumindo uma fila e ciente da fila que o outro consome, podem trocar mensagens entre si. Se a comunicação acordada é em turnos, ou seja, se o cliente A publica, deve esperar uma mensagem do cliente B para publicar novamente, é possível reduzir o sistema a uma única fila, consumida por ambos os clientes.
Além disso, a existência do método "Get" nos indica também um comportamento de cliente-servidor clássico, no qual os clientes apenas extraem a informação desejada sem necessariamente onerar o broker com a criação de uma entidade dedicada para consumir aquela fila. Esse comportamento pode se mostrar útil em um sistema onde há uma quantidade limitada de recursos que podem ser obtidos: no caso de uma distribuição de tickets para um evento, por exemplo.
Voltando agora nas capacidades de troca de mensagens, temos que, com as diferentes exchanges, é possível implementar um sistema de chat de um jogo online, com as seguintes capacidades:
- Mensagens privadas, utilizando uma fila para cada usuário, na exchange
"amq.direct";
- Vale mencionar aqui uma característica do header de uma mensagem que não havia sido citado: a propriedade Reply-To, que indica o endereço (uma fila, no caso) no qual aquela mensagem deve ser respondida.
- Assim, é possível implementar não só o comando usual
/whisper <usuario> <mensagem>
para enviar uma mensagem privada como também o comando/reply <mensagem>
para responder a última mensagem recebida.
- Mensagens de guilda, utilizando a exchange "amq.topic" com uma fila para cada usuário usando a chave "nome-da-guilda.#" e as mensagens sendo publicadas com a chave "nome-da-guilda.mensagem-membro.nome-do-remetente", por exemplo; e
- Mensagens globais, como anúncios de sistema ou mensagens de moderação,
utilizando filas na exchange "amq.fanout".
Pode-se também imaginar um uso para a exchange "amq.match" para que mensagens do servidor de jogo como, por exemplo, venda de um item no mercado do jogo possam ser convertidas em uma mensagem com o header "Vendedor" que conteria o nome do jogador que vendeu o item, e esse jogador receberia a mensagem roteada pelo campo do header.
No mais, temos a capacidade de utilizar esse sistema no contexto de IoT, utilizando sensores,
atuadores,
e uma entidade "decisora" (que pode estar na mesma máquina que o broker, já
que
esta já deve ser mais potente).
Assim, sensores de umidade podem todos enviar seus dados em uma fila "umidade" da exchange
padrão, sensores de temperatura podem enviar seus dados em uma fila separada
"temperatura" da mesma exchange, e a entidade decisora consome a mensagem das duas
filas.
Essa entidade pode, então, fazer os cálculos necessários e, se decidir que o
conforto térmico de um cômodo ou da casa toda está baixo, pode enviar uma
mensagem
ou aos atuadores específicos ou enviar uma mensagem com a chave
"atuadores.temperatura"
na exchange "amq.topic" para ativar todos eles.