Além do Código: Redes de Computadores para Desenvolvedores

Maicol KaiserMaicol Kaiser
18 min read

Resumo

Hoje em dia, utilizamos amplamente o conceito de containers, o que nos obriga a compreender melhor essa tecnologia. Consequentemente, isso nos leva a adquirir ao menos um conhecimento básico sobre redes. Este artigo será focado no essencial que você precisa saber sobre redes para se sentir mais à vontade ao lidar com assuntos relacionados a TCP/IP.

Você vai encontrar os códigos apresentados aqui, no repositorio baixo:

https://github.com/vakaman/Alem-do-Codigo-Redes-de-computador-para-desenvolvedores


Por que entender redes é essencial para o sucesso do seu código?


Já de antemão e para ser bem sincero, pro seu código em sí, não muda muita coisa não, talvez em algum momento você tenha alguns insites interessantes por já ter visto uma abstração parecida em algum protocolo de rede.

Isso significa que redes não são um problema seu? Não! e eu posso provar.

Hoje vivemos na era dos containers e nos comunicamos com eles frequentemente através de redes TCP/IP, sendo assim… se você quer se manter longe de problemas quando o assunto é redes vem comigo, mas antes… vamos passar pelo básico.

Antes de começar, um ponto que acho interessante reforçar é… se você se sente confortável com o conhecimento de redes que tem hoje e o que eu falei anteriormente reforça a sua ideia de que você já sabe o suficiente, não continue, talvez não faça sentido de fato para você. Agora… se você quer desbravar algo que ainda não teve a oportunidade de ter contato, eu fico feliz e te desejo uma boa leitura.

Domínios de sub-rede

Imagine um cenário onde um grupo de computadores precisa se comunicar, porém para que isto seja possível, um padrão deve ser definido, uma das regras deste padrão é que cada um que entre neste grupo não possa ter identificadores duplicados. Outra regra importante é que os grupos têm tamanhos fixos e previamente definidos. Com isto em mente vou tentar explicar de uma forma didática o mínimo que você precisa saber para entender o conceito.

Aposto que você já viu algumas destas redes 10.0.0.1/24, 192.168.0.0/24, 172.31.0.0/24, e isto se dá pelo fato de serem redes utilizadas dentro do que chamamos rede local, para os exemplos à seguir vou utiliza o prefixo 192.168.0.0/24, mas o conceito se aplica para todos os prefixos do protocolo IP.

Dentro do prefixo 192.168.0.0/24 temos 255 posições, imagine que cada posição pode ser ocupada por um computador com exceção das reservadas.

Mas… como eu sei que são 255? que define isso? 🤔

A máscara de sub-rede, que neste caso é o /24. Mais um nome estranho né? eu sei…eu sei… calma… vamos por partes. Vamos passar pelos nomes logo abaixo, vai ficar fácil entender, confia.

Hostpode ser muitas coisas, desde sua televisão ao um contâiner do postgresqlUm dispositivo ou um serviço. Todos que tem suporte ao protocolo tcp/ip podem ser considerados um host.
IP192.168.0.0 ou 192.168.0.239 ou 1.1.1.1Um único endereço, a unidade de fato, o identificador de um host em uma rede.
Mascara de sub-rede255.255.255.0 ou simplesmente /24A definição dos limites, onde termina uma rede.
Prefixo ou Rede192.168.0.0/24A junção do IP com a máscara de sub-rede, define os limites da rede com os dois sabemos onde inicia e onde termina o range dos IP’s daquele domínio.

Quando definimos as fronteiras de uma rede, estamos estabelecendo um grupo onde todos os participantes podem se comunicar de forma direta ou seja

O host com o IP 192.168.0.10/24 pode se comunicar com o host 192.168.10.11/24 se a necessidade de ou terceiro elemento. Caso algum destes host queira se comunicar com o endereço 10.0.0.10 ele precisa passar por um roteador, mas… daí é uma longa história, vou tentar ir pelo caminho mais curto.

Se sabemos que o endereço 192.168.0.0/24 possui 255 ips e alguns são reservados, quais eu não posso usar?

NO | 192.168.0.0 → rede ( define o inicio da rede )

NO | 192.168.0.1 → gateway ( normalmente um roteador, ele conecta a rede local com outras redes)

YES | 192.168.0.2-254 → livre para alocação ( posições que podemos utilizar )

NO | 192.168.0.255 → broadcast ( envia mensagens para todos na mesma sub-rede simultaneamente )

Normalmente o que vemos em nossas casas é algo parecido com o desenho acima, onde temos um roteador e diversos hosts, cada um deles contém sua tabela de rotas, onde através dela os hosts sabem qual a rede que eles fazem parte e para quem ele precisa perguntar nos casos onde ele não conhece a rede.

Ok, ok tudo muito bonito e tals, mas… oq isso tem a ver com os containers?

Pois então… os containers fazem parte de uma sub-rede, e eles só conseguem se comunicar se estiverem na mesma rede e com permissão para tal.


O que é e pra que servem as portas?

De forma simplista as portas TCP têm a função de criar um canal de comunicação entre dois hosts. Essa conexão é associada a um processo, mas… se acalme que a gente vai chegar lá e você vai conseguir entender o que isso significa.

Existem 65535 portas TCP disponíveis, entretanto, existe uma organização separando as portas para cada contexto, vamos passar brevemente por esta separação

FUNÇÃORANGEDETALHE
WELL KNOWN PORT0-1023São portas utilizadas por sistemas e ou processos root
REGISTERED PORT1024-49151Portas registradas são usadas no TCP e listadas pela IANA como conveniência, mas não são controladas.
PRIVATE OR EPHEMETAL PORTS49152-65535Atribuídas dinamicamente para conexões temporárias

Vamos simular o cenário abaixo, para fazer com que o host A, crie uma conexão com o host B através das portas TCP.

Como podemos ver, temos um servidor, que chamamos de host_a_server, ele tem um processo PID 1, escutando na porta 6969.

Já do outro lado, temos um segundo elemento que chamamos de host_b_client, que deverá iniciar uma requisição para o servidor com destino a porta 6969 do servidor e para que a conexão seja estabelecida uma porta de origem é associada a esta conexão.

Servidor

import socket
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

HOST = '0.0.0.0'
PORT = 6969

logging.info("Iniciando servidor...")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(1)

logging.info(f"Servidor escutando na porta {PORT}...")

try:
    while True:
        connection, client_address = server_socket.accept()
        logging.info(f"Conexão estabelecida com {client_address}")

        data = connection.recv(1024).decode('utf-8')
        if data:
            logging.info(f"Mensagem recebida: {data}")
            connection.sendall(
                "Olá, cliente! Conexão estabelecida.".encode('utf-8')
            )
        connection.close()
        logging.info(f"Conexão com {client_address} encerrada.")
except KeyboardInterrupt:
    logging.info("Servidor interrompido manualmente.")
finally:
    server_socket.close()
    logging.info("Servidor finalizado.")

Cliente

import socket
import logging
import os

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)


HOST = os.getenv("SERVER_HOST", "host_a_server")
PORT = 6969

logging.info(f"Tentando conectar ao servidor {HOST}:{PORT}...")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    client_socket.connect((HOST, PORT))
    logging.info(f"Cliente conectado ao servidor {HOST}:{PORT}")

    client_socket.sendall("Olá, servidor!".encode('utf-8'))
    logging.info("Mensagem enviada ao servidor.")

    data = client_socket.recv(1024).decode('utf-8')
    logging.info(f"Resposta do servidor: {data}")
finally:
    client_socket.close()
    logging.info("Conexão com o servidor encerrada.")

Assim que rodamos o projeto, vamos ver este comportamento.

host_a_server  | 2025-01-19 13:05:16,795 - INFO - Iniciando servidor...
host_a_server  | 2025-01-19 13:05:16,795 - INFO - Servidor escutando na porta 6969...

host_b_client  | 2025-01-19 13:05:17,028 - INFO - Tentando conectar ao servidor host_a_server:6969...
host_b_client  | 2025-01-19 13:05:17,029 - INFO - Cliente conectado ao servidor host_a_server:6969
host_b_client  | 2025-01-19 13:05:17,029 - INFO - Mensagem enviada ao servidor.

host_a_server  | 2025-01-19 13:05:17,029 - INFO - Conexão estabelecida com ('1.2.9.3', 44890)

host_b_client  | 2025-01-19 13:05:17,029 - INFO - Resposta do servidor: Olá, cliente! Conexão estabelecida.
host_b_client  | 2025-01-19 13:05:17,029 - INFO - Conexão com o servidor encerrada.

host_a_server  | 2025-01-19 13:05:17,029 - INFO - Mensagem recebida: Olá, servidor!
host_a_server  | 2025-01-19 13:05:17,029 - INFO - Conexão com ('1.2.9.3', 44890) encerrada.

Podemos ver no container do servidor, que o mesmo está com uma porta listen no endereço 0.0.0.0, ou seja, a porta 6969 está disponível em qualquer uma das interfaces que o servidor possuir.

root@host_a_server:/app# netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:6969            0.0.0.0:*               LISTEN

Um ponto importante que vale ressaltar é que no contexto TCP/IP, precisamos entender que existem IP’s de origem e destino e portas de origem e destino para uma conexão estabelecida, ou seja, o IP e porta de origem de um host se comunica com o IP e porta de destino de outro host, esse processo acontece através do 3-way-handshake, mas não vou evoluir o assunto para este caminho, em todo caso se quiser mais detalhes, deixei nas referências um material falando do assunto.


Vamos falar de docker

Aposto que você já teve algum contato com docker, porém mágicamente meio que… em um cenário ideal, tudo meio que funciona certo?

Vamos pensar em um cenário onde temos uma aplicação, seja ela em PHP, Python, Js, tanto faz, vamos chamar ela de APP, um banco de dados, neste caso vou chamar de DB, e um serviço de cache, que simplesmente vou chamar de cache, o docker-compose dele será algo parecido com isso.

services:
    app:
        image: nginx:latest
        container_name: app
    db:
        image: mysql:latest
        container_name: db
        environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: app
            MYSQL_USER: bad_user
            MYSQL_PASSWORD: bad_pass
    cache:
        image: redis:latest
        container_name: cache

Você vai perceber que se tentar subir estes serviços, cada um de forma isolada vai funcionar, porém não vão conseguir se comunicar, isso porque para que os serviços possam se comunicar, precisamos que eles façam parte da mesma rede, lembra?

Verificando a rede dos containers

Assim que subimos os serviços, cada um dos hosts/containers vão ter seu próprio endereço de IP, vamos verificar qual endereço foram associados utilizando o comando do docker em conjunto com o jq abaixo:

## Apenas comando do docker
docker container inspect app

## Comando do docker filtrando com jq a configuração de Network
docker container inspect app | jq '.[0].NetworkSettings'

Utilizando o comando acima, conseguimos ver que o endereço que o host (app) recebeu foi o endereço:

HOST: 1.2.7.2/24

{
  "Bridge": "",
  "SandboxID": "210f0e9ba9e77201d7e0f3a2948ce31826353c4727d8f45c0bcabb6bf5b020ac",
  "SandboxKey": "/var/run/docker/netns/210f0e9ba9e7",
  "Ports": {
    "80/tcp": null
  },
  "HairpinMode": false,
  "LinkLocalIPv6Address": "",
  "LinkLocalIPv6PrefixLen": 0,
  "SecondaryIPAddresses": null,
  "SecondaryIPv6Addresses": null,
  "EndpointID": "",
  "Gateway": "",
  "GlobalIPv6Address": "",
  "GlobalIPv6PrefixLen": 0,
  "IPAddress": "",
  "IPPrefixLen": 0,
  "IPv6Gateway": "",
  "MacAddress": "",
  "Networks": {
    "redes-docker_default": {
      "IPAMConfig": null,
      "Links": null,
      "Aliases": [
        "app",
        "app"
      ],
      "MacAddress": "02:42:01:02:07:02",
      "DriverOpts": null,
      "NetworkID": "961d9113f94092827308148af99793a9ea3d471716a14979b3dcce6683cd5b66",
      "EndpointID": "52e065cd20596fae04409ad82488297ba659c483b42527df4445e3b595533cae",
      "Gateway": "1.2.7.1",
      "IPAddress": "1.2.7.2",
      "IPPrefixLen": 24,
      "IPv6Gateway": "",
      "GlobalIPv6Address": "",
      "GlobalIPv6PrefixLen": 0,
      "DNSNames": [
        "app",
        "5ec26cac5ce0"
      ]
    }
  }
}

Continuando a inspeção, chegamos no seguinte resultado.

ServiçoIP/PREFIXGATEWAY
app1.2.7.2/241.2.7.1
db1.2.7.3/241.2.7.1
cache1.2.7.4/241.2.7.1

Ou seja

Ok, ok… então significa que a aplicação consegue falar com o banco de dados e consequentemente a aplicação também consegue falar com o cache, pois os mesmos estão na mesma rede certo?

Exatamente isso 👏👏👏, obviamente que para o contexto do docker existem restrições a nível de firewall que podem fazer com que os hosts não se comuniquem, mas… com o docker-compose.yaml que eu utilizei e com o Docker Compose version v2.20.3 e o docker Docker version 27.3.1, build ce12230. Assim que eu subi a aplicação, o docker utilizou a rede → redes-docker_default que está em modo bridge, permitindo a comunicação entre os serviços que fazem parte desta rede.

para inspecionar essa rede você pode utilizar o comando

~ docker network inspect redes-docker_default | jq '.[0].IPAM'
{
  "Driver": "default",
  "Options": null,
  "Config": [
    {
      "Subnet": "1.2.7.0/24",
      "Gateway": "1.2.7.1"
    }
  ]
}

Ao tentar pingar, do host da aplicação para o host do banco de dados, percebemos que existe uma resposta. Chegamos a conclusão de que a comunicação existe pois o ping informa quantos pacotes foram transmitidos e quantos destes foram recebidos.

### Acessando o container
~ docker exec -it app bash

### Testando a comunicação
root@app:/# apt-get install -y iputils-ping
root@app:/# ping db
PING db (1.2.7.3) 56(84) bytes of data.
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=1 ttl=64 time=0.184 ms
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=2 ttl=64 time=0.077 ms
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=3 ttl=64 time=0.064 ms
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=4 ttl=64 time=0.076 ms
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=5 ttl=64 time=0.076 ms
64 bytes from db.redes-docker_default (1.2.7.3): icmp_seq=6 ttl=64 time=0.075 ms
^C
--- db ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5138ms
rtt min/avg/max/mdev = 0.064/0.092/0.184/0.041 ms

Em algumas situações, os hosts negam as requisições ICMP, fazendo com que o ping seja retornado, neste caso podemos utilizar o telnet, assim validamos se existe algum serviço escutando na porta informada.

root@app:/# telnet db 3306
Trying 1.2.7.3...
Connected to db.
Escape character is '^]'.
I
5}lNTcaching_sha2_password2#08S01Got timeout reading communication packetsConnection closed by foreign host.

Mais uma forma de testar conectividade é utilizando o nmap. Com ele podemos não só testar uma porta, mas também quais as portas abertas de um host.

root@app:/# nmap -p 3306 db
Starting Nmap 7.93 ( https://nmap.org ) at 2025-01-18 00:11 UTC
Nmap scan report for db (1.2.7.3)
Host is up (0.00011s latency).
rDNS record for 1.2.7.3: db.redes-docker_default

PORT     STATE SERVICE
3306/tcp open  mysql
MAC Address: 02:42:01:02:07:03 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 0.30 seconds

Bora aprofundar?

Sendo bem transparente com você, o básico já passou e talvez… apenas com o que eu falei acima já dá pra ter uma boa noção de como a rede dos containers funciona e também, arrisco a dizer que… é um básico do básico de redes. Os mais entendidos do assunto sabem que tem bastante coisa pra falar ainda, mas… agora vamos dar uma aprofundada 😉👌

Domínios de broadcast

Assim como no mundo da camada 3 do modelo TCP/IP existe um identificador único que chamamos de IP e definimos uma rede através dos prefixos, fazendo com que os hosts participantes das redes possam se comunicar, na camada 2 também existe um identificador único que chamamos de MAC ou endereço físico e também podem se comunicar MAC’s que participam do domínio de broadcast. Partindo disso vamos ver os mesmos hosts de antes de uma perspectiva diferente.

Primeiramente, vamos ver todas as interfaces que fazem parte do domínio de broadcast da rede → redes-docker_default, para isso precisamos saber qual bridge foi criada pelo docker.

docker network ls | grep redes-docker_default
NETWORK ID     NAME                         DRIVER    SCOPE
961d9113f940   redes-docker_default         bridge    local

Com o comando acima, chegamos no NETWORK ID 961d9113f940, o docker cria uma bridge com o mesmo id, porém com o prefixo “br-”, conseguimos ver as interfaces virtuais associadas a bridge criada com o seguinte comando.

> ip link show master br-961d9113f940
39: vethbf30c22@if38: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-961d9113f940 state UP mode DEFAULT group default
    link/ether b6:62:83:bd:3c:88 brd ff:ff:ff:ff:ff:ff link-netnsid 0
43: veth512189f@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-961d9113f940 state UP mode DEFAULT group default
    link/ether ce:c0:66:fb:7a:a9 brd ff:ff:ff:ff:ff:ff link-netnsid 2
45: vethb57ac2a@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-961d9113f940 state UP mode DEFAULT group default
    link/ether 5a:6a:f4:78:1c:9d brd ff:ff:ff:ff:ff:ff link-netnsid 1

Assim chegamos nos seguintes endereços fisicos associados a bridge.

b6:62:83:bd:3c:88

ce:c0:66:fb:7a:a9

5a:6a:f4:78:1c:9d

Bora descobrir quem é cada um?

Primeiramente precisamos entender que o docker relaciona uma interface virtual externa ao container com uma interface física dentro do container através do veth pair, onde uma espécie de túnel é criado, relacionando essas duas interfaces.

Vamos acessar o container e verificar qual o endereço mac está associado a ela e o indice externo que ela recebeu.

❯ docker exec -it app bash
root@5ec26cac5ce0:/# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
38: eth0@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:01:02:07:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0

Podemos ver que o mac do host app internamente é 02:42:01:02:07:02 e o índice associado a ele é o 39, podemos ver isto pois logo após o eth0 temos o @if39. Com esta informação em mente, vamos sair do container e verificar qual é a interface associada a bridge que está relacionada a interface do container.

Primeiramente, vamos localizar a interface com o índice 39, e então partindo da informação da interface virtual, vamos chegar até o endereço mac associado a ela.

### Localizando interface
ip link | grep "^39:"
39: vethbf30c22@if38: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-961d9113f940 state UP mode DEFAULT group default

### Verificando o MAC da interface
ip link show dev vethbf30c22
39: vethbf30c22@if38: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-961d9113f940 state UP mode DEFAULT group default
    link/ether b6:62:83:bd:3c:88 brd ff:ff:ff:ff:ff:ff link-netnsid 0

Assim conseguimos descobrir que, o endereço MAC b6:62:83:bd:3c:88 é na verdade do container app, e que o endereço mac que aparece na bridge na verdade está fazendo o papel de conectar a interface externa com o endereço físico b6:62:83:bd:3c:88 a interface eth0 interna com o endereço 02:42:01:02:07:02

Caso você queira inspecionar diretamente a bridge que foi gerenciada pelo docker, podemos utilizar o comando abaixo

> docker network inspect redes-docker_default | jq .[0].Containers

O resultado será o seguinte

{
  "4a51cc332df584771fd3ec8d5c229073b1971d2167d3385cd9376ee0b7761a04": {
    "Name": "db",
    "EndpointID": "2be625310559ed29d7d9571f30f43668cc86c0f14612eb0e90ebffd1d51d88e3",
    "MacAddress": "02:42:01:02:07:03",
    "IPv4Address": "1.2.7.3/24",
    "IPv6Address": ""
  },
  "5ec26cac5ce0ddbb299aca8368b7ca77d9149b1ea378e5949b9092c4fa93e586": {
    "Name": "app",
    "EndpointID": "52e065cd20596fae04409ad82488297ba659c483b42527df4445e3b595533cae",
    "MacAddress": "02:42:01:02:07:02",
    "IPv4Address": "1.2.7.2/24",
    "IPv6Address": ""
  },
  "ad888d9e5cbd6a84d868d574b94744d16ef853711e5c4dcbaad4007e07eebd31": {
    "Name": "cache",
    "EndpointID": "ad7604bd382e4f7480303a1b2ca6e9f414af7b3fa54af8c4e5646c35af8a6bdf",
    "MacAddress": "02:42:01:02:07:04",
    "IPv4Address": "1.2.7.4/24",
    "IPv6Address": ""
  }
}

Assim conseguimos ver todos os MAC’s associados aos containers.


Internet das Coisas (IoT)

Para os Dev’s que chegaram até aqui… bora falar de uma coisa muito legal que é internet das coisas, e… eu aposto que você em algum momento já se perguntou… ué? mas como que o meu celular sabe que a minha TV está na minha casa? ou… como a Alexa sabe que existe uma lâmpada ou uma câmera de vigilância? Pois então meu amigo… o mundo do dev que conhece redes também é muito legal, vem comigo.

Primeiramente a gente precisa entender que não é somente o broadcast que ajuda a gente com o processo de descoberta, afinal o broadcast tem como funcionalidade principal, enviar uma mensagem para todos os participantes de uma rede.

Algumas estratégias envolvem:

Broadcast

Multicast

UPNP → SSDP (Simple Service Discovery Protocol)

Para a descoberta usando Broadcast

Neste exemplo abaixo, vamos fazer com que dois hosts se identifiquem através de broadcast, vou utilizar Python, mas tenho certeza que você vai encontrar algo na sua linguagem de preferência.

docker-compose.yaml

services:
  device1:
    build: .
    container_name: device1
    environment:
      - DEVICE_NAME=Device1
    networks:
      iot_network:
        ipv4_address: 192.168.1.10

  device2:
    build: .
    container_name: device2
    environment:
      - DEVICE_NAME=Device2
    networks:
      iot_network:
        ipv4_address: 192.168.1.11

networks:
  iot_network:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.1.0/24

dockerfile

FROM python:3.9-slim

WORKDIR /app
COPY broadcast.py /app/broadcast.py
RUN pip install --no-cache-dir --upgrade pip

CMD ["python", "/app/broadcast.py"]

broadcast.py

import socket
import threading
import time

BROADCAST_IP = "255.255.255.255"
PORT = 5005
DEVICE_NAME = "IoT Device"

discovered_devices = set()
stop_discovery = threading.Event()


def broadcast_sender():
    with socket.socket(
        socket.AF_INET,
        socket.SOCK_DGRAM,
        socket.IPPROTO_UDP
    ) as sender_socket:
        sender_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        while not stop_discovery.is_set():
            message = f"DISCOVER from {DEVICE_NAME}"
            print(f"[{DEVICE_NAME}] Enviando broadcast: {message}")
            sender_socket.sendto(message.encode(), (BROADCAST_IP, PORT))
            time.sleep(5)


def broadcast_receiver():
    with socket.socket(
        socket.AF_INET,
        socket.SOCK_DGRAM,
        socket.IPPROTO_UDP
    ) as receiver_socket:
        receiver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        receiver_socket.bind(("", PORT))
        print(f"[{DEVICE_NAME}] Aguardando mensagens de broadcast "
              f"na porta {PORT}...")

        while not stop_discovery.is_set():
            message, address = receiver_socket.recvfrom(1024)
            decoded_message = message.decode()
            sender_ip = address[0]

            print(f"[{DEVICE_NAME}] Recebeu mensagem de {sender_ip}: "
                  f"{decoded_message}")

            if (sender_ip not in discovered_devices and
                    "DISCOVER" in decoded_message):
                discovered_devices.add(sender_ip)
                print(f"[{DEVICE_NAME}] Dispositivo descoberto: {sender_ip}")

                response = f"Hello from {DEVICE_NAME}!"
                print(f"[{DEVICE_NAME}] Respondendo a {sender_ip}: {response}")
                receiver_socket.sendto(response.encode(), address)

                stop_discovery.set()


if __name__ == "__main__":
    threading.Thread(target=broadcast_receiver, daemon=True).start()
    broadcast_sender()

O retorno da execução do exemplo acíma vai ser o seguinte

device2  | [IoT Device] Enviando broadcast: DISCOVER from IoT Device
device2  | [IoT Device] Aguardando mensagens de broadcast na porta 5005...
device2  | [IoT Device] Recebeu mensagem de 192.168.1.11: DISCOVER from IoT Device
device2  | [IoT Device] Dispositivo descoberto: 192.168.1.11
device2  | [IoT Device] Respondendo a 192.168.1.11: Hello from IoT Device!

device1  | [IoT Device] Enviando broadcast: DISCOVER from IoT Device
device1  | [IoT Device] Aguardando mensagens de broadcast na porta 5005...
device1  | [IoT Device] Recebeu mensagem de 192.168.1.10: DISCOVER from IoT Device
device1  | [IoT Device] Dispositivo descoberto: 192.168.1.10
device1  | [IoT Device] Respondendo a 192.168.1.10: Hello from IoT Device!

device2 exited with code 0
device1 exited with code 0

Para descoberta UPNP

dockerfile

FROM python:3.9-slim

WORKDIR /app

COPY upnp_discovery.py /app/upnp_discovery.py

RUN pip install --no-cache-dir --upgrade pip

CMD ["python", "/app/upnp_discovery.py"]

docker-compose.yaml

services:
  device3:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: device3
    environment:
      - DEVICE_NAME=Device3

  device4:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: device4
    environment:
      - DEVICE_NAME=Device4

networks:
  default:
    driver: bridge

upnp_discovery.py

import socket
import threading
import time

MULTICAST_IP = "239.255.255.250"
PORT = 1900
DEVICE_NAME = "UPnP Device"
SERVICE_TYPE = "ssdp:all"
MX = 2

discovered_devices = set()
stop_discovery = threading.Event()


def ssdp_sender():
    with socket.socket(
        socket.AF_INET,
        socket.SOCK_DGRAM,
        socket.IPPROTO_UDP
    ) as sender_socket:
        sender_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
        message = (
            "M-SEARCH * HTTP/1.1\r\n"
            f"HOST: {MULTICAST_IP}:{PORT}\r\n"
            "MAN: \"ssdp:discover\"\r\n"
            f"ST: {SERVICE_TYPE}\r\n"
            f"MX: {MX}\r\n"
            "\r\n"
        )
        while not stop_discovery.is_set():
            print(
                f"[{DEVICE_NAME}] Enviando mensagem SSDP:\n{message.strip()}"
            )
            sender_socket.sendto(message.encode(), (MULTICAST_IP, PORT))
            time.sleep(5)


def ssdp_receiver():
    with socket.socket(
        socket.AF_INET,
        socket.SOCK_DGRAM,
        socket.IPPROTO_UDP
    ) as receiver_socket:
        receiver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        receiver_socket.bind(("", PORT))

        mreq = socket.inet_aton(MULTICAST_IP) + socket.INADDR_ANY.to_bytes(
            4,
            "big"
        )
        receiver_socket.setsockopt(
            socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq
        )

        print(f"[{DEVICE_NAME}] Aguardando respostas SSDP na porta {PORT}...")

        while not stop_discovery.is_set():
            try:
                message, address = receiver_socket.recvfrom(1024)
                sender_ip = address[0]
                decoded_message = message.decode()

                if sender_ip not in discovered_devices:
                    discovered_devices.add(sender_ip)
                    print(f"[{DEVICE_NAME}] Resposta recebida de {sender_ip}:\n{decoded_message.strip()}")
                    stop_discovery.set()
            except socket.timeout:
                pass


if __name__ == "__main__":
    threading.Thread(target=ssdp_receiver, daemon=True).start()
    ssdp_sender()

A resposta do script acima vai se parecer com esta abaixo, informando que os dispositivos passaram pelo processo de discover.

device4  | [UPnP Device] Aguardando respostas SSDP na porta 1900...
device4  | [UPnP Device] Enviando mensagem SSDP:
device4  | M-SEARCH * HTTP/1.1
device4  | HOST: 239.255.255.250:1900
device4  | MAN: "ssdp:discover"
device4  | ST: ssdp:all
device4  | MX: 2
device4  | [UPnP Device] Resposta recebida de 1.2.8.3:
device4  | M-SEARCH * HTTP/1.1
device4  | HOST: 239.255.255.250:1900
device4  | MAN: "ssdp:discover"
device4  | ST: ssdp:all
device4  | MX: 2
device4 exited with code 0


device3  | [UPnP Device] Enviando mensagem SSDP:
device3  | M-SEARCH * HTTP/1.1
device3  | HOST: 239.255.255.250:1900
device3  | MAN: "ssdp:discover"
device3  | ST: ssdp:all
device3  | MX: 2
device3  | [UPnP Device] Aguardando respostas SSDP na porta 1900...
device3  | [UPnP Device] Enviando mensagem SSDP:
device3  | M-SEARCH * HTTP/1.1
device3  | HOST: 239.255.255.250:1900
device3  | MAN: "ssdp:discover"
device3  | ST: ssdp:all
device3  | MX: 2
device3  | [UPnP Device] Resposta recebida de 1.2.8.2:
device3  | M-SEARCH * HTTP/1.1
device3  | HOST: 239.255.255.250:1900
device3  | MAN: "ssdp:discover"
device3  | ST: ssdp:all
device3  | MX: 2
device3 exited with code 0

Conclusão

O mundo do desenvolvimento não se limita às fronteiras do código, se permita aprofundar e olhar o que existe embaixo do capô, tenho certeza de que quanto mais deep você se deixar ir, mais apaixonado você vai ficar com o que a tecnologia tem pra te mostrar. Não sabemos e nunca vamos saber de tudo, mas… se deixe desbravar e se veja como um eterno aprendiz, alguém em constante evolução, pronto para transformar curiosidade em conhecimento e desafios em oportunidades. Afinal, a magia da tecnologia está justamente em sua infinita capacidade de nos surpreender e ensinar algo novo a cada dia.

Muito obrigado por ter chegado até aqui…


Referências

0
Subscribe to my newsletter

Read articles from Maicol Kaiser directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maicol Kaiser
Maicol Kaiser

I am a systems architect with solid experience in developing scalable and high-performance solutions. My work primarily involves cloud technologies, advanced networking (BGP, OSPF, VRF), and relational and non-relational databases. I specialize in designing and implementing RESTful APIs, following best practices like SOLID, Clean Architecture, and DDD to ensure clean, maintainable code. My main stack includes PHP (Symfony), Java, Python, and JavaScript, as well as containerization tools like Docker and Kubernetes. I also work with distributed messaging queues (SQS, RabbitMQ, Kafka) for event-driven systems. I'm always focused on developing robust and reliable solutions, applying rigorous testing methodologies (TDD), and optimizing every aspect of the applications I build.