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:
- Criar um socket TCP e vinculá-lo a um endereço IP e porta.
- Escutar por conexões de clientes.
- Aceitar conexões e adicionar clientes a uma lista.
- 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:
- Conectar-se ao servidor.
- Enviar mensagens digitadas pelo usuário para o servidor.
- 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:
- Criar um socket UDP e vinculá-lo a um endereço IP e porta.
- Escutar mensagens de clientes.
- 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:
- Conectar-se ao servidor.
- Enviar mensagens digitadas pelo usuário para o servidor.
- 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:
- Criar um socket raw para escutar pacotes de rede.
- 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:
- Criar um socket raw para enviar pacotes personalizados.
- 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.pypara 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 noserver_raw.py: - TTL Expirado: Altere o campo
ip_ttlpara 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_protodesocket.IPPROTO_UDPpara 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 campoip_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 de0xFFFF(ou 0 dependendo da implementação), o servidor deve imprimir:"ALERTA: Pacote corrompido detectado!".