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.





