Sockets: comunicação em rede com Python

1. Introdução aos Sockets em Python

Sockets são endpoints de comunicação bidirecional entre processos, seja na mesma máquina ou através de uma rede. Eles formam a base de praticamente toda comunicação na internet, permitindo que programas troquem dados de forma estruturada. Em Python, o módulo socket da biblioteca padrão fornece uma interface direta para as APIs de sockets do sistema operacional.

Existem dois tipos principais de sockets:

  • TCP (SOCK_STREAM): Orientado a conexão, garante entrega ordenada e confiável dos dados. Ideal para aplicações como navegadores web e servidores de email.
  • UDP (SOCK_DGRAM): Sem conexão, não garante entrega nem ordem. Mais rápido e leve, usado em streaming de vídeo e jogos online.

2. Configuração do Ambiente e Primeiro Socket

Para começar, importamos o módulo socket e criamos nosso primeiro socket:

import socket

# Criando um socket TCP (IPv4)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Criando um socket UDP (IPv4)
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Os parâmetros essenciais são:
- AF_INET: Família de endereços IPv4
- SOCK_STREAM: Socket TCP
- SOCK_DGRAM: Socket UDP

Sempre trate exceções e feche os sockets adequadamente:

import socket

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # ... operações com o socket
except socket.error as e:
    print(f"Erro ao criar socket: {e}")
finally:
    sock.close()

# Alternativa moderna com gerenciador de contexto
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    # o socket é fechado automaticamente
    pass

3. Comunicação TCP: Cliente e Servidor

Servidor TCP

O servidor precisa fazer bind (vincular a um endereço e porta), listen (aguardar conexões) e accept (aceitar uma conexão):

import socket

HOST = '127.0.0.1'  # localhost
PORT = 65432        # porta não privilegiada

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()
    print(f"Servidor ouvindo em {HOST}:{PORT}")

    conn, addr = server_socket.accept()
    with conn:
        print(f"Conectado por {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)  # echo

Cliente TCP

O cliente usa connect para estabelecer conexão e send/recv para trocar dados:

import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.sendall(b'Olá, servidor!')
    data = client_socket.recv(1024)
    print(f"Recebido: {data.decode()}")

4. Comunicação UDP: Envio sem Conexão

Servidor UDP

No UDP, não há conexão estabelecida. O servidor apenas recebe datagramas:

import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_socket:
    server_socket.bind((HOST, PORT))
    print(f"Servidor UDP ouvindo em {HOST}:{PORT}")

    data, addr = server_socket.recvfrom(1024)
    print(f"Recebido de {addr}: {data.decode()}")
    server_socket.sendto(b'Resposta do servidor', addr)

Cliente UDP

O cliente envia dados sem estabelecer conexão prévia:

import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_socket:
    client_socket.sendto(b'Olá, UDP!', (HOST, PORT))
    data, addr = client_socket.recvfrom(1024)
    print(f"Recebido: {data.decode()}")

Diferenças práticas: TCP garante entrega e ordem, mas tem overhead maior. UDP é mais rápido, mas não confiável — pacotes podem ser perdidos ou chegar fora de ordem.

5. Gerenciamento de Múltiplas Conexões

Usando select para monitorar múltiplos sockets

O módulo select permite monitorar vários sockets simultaneamente:

import socket
import select

HOST = '127.0.0.1'
PORT = 65432

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen()
server_socket.setblocking(False)

sockets_list = [server_socket]

print(f"Servidor com select ouvindo em {HOST}:{PORT}")

while True:
    read_sockets, _, _ = select.select(sockets_list, [], [])

    for sock in read_sockets:
        if sock == server_socket:
            conn, addr = server_socket.accept()
            conn.setblocking(False)
            sockets_list.append(conn)
            print(f"Nova conexão de {addr}")
        else:
            data = sock.recv(1024)
            if data:
                sock.sendall(data)
            else:
                sockets_list.remove(sock)
                sock.close()

Usando socketserver para servidores concorrentes

O módulo socketserver simplifica a criação de servidores que lidam com múltiplas conexões:

import socketserver

class EchoHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024)
        print(f"Recebido de {self.client_address}: {data.decode()}")
        self.request.sendall(data)

HOST = '127.0.0.1'
PORT = 65432

with socketserver.ThreadingTCPServer((HOST, PORT), EchoHandler) as server:
    print(f"Servidor concorrente em {HOST}:{PORT}")
    server.serve_forever()

6. Tratamento de Erros e Timeouts

Configurar timeouts evita que o programa trave indefinidamente:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)  # timeout de 5 segundos

try:
    sock.connect(('exemplo.com', 80))
    sock.sendall(b'GET / HTTP/1.1\r\nHost: exemplo.com\r\n\r\n')
    data = sock.recv(4096)
except socket.timeout:
    print("Timeout: servidor não respondeu")
except ConnectionRefusedError:
    print("Conexão recusada: servidor não está disponível")
except BrokenPipeError:
    print("Conexão perdida durante comunicação")
except socket.error as e:
    print(f"Erro de socket: {e}")
finally:
    sock.close()

Para operações não bloqueantes:

sock.setblocking(False)  # lançaria BlockingIOError se não houver dados

7. Envio e Recebimento de Dados Complexos

Serialização com pickle

Para enviar objetos Python complexos:

import socket
import pickle

# Cliente
data = {'nome': 'Alice', 'idade': 30, 'cidade': 'São Paulo'}
serialized = pickle.dumps(data)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('127.0.0.1', 65432))
    s.sendall(serialized)

# Servidor
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('127.0.0.1', 65432))
    s.listen()
    conn, addr = s.accept()
    data = conn.recv(4096)
    obj = pickle.loads(data)
    print(f"Objeto recebido: {obj}")

Usando struct para dados binários

Para enviar números e bytes de forma eficiente:

import socket
import struct

# Empacotando dados: inteiro (4 bytes) + float (4 bytes) + string
valor_int = 42
valor_float = 3.14
string = b"exemplo"

# Formato: ! (network byte order), i (int), f (float), 7s (string de 7 bytes)
packed = struct.pack('!if7s', valor_int, valor_float, string)

# Enviando
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('127.0.0.1', 65432))
    s.sendall(packed)

# Recebendo e desempacotando
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('127.0.0.1', 65432))
    s.listen()
    conn, _ = s.accept()
    data = conn.recv(4096)
    int_val, float_val, str_val = struct.unpack('!if7s', data)
    print(f"Int: {int_val}, Float: {float_val}, String: {str_val.decode()}")

Protocolo de comprimento fixo

Para evitar problemas de fragmentação, envie o tamanho antes dos dados:

def send_msg(sock, msg):
    # Envia primeiro o tamanho da mensagem (4 bytes)
    msg_len = len(msg)
    sock.sendall(struct.pack('!I', msg_len))
    sock.sendall(msg)

def recv_msg(sock):
    # Recebe primeiro o tamanho
    raw_len = recv_all(sock, 4)
    if not raw_len:
        return None
    msg_len = struct.unpack('!I', raw_len)[0]
    return recv_all(sock, msg_len)

def recv_all(sock, n):
    data = bytearray()
    while len(data) < n:
        packet = sock.recv(n - len(data))
        if not packet:
            return None
        data.extend(packet)
    return bytes(data)

8. Boas Práticas e Considerações Finais

Segurança básica: Nunca envie dados sensíveis em texto claro. Use SSL/TLS para criptografar a comunicação:

import ssl

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile="server.crt", keyfile="server.key")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.bind(('127.0.0.1', 65432))
    sock.listen()
    with context.wrap_socket(sock, server_side=True) as ssock:
        conn, addr = ssock.accept()
        # comunicação criptografada

Sockets Unix vs Rede: Sockets Unix (AF_UNIX) são mais rápidos para comunicação local, enquanto AF_INET permite comunicação em rede.

Aprofundamento: Para aplicações de alto desempenho, considere asyncio para sockets assíncronos ou bibliotecas como zeroconf para descoberta de serviços em rede.

Sockets são fundamentais para qualquer desenvolvedor Python que trabalhe com redes. Com os conceitos apresentados, você pode construir desde simples ferramentas de chat até servidores web e sistemas distribuídos complexos.

Referências