Programação de Redes: Sockets, Protocolos e Implementação em Python

Arquitetura Cliente/Servidor e Protocolos de Rede

A arquitetura cliente/servidor (C/S) é fundamental em redes, onde um cliente solicita serviços a um servidor. Em software, isso é exemplificado por navegadores (clientes) acessando servidores web. O estudo de sockets visa desenvolver aplicações C/S.

O modelo OSI divide a comunicação em sete camadas, desde a física até a aplicação, fornecendo um framework padrão. Para programação de redes, as camadas de transporte (TCP/UDP) e aplicação são cruciais.

Camada de Sockets e Definição

Sockets atuam como uma abstração de software entre a camada de aplicação e a pilha TCP/IP. Eles encapsulam protocolos complexos, oferecendo uma interface simplificada para comunicação. Um socket pode ser identificado pelo par IP+porta, que identifica exclusivamente uma aplicação na rede.

História e Classificação de Sockets

Sockets originaram-se no BSD Unix na década de 1970, inicialmente para comunicação interprocesso (IPC). Existem duas famílias: AF_UNIX para comunicação baseada em arquivos no mesmo host, e AF_INET (e AF_INET6) para comunicação em redes IP, sendo AF_INET o mais utilizado.

Fluxo de Trabalho de Sockets

O processo segue etapas: o servidor inicializa um socket, vincula a um endereço (bind), escuta conexões (listen) e aceita clientes (accept). O cliente inicializa seu socket e conecta-se (connect). Após comunicação bidirecional, a conexão é encerrada.

Funções Essenciais de Sockets em Python


import socket
# Criar sockets TCP e UDP
socket_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Funções do servidor: bind, listen, accept. Funções do cliente: connect. Funções comuns: recv, send, sendall, recvfrom, sendto, close.

Sockets Baseados em TCP

TCP é orientado a conexão, exigindo que o servidor inicie primeiro. Exemplo de servidor TCP com ciclos de conexão e comunicação:


import socket
endereco = ('127.0.0.1', 9000)
tamanho_buffer = 1024
servidor_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
servidor_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servidor_sock.bind(endereco)
servidor_sock.listen(5)
while True:
    conexao, cliente_end = servidor_sock.accept()
    while True:
        dados = conexao.recv(tamanho_buffer)
        if not dados:
            break
        conexao.send(dados.upper())
    conexao.close()

Exemplo de cliente TCP:


import socket
endereco = ('127.0.0.1', 9000)
tamanho_buffer = 1024
cliente_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cliente_sock.connect(endereco)
while True:
    mensagem = input(">>: ").strip()
    if not mensagem:
        continue
    cliente_sock.send(mensagem.encode('utf-8'))
    resposta = cliente_sock.recv(tamanho_buffer)
    print(resposta.decode('utf-8'))
cliente_sock.close()

Sockets Baseados em UDP

UDP é sem conexão, permitindo comunicação um-para-muitos. Exemplo de servidor UDP:


import socket
endereco = ('127.0.0.1', 9000)
tamanho_buffer = 1024
servidor_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
servidor_udp.bind(endereco)
while True:
    mensagem, cliente_end = servidor_udp.recvfrom(tamanho_buffer)
    servidor_udp.sendto(mensagem.upper(), cliente_end)

Exemplo de cliente UDP:


import socket
endereco = ('127.0.0.1', 9000)
tamanho_buffer = 1024
cliente_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
    mensagem = input(">>: ").strip()
    if not mensagem:
        continue
    cliente_udp.sendto(mensagem.encode('utf-8'), endereco)
    resposta, servidor_end = cliente_udp.recvfrom(tamanho_buffer)
    print(resposta.decode('utf-8'))

Fenômeno de Pacotes Aderidos (Sticky Packets)

Este fenômeno ocorre apenas em TCP devido à sua natureza orientada a fluxo. Dados enviados podem ser recebidos como um bloco contínuo, sem delimitação clara entre mensagens. UDP, sendo orientado a mensagens, preserva fronteiras e evita esse problema.

Causas incluem: aguardar o buffer do remetente encher antes de enviar, ou o receptor não ler dados suficientes de uma vez, fazendo com que dados residuais se misturem com novas leituras.

Soluções para Pacotes Aderidos

Uma abordagem básica é enviar o tamanho dos dados antes do conteúdo real. Exemplo de servidor com essa solução:


import socket
import struct
endereco = ('127.0.0.1', 8080)
tamanho_buffer = 1024
servidor_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
servidor_sock.bind(endereco)
servidor_sock.listen(5)
while True:
    conexao, cliente_end = servidor_sock.accept()
    while True:
        dados = conexao.recv(tamanho_buffer)
        if not dados:
            break
        tamanho_dados = len(dados)
        conexao.send(struct.pack('i', tamanho_dados))
        confirmacao = conexao.recv(tamanho_buffer).decode('utf-8')
        if confirmacao == 'pronto':
            conexao.sendall(dados)
    conexao.close()

Uma abordagem avançada usa cabeçalhos personalizados com comprimento fixo, posivelmente em formato JSON. Exemplo de servidor com cabeçalho JSON:


import socket, struct, json
endereco = ('127.0.0.1', 8080)
servidor_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
servidor_sock.bind(endereco)
servidor_sock.listen(5)
while True:
    conexao, cliente_end = servidor_sock.accept()
    while True:
        comando = conexao.recv(1024)
        if not comando:
            break
        # Processar comando e gerar resposta
        resposta = comando.upper()
        cabecalho = {'tamanho_resposta': len(resposta)}
        cabecalho_json = json.dumps(cabecalho).encode('utf-8')
        conexao.send(struct.pack('i', len(cabecalho_json)))
        conexao.send(cabecalho_json)
        conexao.sendall(resposta)
    conexao.close()

Autenticação de Conexões de Clientes

Para autenticar clientes, pode-se usar HMAC com uma chave secreta. O servidor envia um desafio aleatório, o cliente responde com um digest HMAC, e o servidor verifica a correspondência. Exemplo simplificado:


import socket, hmac, os
chave_secreta = b'chave_secreta_segura'
def autenticar_conexao(conexao):
    mensagem = os.urandom(32)
    conexao.sendall(mensagem)
    h = hmac.new(chave_secreta, mensagem)
    digest_esperado = h.digest()
    digest_recebido = conexao.recv(len(digest_esperado))
    return hmac.compare_digest(digest_recebido, digest_esperado)

Concorrência com socketserver

O módulo socketserver em Python facilita a implementação de servidores concorrentes. Usando ThreadingTCPServer, cada conexão é tratada em uma thread separada. Exemplo de servidor FTP simples:


import socketserver, struct, json, os
class ManipuladorFTP(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            dados = self.request.recv(4)
            if not dados:
                break
            tamanho_cabecalho = struct.unpack('i', dados)[0]
            cabecalho_json = self.request.recv(tamanho_cabecalho).decode('utf-8')
            cabecalho = json.loads(cabecalho_json)
            if cabecalho['comando'] == 'enviar':
                self.receber_arquivo(cabecalho)
    def receber_arquivo(self, info):
        tamanho_arquivo = info['tamanho']
        nome_arquivo = info['nome']
        recebido = 0
        with open(nome_arquivo, 'wb') as arquivo:
            while recebido < tamanho_arquivo:
                dados = self.request.recv(1024)
                arquivo.write(dados)
                recebido += len(dados)
servidor = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), ManipuladorFTP)
servidor.serve_forever()

Tags: Python socket TCP UDP network-programming

Publicado em 7-28 10:00