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 Pythonv1,v2, etc., selon leformatspécifié, et renvoie le résultat sous forme d'octets (bytes`). - **
unpack(format, buffer):** Dépaquette les octets contenus dansbufferselon 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
- 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.
- Envoi de l'en-tête de taille : Utilisez
struct.packpour 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. - Envoi des données : Envoyez ensuite les données réelles.
- 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.
- Décodage de la taille : Utilisez
struct.unpackpour convertir les octets de l'en-tête en un entier, qui représente la taille des données à venir. - 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é.")