Gestion des paquets de données en réseau avec Python : Éviter la fusion de paquets

Introduction à la gestion des paquets en réseau avec Python

Le problème de la fusion de paquets (Message Packing)

Lors de l'utilisation du protocole TCP, il peut arriver que plusieurs petits paquets de données envoyés successivement soient regroupés en un seul grand paquet avant d'atteindre leur destination. Par exemple, si vous envoyez les chaînes "ABC" puis "123", le destinataire pourrait recevoir "ABC123" comme un seul bloc. Ce phénomène, appelé "fusion de paquets" ou "message packing", rend la distinction et le traitement des messages individuels complexes.

Causes de la fusion de paquets

  • Réception de taille fixe : Les fonctions de réception réseau comme recv() nécessitent une taille de réception spécifiée. Comme la taille exacte des messages entrants n'est pas toujours connue, on utilise souvent une taille fixe, ce qui peut entraîner la lecture de plusieurs messages en une seule opération.
  • Nature du protocole TCP : TCP est un protocole orienté flux (stream-oriented). Il traite les données comme un flux continu sans frontières inhérentes entre les messages. Pour optimiser la transmission, TCP peut regrouper des données envoyées sur une courte période en un seul paquet.

Solution : Structurer la communication

Pour éviter la fusion de paquets, l'approche fondamentale consiste à s'assurer que le récepteur sait exactement combien de données il doit s'attendre à recevoir pour chaque message.

Le module struct pour la sérialisation

Le module struct en Python est esentiel pour empaqueter et dépaqueter des données binaires dans des formats de taille fixe. Il permet de convertir des types de données Python (comme les entiers, les flottants) en séquences d'octets et vice-versa, en utilisant des spécificateurs de format.

Fonctions clés du module struct

  • **pack(format, v1, v2, ...) :** Empaquette les valeurs Python v1, v2, etc., selon le format spécifié, et renvoie le résultat sous forme d'octets (bytes`).
  • **unpack(format, buffer) :** Dépaquette les octets contenus dans bufferselon leformat` spécifié, et renvoie un tuple des valeurs Python correspondantes.

Spécificateurs de format courants

Voici quelques spécificateurs de format couramment utilisés avec struct :

Format Type Python Taille standard (octets)
c Caractère (1 octet) 1
s Séquence d'octets Variable (dépend de la longueur)
i Entier signé 4
f Flottant simple précision 4
d Flottant double précision 8

Implémentation pratique pour résoudre la fusion de paquets

L'idée est d'envoyer d'abord la taille du message qui va suivre, puis le message lui-même. Nous pouvons utiliser struct pour encoder la taille du message en un nombre fixe d'octets (par exemple, 4 octets pour un entier).

Stratégie de résolution

  1. Préparation des données : Avant d'envoyer des données, déterminez leur taille. Si les données sont complexes (comme un dictionnaire contenant des métadonnées sur un fichier), sérialisez-les d'abord (par exemple, en JSON), puis obtenez la taille de cette représentatoin sérialisée.
  2. Envoi de l'en-tête de taille : Utilisez struct.pack pour convertir la taille des données (en octets) en une séquence d'octets de longueur fixe (par exemple, 4 octets pour un entier). Envoyez cette séquence d'octets comme en-tête.
  3. Envoi des données : Envoyez ensuite les données réelles.
  4. Réception de l'en-tête : Le récepteur lit d'abord le nombre fixe d'octets correspondant à l'en-tête de taille.
  5. Décodage de la taille : Utilisez struct.unpack pour convertir les octets de l'en-tête en un entier, qui représente la taille des données à venir.
  6. Réception des données : Le récepteur lit ensuite exactement le nombre d'octest spécifié par la taille décodée.

Exemple de code : Client et Serveur

Serveur :

import socket
import struct
import json

server_address = ('127.0.0.1', 8081)
BUFFER_SIZE = 4  # Taille de l'en-tête (pour la longueur du message)

# Création du socket serveur
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(server_address)
server_socket.listen(5)
print(f"Serveur démarré sur {server_address}, en attente de connexions...")

conn, addr = server_socket.accept()
print(f"Connexion établie avec {addr}")

try:
   # 1. Recevoir l'en-tête de taille du message (4 octets)
   packed_msg_size = conn.recv(BUFFER_SIZE)
   if not packed_msg_size:
       print("Connexion fermée par le client.")
       exit()

   # 2. Dépaqueter la taille du message
   msg_size = struct.unpack('i', packed_msg_size)[0]
   print(f"Taille du message à recevoir : {msg_size} octets")

   # 3. Recevoir le message JSON
   json_data_bytes = b''
   while len(json_data_bytes) < msg_size:
       chunk = conn.recv(msg_size - len(json_data_bytes))
       if not chunk:
           break
       json_data_bytes += chunk

   if len(json_data_bytes) < msg_size:
       print("Erreur : Données reçues incomplètes.")
       exit()
       
   # Désérialiser le JSON
   metadata = json.loads(json_data_bytes.decode('utf-8'))
   filename = metadata.get('filename', 'received_file')
   file_size_expected = metadata.get('filesize', 0)
   print(f"Métadonnées reçues : Fichier='{filename}', Taille attendue={file_size_expected}")

   # 4. Recevoir les données du fichier réel
   received_bytes = 0
   with open(f"received_{filename}", 'wb') as f:
       while received_bytes < file_size_expected:
           # Recevoir par morceaux pour gérer les gros fichiers
           chunk_size = min(1024, file_size_expected - received_bytes)
           data_chunk = conn.recv(chunk_size)
           if not data_chunk:
               break
           f.write(data_chunk)
           received_bytes += len(data_chunk)
           print(f"Reçu {received_bytes}/{file_size_expected} octets...", end='\r')
   print(f"\nFichier '{filename}' reçu avec succès.")

finally:
   conn.close()
   server_socket.close()
   print("Connexion fermée.")

   
Client :

import socket
import os
import struct
import json

server_address = ('127.0.0.1', 8081)
BUFFER_SIZE = 4  # Taille de l'en-tête (pour la longueur du message)

# Fichier à envoyer (assurez-vous qu'il existe dans le même répertoire)
# Remplacer par un chemin valide si nécessaire
FILE_TO_SEND = 'my_document.txt' 

# Création du socket client
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
   client_socket.connect(server_address)
   print(f"Connecté au serveur {server_address}")

   if not os.path.exists(FILE_TO_SEND):
       print(f"Erreur : Le fichier '{FILE_TO_SEND}' n'existe pas.")
       exit()

   # 1. Obtenir la taille du fichier
   file_size = os.path.getsize(FILE_TO_SEND)

   # 2. Créer les métadonnées (ex: nom de fichier, taille)
   metadata = {
       'filename': os.path.basename(FILE_TO_SEND),
       'filesize': file_size,
       'description': 'Ceci est un exemple de fichier texte.'
   }

   # 3. Sérialiser les métadonnées en JSON et encoder en bytes
   metadata_bytes = json.dumps(metadata).encode('utf-8')
   metadata_size = len(metadata_bytes)

   # 4. Empaqueter la taille des métadonnées (4 octets)
   packed_metadata_size = struct.pack('i', metadata_size)

   # 5. Envoyer l'en-tête de taille des métadonnées
   client_socket.sendall(packed_metadata_size)

   # 6. Envoyer les métadonnées elles-mêmes
   client_socket.sendall(metadata_bytes)

   # 7. Envoyer le contenu réel du fichier
   sent_bytes = 0
   with open(FILE_TO_SEND, 'rb') as f:
       while sent_bytes < file_size:
           # Lire et envoyer par morceaux
           chunk = f.read(1024) # Lire jusqu'à 1024 octets
           if not chunk:
               break
           client_socket.sendall(chunk)
           sent_bytes += len(chunk)
           print(f"Envoyé {sent_bytes}/{file_size} octets...", end='\r')
   
   print(f"\nFichier '{FILE_TO_SEND}' envoyé avec succès.")

except ConnectionRefusedError:
   print("Erreur : Connexion refusée. Assurez-vous que le serveur est en cours d'exécution.")
except FileNotFoundError:
   print(f"Erreur : Le fichier '{FILE_TO_SEND}' n'a pas été trouvé.")
except Exception as e:
   print(f"Une erreur est survenue : {e}")
finally:
   client_socket.close()
   print("Connexion client fermée.")

   

Le protocole UDP

Contrairement à TCP, le protocole UDP (User Datagram Protocol) est sans connexion et non fiable. Il ne garantit pas la livraison des paquets, l'ordre des paquets ni la déduplication. Chaque envoi UDP est indépendant.

Caractéristiques d'UDP

  • Sans connexion : Pas de handshake préalable comme TCP.
  • Non fiable : Les paquets peuvent être perdus, dupliqués ou arriver dans le désordre.
  • Rapide : Moins de surcharge que TCP car il n'y a pas de mécanismes de confirmation, de contrôle de flux ou de récupération d'erreurs.
  • Orienté datagramme : Les données sont envoyées en paquets distincts (datagrammes), avec des frontières claires entre eux. La fusion de paquets n'est pas un problème avec UDP de la même manière qu'avec TCP.

Implémentation UDP : Serveur


import socket

server_address = ('192.168.108.43', 8090) # IP et port du serveur

# Création du socket UDP
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Liaison de l'adresse et du port
server_socket.bind(server_address)
print(f"Serveur UDP démarré sur {server_address}")

# Réception des données
# recvfrom retourne les données et l'adresse de l'expéditeur
data, client_address = server_socket.recvfrom(1024) 

print(f"Message reçu de {client_address}: {data.decode('utf-8')}")

# Fermeture du socket
server_socket.close()
print("Serveur UDP arrêté.")

   

Implémentation UDP : Client


import socket

server_address = ('192.168.108.43', 8080) # IP et port du serveur destinataire

# Création du socket UDP
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

message = input("Entrez le message à envoyer : ")
encoded_message = message.encode('utf-8')

# Envoi des données au serveur
client_socket.sendto(encoded_message, server_address)
print(f"Message envoyé à {server_address}")

# Fermeture du socket
client_socket.close()
print("Client UDP arrêté.")

   

Étiquettes: Python réseau TCP UDP fusion de paquets

Publié le 6 juillet à 02h23