UDP Multicast

Este post faz parte da série IPC(Inter process communication)

Introdução

O UDP no modo broadcast permite enviar mensagens para todas as máquinas conectadas na rede de uma única vez, porém essa forma de envio pode prejudicar o desempenho da rede dependendo do tamanho e da frequência da mensagem enviada, para contornar esses problemas existe um modo conhecido como multicast, que é parecido com o broadcast mas envia a mensagem somente para as máquinas que estejam interessadas nesse conteúdo, dessa forma evita-se que haja um congestionamento na rede devido a replicação de mensagens para máquinas não interessadas. Para que as máquinas interessadas na mensagem transmitida, essas máquinas deverão se cadastrar em um grupo conhecido como IP multicast para que possam a partir desse registro receberem as mensagens.

Endereço Multicast

Para determinar o endereço multicast é necessário conhecer as classes de IP que são separadas em classes A, B, C, D e E. Para modo multicast foi reservado a classe D que é dedicado exclusivamente para esse propósito, possuindo um range de 224.0.0.0 até 239.255.255.255, dessa forma o emissor pode enviar para qualquer um desses endereços.

Representação do Multicast na rede

Quando uma mensagem multicast é enviada as máquinas registradas irão receber essas mensagens. Para ilustrar, o exemplo representa a transmissão de uma mensagem multicast.

Na imagem é possível notar que as mensagem chegam somente nas máquinas interessadas

Selecionando o endereço multicast

Para selecionar o endereço de multicast para aplicação podemos pesquisar no site www.iana.org, que apresenta a finalidade de cada range.

De acordo com as recomendações, é selecionado o intervalo 232.192.0.0/24, sendo esse reservado para aplicações de uso privado, dentro desse range é selecionado o 239.192.1.1 como endereço do grupo multicast para a aplicação.

Preparação do Ambiente

Antes de apresentar o exemplo, primeiro é necessário instalar algumas ferramentas para auxiliar na análise da comunicação. As ferramentas necessárias para esse artigo são o tcpdump e o netcat(nc), para instalá-las basta executar os comandos abaixo:

sudo apt-get update
sudo apt-get install netcat
sudo apt-get install tcpdump

netcat

O netcat é uma ferramenta capaz de interagir com conexões UDP e TCP, podendo abrir conexões, ouvindo como um servidor, ou como cliente enviando mensagens para um servidor.

tcpdump

O tcpdump é uma ferramenta capaz de monitorar o tráfego de dados em uma dada interface como por exemplo eth0, com ele é possível analisar os pacotes que são recebido e enviados.

Implementação

Para demonstrar o uso desse IPC, é adotado o modelo Cliente/Servidor, onde o processo Cliente(button_process) vai enviar uma mensagem via multicast para o servidor(led_process) que vai ler a mensagem, e verificar se corresponde com os comandos cadastrados internamente e processar o comando caso seja válido.

Biblioteca

A biblioteca criada permite uma fácil criação do servidor, sendo o servidor orientado a eventos, ou seja, fica aguardando as mensagens chegarem.

udp_multicast_receiver.h

Primeiramente é criado um callback responsável pelo tratamento de eventos de recebimento, essa função será chamada quando houver esse evento.

typedef void (*Event)(const char *buffer, size_t buffer_size, void *data);

É criado também um contexto que armazena os parâmetros utilizados pelo servidor, sendo o socket para armazenar a instância criada, port que recebe o número que corresponde onde o serviço será disponibilizado, buffer que aponta para a memória alocada previamente pelo usuário, buffer_size o representa o tamanho do buffer, o callback para recepção da mensagem e o endereço do grupo multicast

typedef struct 
{
    int socket;
    int port;
    char *buffer;
    size_t buffer_size;
    Event on_receive_message;
    const char *multicast_group;
} UDP_Receiver;

Essa função inicializa o servidor com os parâmetros do contexto

bool UDP_Multicast_Receiver_Init(UDP_Receiver *receiver);

Essa função aguarda uma mensagem publicada no grupo multicast pelo cliente.

bool UDP_Multicast_Receiver_Run(UDP_Receiver *receiver, void *user_data);

udp_multicast_receiver.c

No UDP_Multicast_Receiver_Init é definido algumas variáveis para auxiliar na inicialização do servidor, sendo uma variável booleana que representa o estado da inicialização do servidor, uma variável do tipo inteiro para habilitar o reuso da porta caso o servidor precise reiniciar, uma estrutura sockaddr_in que é usada para configurar o servidor para se comunicar através da rede e uma estrutura utilizada para o registro do servidor no grupo multicast.

bool status = false;
int yes = 1;
struct sockaddr_in server_addr;
struct ip_mreq multicast;

Para realizar a inicialização é criado um dummy do while, para que quando houver falha em qualquer uma das etapas, irá sair da função com status de erro, nesse ponto é verificado se o contexto, o buffer e se o tamanho do buffer foi inicializado, sendo sua inicialização de responsabilidade do usuário

if(!receiver || !receiver->buffer || !receiver->buffer_size)
    break;

É criado um endpoint com o perfil de se conectar via protocolo IPv4(AF_INET), do tipo datagram que caracteriza o UDP(SOCK_DGRAM), o último parâmetro pode ser 0 nesse caso.

receiver->socket = socket(AF_INET, SOCK_DGRAM, 0);
if(receiver->socket < 0)
    break;

A estrutura é preenchida com parâmetros fornecidos pelo usuário como em qual porta que o serviço vai rodar.

memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(receiver->port);

Aqui é habilitado o reuso do socket caso necessite reiniciar o serviço

if (setsockopt(receiver->socket, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0)
    break;

É aplicada as configurações ao socket criado

if (bind(receiver->socket, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    break;

O servidor é registrado no grupo multicast e é atribuído true na variável status

multicast.imr_multiaddr.s_addr = inet_addr(receiver->multicast_group);
multicast.imr_interface.s_addr = htonl(INADDR_ANY);

if(setsockopt(receiver->socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&multicast, sizeof(multicast)) < 0)
    break;
status = true;

Na função UDP_Multicast_Receiver_Run é declarada algumas variáveis para receber as mensagens por meio do multicast

bool status = false;
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
size_t read_size;

É verificado se o socket é válido e é aguardada uma mensagem no canal de multicast, a mensagem é repassada para o callback para realizar o tratamento de acordo com a aplicação do cliente, e é retornado o status.

if(receiver->socket > 0)
{
    read_size = recvfrom(receiver->socket, receiver->buffer, receiver->buffer_size, MSG_WAITALL,
                                (struct sockaddr *)&client_addr, &len); 
    receiver->buffer[read_size] = 0;
    receiver->on_receive_message(receiver->buffer, read_size, user_data);
    memset(receiver->buffer, 0, receiver->buffer_size);
    status = true;
}

return status;

udp_multicast_sender.h

É criado também um contexto que armazena os parâmetros utilizados pelo cliente, sendo o socket para armazenar a instância criada, hostname é o ip do canal multicast que vai ser enviado as mensagens e o port que recebe o número que corresponde onde o serviço vai ser disponibilizado

typedef struct 
{
    int socket;
    const char *hostname;
    const char *port;
} UDP_Sender;

Inicializa o cliente com os parâmetros do descritor

bool UDP_Multicast_Sender_Init(UDP_Sender *sender);

Envia mensagem para o grupo multicast baseado nos parâmetros do descritor.

bool UDP_Multicast_Sender_Send(UDP_Sender *sender, const char *message, size_t message_size);

udp_multicast_sender.c

Na função UDP_Multicast_Sender_Init é verificado se o contexto foi iniciado, o socket é configurado como UDP e é habilitado o envio no modo multicast

int multicast_enable;
bool status = false;
do 
{
    if(!sender)
        break;

    sender->socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if(sender->socket < 0)
        break;

    multicast_enable = 1;
    if(setsockopt(sender->socket, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&multicast_enable, sizeof(multicast_enable)) < 0)
        break;

    status = true;        
}while(false);

return status;

Na função UDP_Multicast_Sender_Send é declarada algumas variáveis para auxiliar na comunicação com o servidor, sendo uma variável booleana que representa o estado de envio para o servidor, uma estrutura sockaddr_in que é usada para configurar o servidor no qual será enviado as mensagens e uma variável de quantidade de dados enviados.

bool status = false;
struct sockaddr_in server;
ssize_t send_len;

A estrutura é parametrizada com os dados do servidor

memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(sender->hostname);
server.sin_port = htons(atoi(sender->port));

Realiza o envio da mensagem para o canal multicast

send_len = sendto(sender->socket, message, message_size, 0, (struct sockaddr *)&server, sizeof(server));
  if(send_len == message_size)
      status = true;

return status;

Aplicação é composta por três executáveis sendo eles:

  • launch_processes – é responsável por lançar os processos button_process e led_process através da combinação fork e exec
  • button_interface – é responsável por ler o GPIO em modo de leitura da Raspberry Pi e escrever o estado interno no arquivo
  • led_interface – é responsável por ler do arquivo o estado interno do botão e aplicar em um GPIO configurado como saída

launch_processes

No main é criada duas variáveis para armazenar o PID do button_process e do led_process, e mais duas variáveis para armazenar o resultado caso o exec venha a falhar.

int pid_button, pid_led;
int button_status, led_status;

Em seguida é criado um processo clone, se processo clone for igual a 0, é criado um array de strings com o nome do programa que será usado pelo exec, em caso o exec retorne, o estado do retorno é capturado e será impresso no stdout e aborta a aplicação. Se o exec for executado com sucesso o programa button_process será carregado.

pid_button = fork();

if(pid_button == 0)
{
    //start button process
    char *args[] = {"./button_process", NULL};
    button_status = execvp(args[0], args);
    printf("Error to start button process, status = %d\n", button_status);
    abort();
}   

O mesmo procedimento é repetido novamente, porém com a intenção de carregar o led_process.

pid_led = fork();

if(pid_led == 0)
{
    //Start led process
    char *args[] = {"./led_process", NULL};
    led_status = execvp(args[0], args);
    printf("Error to start led process, status = %d\n", led_status);
    abort();
}

button_interface

É definida uma lista de comandos que para o envio

const char *led_commands[] = 
{
    "LED ON",
    "LED OFF"
};

A implementação do Button_Run ficou simples, onde é realizada a inicialização do interface de botão e fica em loop aguardando o pressionamento do botão para alterar o estado da variável e enviar a mensagem para o canal multicast

bool Button_Run(UDP_Sender *sender, Button_Data *button)
{
    int state = 0;

    if(button->interface->Init(button->object) == false)
        return false;

    if(UDP_Multicast_Sender_Init(sender) == false)
        return false;

    while (true)
    {
        wait_press(button);
        state ^= 0x01;
        UDP_Multicast_Sender_Send(sender, led_commands[state], strlen(led_commands[state]));
    }

    return false;
}

led_interface

A implementação do LED_Run ficou simplificada, é realizada a inicialização da interface de LED, do servidor e fica em loop aguardando o recebimento de uma mensagem.

bool LED_Run(UDP_Receiver *receiver, LED_Data *led)
{

	if(led->interface->Init(led->object) == false)
		return false;

	if(UDP_Multicast_Receiver_Init(receiver) == false) 
		return false;


	while(true)
	{
		UDP_Multicast_Receiver_Run(receiver, led);
	}

	return false;	
}

button_process

A parametrização do cliente fica por conta do processo de botão que inicializa o contexto com o endereço multicast, o serviço e assim os argumentos são passados para Button_Run iniciar o processo.

UDP_Sender sender = 
{
    .hostname = "239.192.1.1",
    .port  = "1234"
};

Button_Run(&sender, &button);

led_process

A parametrização do servidor fica por conta do processo de LED que inicializa o contexto com o buffer, seu tamanho, a porta do serviço que vai consumir e o callback preenchido, e assim os argumentos são passados para LED_Run iniciar o serviço.

UDP_Server receiver = 
{
    .buffer = server_buffer,
    .buffer_size = BUFFER_SIZE,
    .port = 1234,
    .on_receive_message = on_receive_message,
    .multicast_group = "239.192.1.1"
};

LED_Run(&receiver, &led);

A implementação no evento de recebimento da mensagem, compara a mensagem recebida com os comandos internos para o acionamento do LED, caso for igual executa a ação correspondente.

void on_receive_message(const char *buffer, size_t buffer_size, void *data)
{
    LED_Data *led = (LED_Data *)data;

    if(strncmp("LED ON", buffer, strlen("LED ON")) == 0)
        led->interface->Set(led->object, 1);
    else if(strncmp("LED OFF", buffer, strlen("LED OFF")) == 0)
        led->interface->Set(led->object, 0);
}

Compilando, Executando e Matando os processos

Para compilar e testar o projeto é necessário instalar a biblioteca de hardware necessária para resolver as dependências de configuração de GPIO da Raspberry Pi.

Compilando

Para facilitar a execução do exemplo, o exemplo proposto foi criado baseado em uma interface, onde é possível selecionar se usará o hardware da Raspberry Pi 3, ou se a interação com o exemplo vai ser através de input feito por FIFO e o output visualizado através de LOG.

Clonando o projeto

Pra obter uma cópia do projeto execute os comandos a seguir:

$ git clone https://github.com/NakedSolidSnake/Raspberry_IPC_Socket_UDP_Multicast
$ cd Raspberry_IPC_Socket_UDP_Multicast
$ mkdir build && cd build

Selecionando o modo

Para selecionar o modo é necessário passar para o cmake uma variável de ambiente chamada de ARCH, e pode-se passar os seguintes valores, PC ou RASPBERRY, para o caso de PC o exemplo terá sua interface preenchida com os sources presentes na pasta src/platform/pc, que permite a interação com o exemplo através de FIFO e LOG, caso seja RASPBERRY usará os GPIO’s descritos no artigo.

Modo PC

$ cmake -DARCH=PC ..
$ make

Modo RASPBERRY

$ cmake -DARCH=RASPBERRY ..
$ make

Executando

Para executar a aplicação execute o processo launch_processes para lançar os processos button_process e led_process que foram determinados de acordo com o modo selecionado.

$ cd bin
$ ./launch_processes

Uma vez executado podemos verificar se os processos estão rodando atráves do comando

$ ps -ef | grep _process

O output

cssouza  31588  2298  0 08:15 pts/1    00:00:00 ./button_process
cssouza  31589  2298  0 08:15 pts/1    00:00:00 ./led_process

Interagindo com o exemplo

Dependendo do modo de compilação selecionado a interação com o exemplo acontece de forma diferente

MODO PC

Para o modo PC, precisamos abrir um terminal e monitorar os LOG’s

$ sudo tail -f /var/log/syslog | grep LED

Dessa forma o terminal irá apresentar somente os LOG’s referente ao exemplo.

Para simular o botão, o processo em modo PC cria uma FIFO para permitir enviar comandos para a aplicação, dessa forma todas as vezes que for enviado o número 0 irá logar no terminal onde foi configurado para o monitoramento, segue o exemplo

echo  "0" > /tmp/multicast_fifo

Output do LOG quando enviado o comando algumas vezes

May 23 08:16:19 dell-cssouza LED UDP[31589]: LED Status: On
May 23 08:16:20 dell-cssouza LED UDP[31589]: LED Status: Off
May 23 08:16:21 dell-cssouza LED UDP[31589]: LED Status: On
May 23 08:16:21 dell-cssouza LED UDP[31589]: LED Status: Off
May 23 08:16:22 dell-cssouza LED UDP[31589]: LED Status: On
May 23 08:16:23 dell-cssouza LED UDP[31589]: LED Status: Off

MODO RASPBERRY

Para o modo RASPBERRY a cada vez que o botão for pressionado irá alternar o estado do LED.

Monitorando o tráfego usando o tcpdump

Para monitorar as mensagens que trafegam, é necessário ler uma interface que corresponde ao endereço multicast, para saber quais interfaces que o computador possui é utilizado o comando:

$ netstat -ng

Output

IPv6/IPv4 Group Memberships
Interface       RefCnt Group
--------------- ------ ---------------------
lo              1      224.0.0.251
lo              1      224.0.0.1
enp0s31f6       1      239.192.1.1
enp0s31f6       1      224.0.0.251
enp0s31f6       1      224.0.0.1
docker0         1      224.0.0.251
docker0         1      224.0.0.1
docker0         1      224.0.0.106
vboxnet0        1      224.0.0.251
vboxnet0        1      224.0.0.1
lo              1      ff02::fb
lo              1      ff02::1
lo              1      ff01::1
enp0s31f6       1      ff02::1:ff08:a1fb
enp0s31f6       1      ff02::1:ff92:16a6
enp0s31f6       1      ff02::fb
enp0s31f6       1      ff02::1:ff1e:b93
enp0s31f6       1      ff02::1:ff1c:79de
enp0s31f6       1      ff02::1:ffda:d8cd
enp0s31f6       1      ff02::1
enp0s31f6       1      ff01::1
wlp2s0          1      ff02::1
wlp2s0          1      ff01::1
docker0         1      ff02::6a
docker0         1      ff02::1
docker0         1      ff01::1
vboxnet0        1      ff02::fb
vboxnet0        1      ff02::1:ff00:0
vboxnet0        1      ff02::1
vboxnet0        1      ff01::1

Como é possível ver existem diversos grupos multicast disponíveis, no caso dessa máquina em questão podemos verificar que existe o endereço do grupo selecionado para a aplicação na inteface enp0s31f6 com o endereço 239.192.1.1

O tcpdump possui opções que permite a visualização dos dados, não irei explicar tudo, fica de estudo para quem quiser saber mais sobre a ferramenta. Executando o comando podemos ver todas as mensagens de multicast

sudo tcpdump -i enp0s31f6 -nnSX "multicast"

Após executar o comando o tcpdump ficará fazendo sniffing da interface, tudo o que for trafegado nessa interface será apresentado, dessa forma enviando um comando e é possível ver a seguinte saída:

08:21:58.903735 IP 192.168.0.140.38455 > 239.192.1.1.1234: UDP, length 6
	0x0000:  4500 0022 e013 4000 0111 e7c1 c0a8 008c  E.."..@.........
	0x0010:  efc0 0101 9637 04d2 000e b215 4c45 4420  .....7......LED.
	0x0020:  4f4e                                     ON
  • No instante 08:21:58.903735 IP 192.168.0.140.38455 > 239.192.1.1.1234 o cliente envia uma mensagem para o servidor via multicast

Testando conexão com o servidor via netcat

A aplicação realiza a comunicação entre processos locais, para testar uma comunicação remota é usado o netcat que permite se conectar de forma prática ao servidor e enviar os comandos. Para se conectar basta usar o seguinte comando:

nc -u ip port

Como descrito no comando netstat é usado o ip de multicast apresentado na interface enp0s31f6 que é o IP 239.192.1.1, então o comando fica

echo -e "LED ON" | nc -u 239.192.1.1 1234

É enviado o comando LED ON, se visualizar no log irá apresentar que o comando foi executado, para monitorar com o tcpdump basta mudar a interface

Matando os processos

Para matar os processos criados execute o script kill_process.sh

$ cd bin
$ ./kill_process.sh

Conclusão

O multicast é uma boa solução para enviar mensagens de uma única vez para os interessados, diferente do broadcast somente os registrados irão receber as mensagens, evitando assim o congestionamento do tráfego, essa forma de envio se assemelha ao padrão arquitetural publish–subscribe como por exemplo o MQTT.

Referência

IPC(Inter process communication)

UDP Broadcast Socketpair
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Linux Embarcado » UDP Multicast

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: