A comunicação via TCP, por ser orientada a fluxo, pode apresentar um fenômeno conhecido como "empacotamento" (message packing ou message coalescing). Isso ocorre quando múltiplos pacotes de dados enviados em rápida sucessão são, na verdade, agrupados em um único pacote pelo sistema operacional antes de serem transmitidos pela rede, ou quando o receptor não consegue distinguir os limites das mensagens originais.
Vamos analisar um exemplo de cliente e servidor que executam comandos remotos.
# server.py
import socket
import subprocess
server_socket = socket.socket()
server_socket.bind(('localhost', 8080))
server_socket.listen(5)
connection, address = server_socket.accept()
while True:
command = connection.recv(1024).decode()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = process.communicate()
result = stdout if stdout else stderr
connection.sendall(result)
# client.py
import socket
client_socket = socket.socket()
client_socket.connect(('localhost', 8080))
while True:
command_input = input('>>>').strip()
if not command_input:
continue
client_socket.sendall(command_input.encode())
# O buffer de recebimento é fixo em 1024, o que pode ser um problema.
received_data = client_socket.recv(1024).decode('gbk', errors='ignore')
print(received_data)
Ao executar o tasklist seguido de dir no cliente, pode-se observar que a saída do comando dir pode aparecer incompleta ou misturada com partes não exibidas do tasklist. Isso é o empacotamento.
É crucial entender que o empacotamento é uma característica inerente ao protocolo TCP e não ocorre com UDP.
Causas do Empacotamento
- Fragmentação de Pacotes TCP: Quando o volume de dados a ser enviado excede a Unidade Máxima de Transmissão (MTU) da rede, o TCP divide os dados em pacotes menores. Ao receber, esses pacotes podem ser remontados de forma que os limites originais das mensagens se percam.
- Comunicação Orientada a Fluxo e Algoritmo de Nagle: O TCP trata os dados como um fluxo contínuo. Para otimizar o tráfego, especialmente com mensagens pequenas e frequentes, o algoritmo de Nagle pode agrupar esses pacotes. Na ponta receptora, é necessário um mecanismo para desempacotar corretamente esses dados. A comunicação orientada a fluxo não preserva os limites das mensagens.
- Mensagens Vazias: O TCP, por ser um protocolo de fluxo, não lida nativamente com mensagens vazias. O UDP, por outro lado, encapsula cada datagrama com um cabeçalho, mesmo que o conteúdo seja vazio.
- Protocolo TCP Confiável: A confiabilidade do TCP garante que os dados não serão perdidos e que pacotes incompletos serão retransmitidos. No entanto, essa garantia de entrega contínua contribui para o empacotamento.
Limitações de Tamanho de Envio
- UDP: O tamanho máximo de um datagrama UDP é de aproximadamente 65507 bytes (65535 - cabeçalho IP de 20 bytes - cabeçalho UDP de 8 bytes).
- TCP: Por ser um protocolo de fluxo, o TCP não impõe um limite rígido de tamanho de pacote para a função
send. No entanto, os dados enviados podem ser divididos em segmentos pela rede, ou, se forem pequenos, podem ser adiados para serem enviados junto com dados subsequentes.
Cenários de Empacotamento
O empacotamento ocorre principalmente em duas situações:
- Buffer do Remetente: O remetente pode acumular dados no buffer antes de enviá-los, especialmente se a rede estiver lenta ou o receptor não estiver processando os dados rapidamente.
- Buffer do Receptor: O receptor pode não ser capaz de processar os pacotes recebidos em tempo hábil, levando ao acúmulo de dados no buffer e à perda dos limites originais das mensagens.
Em essência, o problema central é que o receptor não tem como saber o tamanho exato da mensagem completa que está recebendo.
Soluções para o Empacotamento
A estratégia fundamental para resolver o empacotamento é garantir que o receptor saiba o tamanho total dos dados que está prestes a receber antes de começar a lê-los.
Solução 1: Envio do Tamanho da Mensagem
O remetente envia o tamanho da mensagem antes da mensagem em si. O receptor, então, lê o tamanho e continua recebendo dados até que o tamanho total seja alcançado.
# server.py
import socket
import subprocess
server_socket = socket.socket()
server_socket.bind(('localhost', 8080))
server_socket.listen(5)
connection, address = server_socket.accept()
while True:
command = connection.recv(1024).decode()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = process.communicate()
result = stdout if stdout else stderr
# Envia o tamanho do resultado antes dos dados
result_size = len(result)
connection.sendall(str(result_size).encode())
# O cliente pode enviar uma confirmação, mas aqui simplificamos
# connection.recv(1024) # Para garantir que o cliente está pronto (opcional)
connection.sendall(result)
# client.py
import socket
client_socket = socket.socket()
client_socket.connect(('localhost', 8080))
while True:
command_input = input('>>>').strip()
if not command_input:
continue
client_socket.sendall(command_input.encode())
# Recebe o tamanho do resultado
size_str = client_socket.recv(1024).decode()
# Envia uma confirmação para o servidor (opcional, para sincronizar)
# client_socket.sendall(b'000')
total_received_size = 0
# Loop para receber a quantidade total de dados especificada pelo tamanho
while total_received_size < int(size_str):
data_chunk = client_socket.recv(1024)
total_received_size += len(data_chunk)
print(data_chunk.decode('gbk', errors='ignore'))
Esta abordagem, embora funcional, pode introduzir latência adicional, pois a transmissão do tamanho da mensagem pode atrasar o envio dos dados reais.
Solução 2: Uso do Módulo struct para Tamanho Fixo
O módulo struct permite converter dados (como inteiros) em sequências de bytes de tamanho fixo. Isso é útil para criar um cabeçalho de tamanho conhecido.
import struct
# Empacota um inteiro em 4 bytes
packed_data = struct.pack('i', 111111)
print(packed_data) # Saída: b'\x07\xb2\x01\x00'
print(len(packed_data)) # Saída: 4
# Desempacota os 4 bytes de volta para um inteiro
unpacked_data = struct.unpack('i', packed_data)
print(unpacked_data) # Saída: (111111,)
Implementando com struct:
# server.py
import socket
import struct
import subprocess
server_socket = socket.socket()
server_socket.bind(('localhost', 8080))
server_socket.listen(5)
connection, address = server_socket.accept()
while True:
command = connection.recv(1024).decode()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = process.communicate()
result = stdout if stdout else stderr
# Empacota o tamanho do resultado em 4 bytes
result_size = len(result)
connection.sendall(struct.pack('i', result_size))
connection.sendall(result)
# client.py
import socket
import struct
client_socket = socket.socket()
client_socket.connect(('localhost', 8080))
while True:
command_input = input('>>>').strip()
if not command_input:
continue
client_socket.sendall(command_input.encode())
# Recebe os 4 bytes do cabeçalho de tamanho
size_header = client_socket.recv(4)
# Desempacota para obter o tamanho real dos dados
res_size = struct.unpack('i', size_header)[0]
received_total_size = 0
# Loop para receber os dados até atingir o tamanho especificado
while received_total_size < res_size:
data_chunk = client_socket.recv(1024)
received_total_size += len(data_chunk)
print(data_chunk.decode('gbk', errors='ignore'))
Solução 3: Cabeçalho com Metadados (JSON + struct)
Para cenários mais complexos, onde é preciso enviar mais informações além do tamanho dos dados (como tipo de comando, nome do arquivo, etc.), pode-se usar JSON para serializar um dicionário de metadados e struct para empacotar o tamanho desses metadados.
# server.py
import socket
import struct
import json
import subprocess
server_socket = socket.socket()
server_socket.bind(('localhost', 8080))
server_socket.listen(5)
connection, address = server_socket.accept()
while True:
command = connection.recv(1024).decode()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = process.communicate()
result = stdout if stdout else stderr
# Cria um dicionário com metadados
headers = {'res_size': len(result)}
# Serializa o dicionário para JSON
head_json_bytes = json.dumps(headers).encode('utf-8')
# Envia o tamanho do cabeçalho JSON (4 bytes)
connection.send(struct.pack('i', len(head_json_bytes)))
# Envia o cabeçalho JSON
connection.send(head_json_bytes)
# Envia os dados resultantes
connection.send(result)
# client.py
import socket
import struct
import json
client_socket = socket.socket()
client_socket.connect(('localhost', 8080))
while True:
command_input = input('>>>').strip()
if not command_input:
continue
client_socket.send(command_input.encode())
# Recebe os 4 bytes do tamanho do cabeçalho JSON
header_size_bytes = client_socket.recv(4)
header_size = struct.unpack('i', header_size_bytes)[0]
# Recebe o cabeçalho JSON
header_json_bytes = client_socket.recv(header_size)
header_dict = json.loads(header_json_bytes.decode('utf-8'))
# Obtém o tamanho real dos dados a serem recebidos
data_len = header_dict['res_size']
received_total_size = 0
# Loop para receber os dados reais
while received_total_size < data_len:
data_chunk = client_socket.recv(1024)
received_total_size += len(data_chunk)
print(data_chunk.decode('gbk', errors='ignore'))
Exemplo de FTP para Upload/Download
Um exemplo prático de uso dessas técnicas é em um cliente/servidor FTP, onde é necessário transferir arquivos.
# server.py (Fragmento para upload)
import socket
import json
import struct
import os
class MyServer:
def __init__(self, ip_port):
self.socket = socket.socket()
self.ip_port = ip_port
self.activate()
def activate(self):
self.socket.bind(self.ip_port)
self.socket.listen(5)
def get_request(self):
return self.socket.accept()
def run(self):
while True:
self.connection, self.client_addr = self.get_request()
print('Client connected:', self.client_addr)
while True:
try:
# Recebe o cabeçalho (tamanho do JSON)
head_struct = self.connection.recv(4)
if not head_struct: break # Conexão fechada
head_len = struct.unpack('i', head_struct)[0]
# Recebe o cabeçalho JSON
head_json = self.connection.recv(head_len).decode()
head_dict = json.loads(head_json)
command = head_dict.get('command')
if command == 'put':
self.handle_put(head_dict)
# Outros comandos podem ser adicionados aqui
except Exception as e:
print(f"Error handling request: {e}")
break
def handle_put(self, args):
filename = args['filename']
file_size = args['file_size']
upload_dir = 'uploaded_files'
os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(upload_dir, filename)
received_size = 0
with open(file_path, 'wb') as f:
while received_size < file_size:
chunk = self.connection.recv(1024)
if not chunk: break # Conexão fechada inesperadamente
received_size += len(chunk)
f.write(chunk)
print(f"File '{filename}' uploaded successfully.")
if __name__ == '__main__':
ftp_server = MyServer(('localhost', 8080))
ftp_server.run()
# client.py (Fragmento para upload)
import socket
import json
import struct
import os
import sys
class MyClient:
def __init__(self, ip_port):
self.socket = socket.socket()
self.ip_port = ip_port
self.connect()
def connect(self):
self.socket.connect(self.ip_port)
def run(self):
while True:
cmd_input = input('>>>').strip()
if not cmd_input: continue
parts = cmd_input.split()
command = parts[0]
if command == 'put':
self.handle_put(cmd_input)
# Outros comandos...
def handle_put(self, command_str):
if len(command_str.split()) < 2:
print("Usage: put <filename>")
return
filename = command_str.split()[1]
if not os.path.isfile(filename):
print(f"Error: File '{filename}' not found.")
return
file_size = os.path.getsize(filename)
headers = {'command': 'put', 'filename': filename, 'file_size': file_size}
head_json_bytes = json.dumps(headers).encode('utf-8')
head_struct_bytes = struct.pack('i', len(head_json_bytes))
self.socket.send(head_struct_bytes)
self.socket.send(head_json_bytes)
sent_size = 0
with open(filename, 'rb') as f:
while True:
data_chunk = f.read(1024)
if not data_chunk:
print('\nUpload successful.')
break
self.socket.send(data_chunk)
sent_size += len(data_chunk)
self.show_progress(sent_size, file_size, 'Uploading')
def show_progress(self, current, total, status):
bar_length = 50
percent = float(current) / float(total)
arrow = '=' * int(round(percent * bar_length))
spaces = ' ' * (bar_length - len(arrow))
sys.stdout.write(f'\r{status}: [{arrow + spaces}] {int(percent*100)}%')
sys.stdout.flush()
if __name__ == '__main__':
ftp_client = MyClient(('localhost', 8080))
ftp_client.run()
Módulo socketserver
O módulo socketserver simplifica a criação de servidores de rede, abstraindo a complexidade de lidar com múltiplos clientes simultaneamente. Ele oferece classes como TCPServer, UDPServer, e mixins para thredaing (ThreadingMixIn) ou forking (ForkingMixIn) para processamento assíncrono.
A estrutura básica envolve:
- Selecionar um tipo de servidor (ex:
ThreadingTCPServer). - Criar uma classe de manipulador de requisições que herda de
socketserver.BaseRequestHandler. O métodohandle()desta classe define como cada requisição de cliente será processada. - Instanciar o servidor, passando o endereço e a classe manipuladora.
- Iniciar o servidor com
serve_forever().
Exemplo de servidor usando socketserver:
# server.py
import socketserver
class EchoRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print(f"Connection from {self.client_address}")
while True:
try:
data = self.request.recv(1024).strip()
if not data:
break # Cliente desconectou
print(f"Received: {data.decode()}")
self.request.sendall(data.upper()) # Envia de volta em maiúsculas
except Exception as e:
print(f"Error: {e}")
break
if __name__ == '__main__':
HOST, PORT = 'localhost', 8080
# TCPServer com processamento em threads separadas para cada cliente
server = socketserver.ThreadingTCPServer((HOST, PORT), EchoRequestHandler)
print(f"Server started on {HOST}:{PORT}")
server.serve_forever()
Exemplo de cliente para o servidor acima:
# client.py
import socket
class SimpleClient:
def __init__(self, ip_port):
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.ip_port = ip_port
self.connect()
def connect(self):
try:
self.client_socket.connect(self.ip_port)
print(f"Connected to {self.ip_port}")
except ConnectionRefusedError:
print("Connection refused. Make sure the server is running.")
self.client_socket.close()
exit()
def send_message(self, message):
self.client_socket.sendall(message.encode())
response = self.client_socket.recv(1024)
print(f"Server response: {response.decode()}")
def run(self):
while True:
command = input("Enter message (or 'quit' to exit): ").strip()
if command.lower() == 'quit':
break
if command:
self.send_message(command)
self.client_socket.close()
if __name__ == '__main__':
client = SimpleClient(('localhost', 8080))
client.run()