Skip to content

MC833 - Programação com Sockets

Objetivos do Laboratório

  • Aprender a utilizar diferentes tipos de sockets em Python.
  • Compreender a comunicação entre processos utilizando Sockets TCP e UDP.
  • Implementar exemplos práticos de Sockets para troca de dados.
  • Explorar o funcionamento de Raw Sockets e suas aplicações.
  • Desenvolver habilidades para depurar e testar aplicações de rede.
  • Analisar as diferenças entre os tipos de sockets e suas utilizações em cenários reais.

Sockets TCP - Aplicação de Chat Cliente e Servidor

Nesta seção, descreveremos uma aplicação de chat simples utilizando sockets TCP, baseada nos arquivos server_tcp.py e client_tcp.py. A aplicação permite que múltiplos clientes se conectem a um servidor e troquem mensagens em tempo real.

Funcionamento Geral

A aplicação é composta por um servidor que escuta conexões de clientes e um cliente que se conecta ao servidor. Quando um cliente envia uma mensagem, o servidor a recebe e a retransmite para todos os clientes conectados. Isso permite que todos os participantes do chat vejam as mensagens enviadas.

Código do Servidor (server_tcp.py)

O código do servidor é responsável por:

  1. Criar um socket TCP e vinculá-lo a um endereço IP e porta.
  2. Escutar por conexões de clientes.
  3. Aceitar conexões e adicionar clientes a uma lista.
  4. Receber mensagens de clientes e retransmiti-las para todos os clientes conectados.
import socket
import threading

# Função para lidar com a comunicação com um cliente
def handle_client(client_socket, clients):
    while True:
        try:
            message = client_socket.recv(1024).decode('utf-8')
            if message:
                print(f"Mensagem recebida: {message}")
                broadcast(message, clients, client_socket)
            else:
                break
        except:
            break

    client_socket.close()

# Função para retransmitir mensagens para todos os clientes
def broadcast(message, clients, sender_socket):
    for client in clients:
        if client != sender_socket:
            client.send(message.encode('utf-8'))

# Configuração do servidor
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 12345))
server.listen(5)
print("Servidor escutando na porta 12345...")

clients = []

while True:
    client_socket, addr = server.accept()
    print(f"Conexão aceita de {addr}")
    clients.append(client_socket)
    threading.Thread(target=handle_client, args=(client_socket, clients)).start()

O código implementa um servidor de chat simples utilizando sockets TCP e múltiplas threads em Python. Ele permite que vários clientes se conectem simultaneamente ao servidor através da porta 12345. Cada nova conexão é tratada em uma thread separada, possibilitando comunicação paralela entre diversos clientes. Quando um cliente envia uma mensagem:

  • O servidor recebe essa mensagem.
  • Exibe no terminal.
  • Reenvia automaticamente para todos os outros clientes conectados (modelo de broadcast).

O servidor permanece em execução contínua, aguardando novas conexões.

  • 🔌 Cria um servidor TCP.
  • 👥 Mantém uma lista de clientes conectados.
  • 🧵 Usa threads para atender múltiplos clientes ao mesmo tempo.
  • 📤 Distribui mensagens entre todos os participantes do chat.

📌 Em resumo

Trata-se de um servidor de chat multicliente básico, que permite comunicação em tempo real entre vários usuários conectados à mesma rede, utilizando programação concorrente com threads.

Código do Cliente (client_tcp.py)

O código do cliente é responsável por:

  1. Conectar-se ao servidor.
  2. Enviar mensagens digitadas pelo usuário para o servidor.
  3. Receber mensagens do servidor e exibi-las na tela.
import socket

def cliente():
    host = '127.0.0.1'  # Endereço IP do servidor
    porta = 12345       # Porta do servidor

    cliente_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    cliente_socket.connect((host, porta))

    print(f"Conectado ao servidor em {host}:{porta}")

    while True:
        mensagem_cliente = input("Cliente: ")
        cliente_socket.send(mensagem_cliente.encode('utf-8'))
        if mensagem_cliente.lower() == 'sair':
            print("Encerrando conexão com o servidor.")
            break
        mensagem_servidor = cliente_socket.recv(1024).decode('utf-8')
        if mensagem_servidor.lower() == 'sair':
            print("Servidor encerrou a conexão.")
            break
        print(f"Servidor: {mensagem_servidor}")

    cliente_socket.close()

if __name__ == "__main__":
    cliente()

O código implementa um cliente de chat TCP em Python, responsável por se conectar a um servidor e permitir a troca de mensagens. Ele estabelece conexão com um servidor local (127.0.0.1) na porta 12345. Após a conexão, o cliente entra em um loop contínuo onde:

  • O usuário digita uma mensagem no terminal.
  • A mensagem é enviada ao servidor.
  • O cliente aguarda e exibe a resposta recebida.

A comunicação continua até que o usuário digite "sair" ou o servidor envie essa mesma palavra, encerrando a conexão.

  • 🔌 Cria um socket TCP.
  • 🌐 Conecta ao servidor especificado.
  • ⌨️ Permite envio de mensagens digitadas pelo usuário.
  • 📥 Recebe respostas do servidor.
  • ❌ Encerra a conexão quando solicitado.

📌 Em resumo

Trata-se de um cliente simples de chat, que permite comunicação direta com um servidor TCP, enviando e recebendo mensagens até que uma das partes decida finalizar a conexão.

Sockets UDP - Aplicação de Chat Mural

Nesta seção, descreveremos uma aplicação de chat utilizando sockets UDP, onde múltiplos usuários podem escrever em um mural e as mensagens aparecem para todos. A aplicação é baseada nos arquivos server_udp.py e client_udp.py.

A aplicação consiste em um servidor que escuta mensagens de clientes e retransmite essas mensagens para todos os clientes conectados. Ao contrário do TCP, o UDP não estabelece uma conexão, permitindo que as mensagens sejam enviadas de forma mais rápida, mas sem garantias de entrega.

Código do Servidor (server_udp.py)

O código do servidor é responsável por:

  1. Criar um socket UDP e vinculá-lo a um endereço IP e porta.
  2. Escutar mensagens de clientes.
  3. Retransmitir mensagens recebidas para todos os clientes.
import socket

def servidor_udp():
    host = '127.0.0.1'  # Endereço IP do servidor
    porta = 12345       # Porta para escutar conexões

    servidor_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    servidor_socket.bind((host, porta))

    print(f"Servidor UDP aguardando mensagens em {host}:{porta}...")

    clientes = set()  # Conjunto para armazenar endereços dos clientes

    while True:
        mensagem, endereco = servidor_socket.recvfrom(1024)
        mensagem_decodificada = mensagem.decode('utf-8')

        if endereco not in clientes:
            clientes.add(endereco)

        print(f"Mensagem recebida de {endereco}: {mensagem_decodificada}")

        # Retransmitir a mensagem para todos os clientes
        for cliente in clientes:
            if cliente != endereco:  # Não enviar de volta para o remetente
                servidor_socket.sendto(mensagem, cliente)

if __name__ == "__main__":
    servidor_udp()

O código implementa um servidor de chat utilizando o protocolo UDP em Python.

Ele fica escutando mensagens enviadas para o endereço 127.0.0.1 na porta 12345. Diferente do TCP, o UDP não estabelece conexão formal com os clientes — ele apenas recebe e envia datagramas (pacotes de dados). Sempre que uma mensagem é recebida:

  • O servidor identifica o endereço do remetente.
  • Armazena esse endereço em uma lista (caso ainda não esteja registrado).
  • Exibe a mensagem no terminal.
  • Reenvia a mensagem para todos os outros clientes conhecidos (broadcast).

O servidor permanece executando indefinidamente, aguardando novas mensagens.

  • 📡 Cria um socket UDP.
  • 📥 Recebe mensagens junto com o endereço do remetente.
  • 👥 Mantém um conjunto de clientes que já enviaram mensagens.
  • 🔁 Reencaminha mensagens para todos os clientes, exceto o remetente.
  • 🔄 Opera continuamente sem estabelecer conexões formais.

📌 Em resumo

Trata-se de um servidor de chat simples baseado em UDP, que distribui mensagens entre múltiplos clientes sem controle de conexão, priorizando simplicidade e leveza na comunicação.

Código do Cliente (client_udp.py)

O código do cliente é responsável por:

  1. Conectar-se ao servidor.
  2. Enviar mensagens digitadas pelo usuário para o servidor.
  3. Receber mensagens do servidor e exibi-las na tela.
import socket
import threading

def receber_mensagens(cliente_socket):
    while True:
        try:
            mensagem, _ = cliente_socket.recvfrom(1024)
            print(f"\nMensagem recebida: {mensagem.decode('utf-8')}")
        except:
            break

def cliente_udp():
    host = '127.0.0.1'  # Endereço IP do servidor
    porta = 12345       # Porta do servidor

    cliente_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    nome = input("Digite seu nome: ")
    print(f"Conectado ao servidor UDP em {host}:{porta}")

    # Thread para receber mensagens do servidor
    threading.Thread(target=receber_mensagens, args=(cliente_socket,), daemon=True).start()

    while True:
        mensagem = input(f"{nome}: ")
        if mensagem.lower() == 'sair':
            print("Encerrando conexão.")
            break
        mensagem_completa = f"{nome}: {mensagem}"
        cliente_socket.sendto(mensagem_completa.encode('utf-8'), (host, porta))

    cliente_socket.close()

if __name__ == "__main__":
    cliente_udp()

O código implementa um cliente de chat utilizando o protocolo UDP em Python, permitindo comunicação com um servidor sem estabelecer conexão formal (diferente do TCP). Ao iniciar:

  • O usuário informa seu nome.
  • O cliente cria um socket UDP.
  • Uma thread separada é iniciada para receber mensagens do servidor continuamente.
  • O programa entra em um loop onde o usuário pode digitar e enviar mensagens.

Cada mensagem enviada é formatada com o nome do usuário e transmitida ao servidor. A comunicação continua até que o usuário digite "sair", encerrando o programa.

  • 📡 Cria um socket UDP.
  • 🧵 Usa uma thread paralela para receber mensagens enquanto o usuário digita.
  • 📤 Envia mensagens ao servidor usando sendto().
  • 📥 Recebe mensagens usando recvfrom().
  • ❌ Encerra ao digitar "sair".

📌 Em resumo

Trata-se de um cliente de chat UDP com suporte a envio e recebimento simultâneo de mensagens, utilizando threads para permitir comunicação em tempo real sem bloquear a entrada do usuário.

Raw Sockets

Os raw sockets permitem que os desenvolvedores tenham acesso direto ao protocolo de rede, permitindo a criação e manipulação de pacotes de rede em um nível mais baixo. Isso é útil para aplicações que precisam de controle total sobre os cabeçalhos dos pacotes, como ferramentas de análise de rede e segurança.

Um raw socket permite que você crie pacotes de rede personalizados, incluindo a manipulação dos cabeçalhos de protocolo. Isso é feito utilizando a biblioteca socket do Python, que permite a criação de sockets de diferentes tipos, incluindo raw sockets. Ao usar raw sockets, você deve ter permissões de administrador, pois eles podem ser usados para enviar pacotes que podem afetar a rede.

Código do Servidor (server_raw.py)

O código do servidor é responsável por:

  1. Criar um socket raw para escutar pacotes de rede.
  2. Processar pacotes recebidos e exibir informações sobre eles.
import socket
import struct

def start_server():
    # Escuta especificamente o protocolo UDP na camada raw
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
    s.bind(("127.0.0.1", 0))
    print("Servidor Raw aguardando pacotes UDP...")

    while True:
        packet, addr = s.recvfrom(9999)

        # O cabeçalho IP tem 20 bytes, o UDP tem 8 bytes
        # O payload começa após o byte 28
        ip_header = packet[0:20]
        udp_header = packet[20:28]
        payload = packet[28:]

        # Desempacotando IP para ver quem enviou (opcional)
        iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
        s_addr = socket.inet_ntoa(iph[8])

        print(f"\n--- Novo Pacote de {s_addr} ---")
        print(f"IP Header: {ip_header.hex()}")
        print(f"UDP Header: {udp_header.hex()}")
        print(f"Payload Recebido: {payload.decode(errors='ignore')}")

if __name__ == "__main__":
    start_server()

O código implementa um servidor que utiliza Raw Socket para capturar pacotes UDP diretamente na camada de rede, permitindo analisar manualmente os dados do pacote. Diferente de um servidor UDP comum, ele:

  • Cria um socket do tipo RAW.
  • Intercepta pacotes completos (incluindo cabeçalhos IP e UDP).
  • Separa manualmente:
  • 📦 Cabeçalho IP
  • 📦 Cabeçalho UDP
  • 📄 Payload (dados da aplicação)
  • Exibe as informações detalhadas no terminal.

O servidor permanece em execução contínua, aguardando novos pacotes.

  • 🧷 Cria um socket SOCK_RAW.
  • 📡 Captura pacotes completos da rede.
  • ✂️ Separa os primeiros 20 bytes (IP), os próximos 8 bytes (UDP) e o restante como dados.
  • 🔍 Decodifica o endereço IP de origem.
  • 🖨️ Exibe os cabeçalhos em formato hexadecimal e o conteúdo da mensagem.

⚠️ Observações Importantes

  • Requer permissões administrativas/root para funcionar.
  • Não estabelece conexão.
  • Não envia respostas — apenas monitora e analisa pacotes.
  • É utilizado para fins de estudo, monitoramento ou análise de protocolos.

📌 Em resumo

Trata-se de um servidor de captura e análise de pacotes UDP em nível baixo (raw socket), permitindo visualizar a estrutura interna dos pacotes IP e UDP que trafegam pela rede.

Código do Cliente (client_raw.py)

O código do cliente é responsável por:

  1. Criar um socket raw para enviar pacotes personalizados.
  2. Construir um pacote Ethernet e enviá-lo para o servidor.
import socket
import struct

def checksum(msg):
    s = 0
    # Adiciona preenchimento se o comprimento for ímpar
    if len(msg) % 2 != 0:
        msg += b'\x00'
    for i in range(0, len(msg), 2):
        w = (msg[i] << 8) + (msg[i+1])
        s = s + w
    s = (s >> 16) + (s & 0xffff)
    s = ~s & 0xffff
    return s

def send_raw_packet():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
        # s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
    except PermissionError:
        print("Erro: Requer privilégios de Root/Admin.")
        return

    src_ip = "127.0.0.1"
    dest_ip = "127.0.0.1"

    while True:
        msg = input("Cliente: ")
        if msg.lower() == 'sair':
            print("Encerrando cliente.")
            break
        msg = msg.encode('utf-8')

        # --- CABEÇALHO IP (20 Bytes) ---
        ip_ver_ihl = (4 << 4) + 5  # Versão 4, IHL 5
        ip_tos = 0
        ip_tot_len = 20 + 8 + len(msg)
        ip_id = 54321
        ip_frag_off = 0
        ip_ttl = 255
        ip_proto = socket.IPPROTO_UDP
        ip_check = 0 # Kernel preenche se deixarmos 0 em alguns sistemas
        ip_saddr = socket.inet_aton(src_ip)
        ip_daddr = socket.inet_aton(dest_ip)

        ip_header = struct.pack('!BBHHHBBH4s4s', ip_ver_ihl, ip_tos, ip_tot_len, 
                                ip_id, ip_frag_off, ip_ttl, ip_proto, ip_check, 
                                ip_saddr, ip_daddr)

        # --- CABEÇALHO UDP (8 Bytes) ---
        sport = 12345
        dport = 9999
        udp_len = 8 + len(msg)
        udp_check = 0 # Opcional para UDP em IPv4

        udp_header = struct.pack('!HHHH', sport, dport, udp_len, udp_check)

        # Envio do pacote completo: IP + UDP + MSG
        packet = ip_header + udp_header + msg
        s.sendto(packet, (dest_ip, 0))
        print(f"Pacote bruto enviado com sucesso para {dest_ip}!")

if __name__ == "__main__":
    send_raw_packet()

O código implementa um cliente que constrói e envia pacotes IP/UDP manualmente utilizando Raw Socket em Python. Diferente de um cliente UDP tradicional, ele:

  • Cria um socket do tipo RAW.
  • Monta manualmente o cabeçalho IP.
  • Monta manualmente o cabeçalho UDP.
  • Anexa a mensagem digitada pelo usuário como payload.
  • Envia o pacote completo diretamente pela camada de rede.

  • 🔐 Requer privilégios de Administrador/Root.

  • 🧱 Constrói manualmente:
  • Cabeçalho IP (20 bytes)
  • Cabeçalho UDP (8 bytes)
  • Dados da aplicação (mensagem)
  • 📤 Envia o pacote bruto usando sendto().
  • 🔄 Permite envio contínuo até o usuário digitar "sair".

O código também possui uma função de cálculo de checksum, embora não esteja sendo aplicada diretamente no envio do pacote.

⚙️ Características Técnicas

  • Define manualmente:
  • Versão do IP
  • Tamanho total do pacote
  • TTL
  • Protocolo (UDP)
  • Portas de origem e destino
  • Não depende do sistema operacional para montar os cabeçalhos.
  • Envia o pacote completo já estruturado.

⚠️ Observações Importantes

  • Funciona apenas com permissões elevadas.
  • Pode não funcionar da mesma forma em todos os sistemas operacionais.
  • É voltado para fins educacionais, testes de rede ou estudo de protocolos.

📌 Em resumo

Trata-se de um cliente avançado que envia pacotes UDP manualmente construídos via Raw Socket, permitindo controle total sobre a estrutura do pacote na camada IP.

Entender a string de formatação do struct.pack é como aprender a ler o mapa de memória do hardware. Cada letra representa um tipo de dado e seu tamanho em bytes.

No exemplo anterior, usamos !BBHHHBBH4s4s. Vamos dissecar isso:

O Prefixo: Ordem dos Bytes * ! (Ponto de Exclamação): Define o Network Byte Order (Big-Endian). Na rede, o byte mais significativo vem primeiro. Sem isso, o Python usaria a ordem nativa do seu processador (geralmente Little-Endian em Intel/AMD), o que inverteria os números e faria o roteador descartar seu pacote.

O Cabeçalho IP (!BBHHHBBH4s4s)

O cabeçalho IPv4 padrão tem 20 bytes. Veja como cada letra mapeia para os campos do protocolo:

Letra Significado Tamanho Campo no IPv4
B Unsigned Char 1 Byte Versão (4 bits) + IHL (4 bits)
B Unsigned Char 1 Byte Type of Service (ToS)
H Unsigned Short 2 Bytes Total Length (Comprimento Total)
H Unsigned Short 2 Bytes Identification (ID do Pacote)
H Unsigned Short 2 Bytes Flags (3 bits) + Fragment Offset (13 bits)
B Unsigned Char 1 Byte Time to Live (TTL)
B Unsigned Char 1 Byte Protocol (TCP = 6, UDP = 17)
H Unsigned Short 2 Bytes Header Checksum
4s Char[4] 4 Bytes Source IP Address (binário)
4s Char[4] 4 Bytes Destination IP Address (binário)

O Cabeçalho UDP (!HHHH)

Este é muito mais simples, possuindo apenas 8 bytes:

Letra Significado Tamanho Campo no UDP
H Unsigned Short 2 Bytes Source Port
H Unsigned Short 2 Bytes Destination Port
H Unsigned Short 2 Bytes Length (Cabeçalho + Dados)
H Unsigned Short 2 Bytes Checksum

Exercícios

Exercício 1: Identificação e Auditoria de Protocolos (TCP/UDP)

  • Objetivo: Modificar o comportamento do servidor para identificar o tipo de cliente. No código do Chat TCP e do Chat Mural UDP, altere o servidor para que, ao receber uma mensagem, ele imprima não apenas o conteúdo, mas também:

  • O protocolo utilizado (TCP ou UDP).

  • O endereço IP e a porta de origem do cliente de forma formatada.
  • O tamanho exato (em bytes) do payload recebido.

Tip

No TCP, use client_socket.getpeername(). No UDP, use o objeto endereco retornado pelo recvfrom.

Exercício 2: Controle de Fluxo e Encerramento Gracioso (TCP)

  • Objetivo: Implementar um comando de administração no chat. Atualmente, o chat TCP encerra apenas se a palavra "sair" for enviada. Modifique o server_tcp.py para que:

  • Se um cliente enviar a mensagem /list, o servidor responda apenas para aquele cliente a quantidade de usuários conectados no momento.

  • Se um cliente enviar /kick [IP], o servidor deve fechar a conexão do socket correspondente àquele IP (simulando uma função de moderador).

Exercício 3: Implementação de Timeout e Confiabilidade (UDP)

  • Objetivo: Lidar com a natureza "não confiável" do UDP. O UDP não garante a entrega. Modifique o client_udp.py para implementar um mecanismo de Eco/Confirmação:

  • O cliente envia a mensagem e aguarda por 2 segundos (socket.settimeout(2.0)).

  • O servidor, ao receber, deve enviar de volta uma mensagem automática: ACK: [conteúdo da mensagem].
  • Se o cliente não receber o ACK em 2 segundos, ele deve imprimir: "Falha no envio: Servidor não respondeu".

Exercício 4: Injeção de Erros e Manipulação de TTL (Raw Sockets)

  • Objetivo: Experimentar com os campos do cabeçalho IP. Utilizando o client_raw.py, realize os seguintes testes e observe o comportamento no server_raw.py:
  • TTL Expirado: Altere o campo ip_ttl para 1. Tente enviar para um IP externo (que não seja o localhost) e veja se o pacote chega ou se é descartado por roteadores.
  • Identificação de Protocolo: Altere o campo ip_proto de socket.IPPROTO_UDP para um valor arbitrário (ex: 253 - usado para testes). O servidor raw ainda consegue capturar o pacote? Por quê?

Exercício 5: O Desafio do Checksum (Raw Sockets)

  • Objetivo: Validar a integridade do cabeçalho IP. O código fornecido no laboratório tem uma função checksum(msg), mas ela não está sendo usada para preencher o campo ip_check (que está como 0).

  • Implemente a chamada da função checksum no client_raw.py. Lembre-se que para calcular o checksum do IP, o campo de checksum deve estar zerado durante o cálculo.

  • No server_raw.py, implemente uma lógica que recalcula o checksum do cabeçalho IP recebido. Se o resultado for diferente de 0xFFFF (ou 0 dependendo da implementação), o servidor deve imprimir: "ALERTA: Pacote corrompido detectado!".