UDP Broadcast

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, para exemplificar tome a televisão como exemplo, o sinal de TV é transmitido pelo ar onde qualquer televisão sintonizada nessa determinada frequência pode captar o programa transmitido, esse tipo de aplicação seria inviável se o sinal fosse enviado para cada televisor existente. O broadcast está presente somente no protocolo IPv4, no IPv6 é usado uma outra técnica conhecida como multicast. O broadcast é usado no protocolo ARP(Address Resolution Protocol) que mapeia o endereço físico, o endereço MAC.

Endereço Broadcast

Para entender o endereço broadcast é adotado ip da classe C onde o primeiro octeto tem um range de 192 até 223, normalmente as redes domésticas utilizam essa classe como por exemplo 192.168.0.XXX. Na classe C quando a rede é descrita na forma 192.168.0.XXX, não devemos usar os valores 0 e 255, onde 0 representa a rede e o 255 representa o endereço broadcast dessa rede, sendo assim se uma mensagem for enviada para esse endereço todas as máquinas conectadas nessa rede irá receber a mensagem.

Representação do Broadcast na rede

Quando uma mensagem é enviada para esse endereço, todas as máquinas irão receber a mensagem mesmo que não esteja interessada. Para ilustrar, o exemplo representa uma mensagem broadcast enviada em uma rede classe B

Na imagem é possível notar que as mensagem não é propagada pelo roteador

Identificando o endereço de broadcast

Para identificar qual o endereço broadcast da rede pode ser usado o comando ip

$ ip a

Nas interfaces do computador é possível notar o ip que é atribuído para a máquina e em seguida o endereço de broadcast logo a frente do acrônimo brd(broadcast), esse é o endereço que é usado para enviar mensagens broadcast

....
2: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 10:65:30:22:8a:1a brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.140/24 brd 192.168.0.255 scope global dynamic noprefixroute enp0s31f6
       valid_lft 4736sec preferred_lft 4736sec
....

Obs: Durante o exemplo é necessário inserir esse endereço correspondente a rede em que está rodando o exemplo no descritor usado em button_process

Preparação do Ambiente

Antes de apresentarmos 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 com 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, iremos utilizar o modelo Cliente/Servidor, onde o processo Cliente(button_process) vai enviar uma mensagem via broadcast para o Servidor(led_process) que vai ler a mensagem e verificar se corresponde com os comandos cadastrados internamente, aplicando 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_broadcast_receiver.h

Primeiramente é criado um callback responsável por 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 e o callback para recepção da mensagem

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

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

bool UDP_Broadcast_Receiver_Init(UDP_Receiver *receiver);

Essa função aguarda uma mensagem enviada pelo cliente.

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

udp_broadcast_receiver.c

No UDP_Broadcast_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 e uma estrutura sockaddr_in que é usada para configurar o servidor para se comunicar através da rede.

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

Para realizar a inicialização é criado um dummy do while, para que quando houver falha em qualquer uma das etapas 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;

Neste ponto é aplicado as configurações ao socket criado e é atribuído true na variável status

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

Na função UDP_Broadcast_Receiver_Run é declarado algumas variáveis para receber as mensagens por meio do broadcast

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 aguarda o envio de uma mensagem do cliente, a mensagem é passada para o callback realizar o tratamento de acordo com a aplicação do cliente, e o status é retornado.

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_broadcast_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 broadcast onde vão ser enviadas as mensagens e o port que recebe o número que corresponde qual o serviço deseja consumir

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

Inicializa o cliente com os parâmetros preenchidos no descritor

bool UDP_Broadcast_Sender_Init(UDP_Sender *sender);

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

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

udp_broadcast_sender.c

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

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

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

    broadcast_enable = 1;
    if(setsockopt(sender->socket, SOL_SOCKET, SO_BROADCAST, (void *)&broadcast_enable, sizeof(broadcast_enable)) < 0)
        break;

    status = true;        
}while(false);

return status;

Na função UDP_Broadcast_Sender_Send é definido 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));

Aqui é realizado o envio da mensagem para o endereço broadcast

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 enviar os comandos

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 o programa fica em loop aguardando o pressionamento do botão para alterar o estado da variável e enviar a mensagem para o IP broadcast

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

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

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

    while (true)
    {
        wait_press(button);
        state ^= 0x01;
        UDP_Broadcast_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 o programa 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_Broadcast_Receiver_Init(receiver) == false) 
		return false;


	while(true)
	{
		UDP_Broadcast_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 broadcast, o serviço que deseja consumir, e assim passamos os argumentos para Button_Run iniciar o processo.

UDP_Sender sender = 
{
    .hostname = "192.168.0.255",
    .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 onde vai servir e o callback preenchido, e assim passamos os argumentos para LED_Run iniciar o serviço.

UDP_Server server = 
{
    .buffer = server_buffer,
    .buffer_size = BUFFER_SIZE,
    .port = 1234,
    .on_receive_message = on_receive_message
};

LED_Run(&server, &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_Broadcast
$ cd Raspberry_IPC_Socket_UDP_Broadcast
$ mkdir build && cd build

Selecionando o modo

Para selecionar o modo devemos 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  30226  2298  0 16:27 pts/12   00:00:00 ./button_process
cssouza  30227  2298  0 16:27 pts/12   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/broadcast_fifo

Output do LOG quando enviado o comando algumas vezez

May 21 16:34:26 dell-cssouza LED UDP[30227]: LED Status: On
May 21 16:34:27 dell-cssouza LED UDP[30227]: LED Status: Off
May 21 16:34:27 dell-cssouza LED UDP[30227]: LED Status: On
May 21 16:34:28 dell-cssouza LED UDP[30227]: LED Status: Off
May 21 16:34:28 dell-cssouza LED UDP[30227]: LED Status: On
May 21 16:34:29 dell-cssouza LED UDP[30227]: 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, precisamos ler uma interface, para saber quais interfaces que o computador possui usamos o comando

$ ip a

Output

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 10:65:30:22:8a:1a brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.140/24 brd 192.168.0.255 scope global dynamic noprefixroute enp0s31f6
       valid_lft 4736sec preferred_lft 4736sec
    inet6 2804:6828:c07d:3800:8e1:7295:eb55:6dc1/64 scope global temporary dynamic 
       valid_lft 296sec preferred_lft 296sec
    inet6 2804:6828:c07d:3800:dcee:5cbc:c056:32a2/64 scope global temporary deprecated dynamic 
       valid_lft 296sec preferred_lft 0sec
    inet6 2804:6828:c07d:3800:72d1:f865:c51c:79de/64 scope global dynamic mngtmpaddr noprefixroute 
       valid_lft 296sec preferred_lft 296sec
    inet6 fe80::3b0:2187:f4da:d8cd/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: wlp2s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 7c:2a:31:df:f0:02 brd ff:ff:ff:ff:ff:ff
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:15:e4:fe:cc brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
5: vboxnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 172.16.11.100/24 brd 172.16.11.255 scope global vboxnet0
       valid_lft forever preferred_lft forever
    inet6 fe80::800:27ff:fe00:0/64 scope link 
       valid_lft forever preferred_lft forever

Como podemos ver temos 5 interfaces no computador onde o comando foi executado, pode ser que a máquina que esteja usando possa ter mais interfaces ou menos interfaces. Para teste local, iremos usar a interface local denominada lo, que representa a interface de loopback.

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 broadcast

sudo tcpdump -i enp0s31f6 -nnSX "broadcast"

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

16:30:53.482390 IP 192.168.0.140.39611 > 192.168.0.255.1234: UDP, length 7
	0x0000:  4500 0023 462e 4000 4011 71c0 c0a8 008c  E..#F.@.@.q.....
	0x0010:  c0a8 00ff 9abb 04d2 000f 82fc 4c45 4420  ............LED.
	0x0020:  4f46 46                                  OFF

Podemos ver que não há o processo de handshake somente o envio da mensagem, como descrito a seguir:

  • No instante 16:30:53.482390 IP 192.168.0.140.39611 > 192.168.0.255.1234 o cliente envia uma mensagem para o server via broadcast

Testando conexão com o servidor via netcat

A aplicação realiza a comunicação entre processos locais, para testar uma comunicação remota usaremos 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 ip usaremos o ip de broadcast apresentado na interface enp0s31f6 que é o IP 192.168.0.255, então o comando fica

 echo -e "LED ON" | nc -b -u  192.168.0.255 1234

E enviamos 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 broadcast é uma boa solução para enviar mensagens de uma única vez para todas as máquinas, porém dependendo da frequência em que essa mensagem é disseminada pode causar congestionamento na rede, causando uma queda de desempenho, e enviando mensagens para máquinas que não estejam interessados nesses dados. Para resolver esse problema existe um modo de envio conhecido como Multicast que será abordado no próximo artigo.

Referência

IPC(Inter process communication)

Socket UDP UDP Multicast
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 Broadcast

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: