Programmation réseau en Python : fondamentaux et applications pratiques

Un réseau informatique est un ensemble d’équipements autonomes interconnectés. Son objectif principal est de permettre la communication et le partage de ressources entre machines. Internet constitue aujourd’hui un système d’une grande complexité, dont l’étude complète dépasse le cadre d’un seul article.

Bref historique

  1. Années 1960 — Le projet ARPANET, financé par le département de la Défense américain, pose les bases des réseaux à commutation de paquets.
  2. Années 1980 — L’ISO publie le modèle OSI/RM, qui structure la normalisation des réseaux.
  3. Années 1990 — L’arrivée des navigateurs graphiques et du Web rend l’accès à Internet accessible au grand public.

Le modèle TCP/IP

La communication sur Internet repose sur des protocoles, principalement définis par l’IETF. Le modèle TCP/IP est un ensemble de protocoles organisés en quatre couches : interface réseau, Internet, transport et application.

IP (Internet Protocol) travaille à la couche réseau. Il attribue une adresse à chaque hôte et assure le routage des paquets. Avec la pénurie d’adresses IPv4 publiques, le NAT permet d’utiliser des adresses privées au sein d’un LAN.

TCP (Transmission Control Protocol) repose sur IP et fournit une transmission fiable de bout en bout. Il garantit :

  • L’intégrité et la remise ordonnée des données (poignée de mains, sommes de contrôle, retrasnmissions).
  • Le contrôle de flux grâce à la fenêtre glissante.
  • Le contrôle de congestion par l’analyse du RTT et l’ajustement de la fenêtre d’envoi.

Architectures d’applications

  • Client/Serveur (C/S) et Navigateur/Serveur (B/S) : un client ou un navigateur accède à un serveur central.
  • Décentralisée : chaque nœud peut à la fois fournir et consommer des ressources, sans serveur fixe.

Accès aux ressources avec HTTP

HTTP et JSON

HTTP est le protocole applicatif fondamental du Web. Il permet de récupérer des ressources identifiées par des URI. Les API REST modernes renvoient généralement des données au format JSON, un format texte léger et lisible, plus compact que XML.

Exemple de message JSON :

{
    "expediteur": "Alice",
    "destinataire": "Bob",
    "texte": "Rendez-vous à 18h ?"
}

Utilisation de la bibliothèque requests

La bibliothèque requests simplifie les requêtes HTTP. L’exemple suivant interroge une API qui renvoie une liste d’images, puis télécharge ces images en parallèle.

import requests
from concurrent.futures import ThreadPoolExecutor


def telecharger(url):
    try:
        reponse = requests.get(url, timeout=10)
        reponse.raise_for_status()
        nom = url.split('/')[-1]
        with open(f'/tmp/{nom}', 'wb') as fichier:
            fichier.write(reponse.content)
    except requests.RequestException as err:
        print(f'Échec pour {url}: {err}')


def main():
    api_url = 'https://api.example.demo/images'
    liens = [element['url'] for element in requests.get(api_url).json()['images']]

    with ThreadPoolExecutor(max_workers=8) as pool:
        pool.map(telecharger, liens)


if __name__ == '__main__':
    main()

Programmation avec les sockets

Un socket est une interface de programmation permettant d’utiliser les services de la couche transport. En Python, le module socket offre plusieurs types : stream (TCP), datagram (UDP) et raw.

Serveur TCP simple

Le serveur suivant écoute sur un port et renvoie la date et l’heure courante à chaque client connecté.

from socket import socket, AF_INET, SOCK_STREAM
from datetime import datetime


def lancer_serveur(hote='0.0.0.0', port=9000):
    serveur = socket(AF_INET, SOCK_STREAM)
    serveur.bind((hote, port))
    serveur.listen(5)
    print(f'Écoute sur {hote}:{port}...')

    while True:
        client, adresse = serveur.accept()
        print(f'Connexion depuis {adresse}')
        message = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        client.sendall(message.encode('utf-8'))
        client.close()


if __name__ == '__main__':
    lancer_serveur()

On peut le tester avec telnet :

telnet localhost 9000

Client TCP correspondant

from socket import socket


def interroger_serveur(hote='localhost', port=9000):
    client = socket()
    client.connect((hote, port))
    donnees = client.recv(1024)
    print(donnees.decode('utf-8'))
    client.close()


if __name__ == '__main__':
    interroger_serveur()

Serveur multi-thread de transfert d’image

Le serveur ci-dessous envoie une image encodée en base64 à chaque client dans un message JSON. Le traitement de chaque client s’effectue dans un thread dédié pour permettre plusieurs connexions simultanées.

import json
from base64 import b64encode
from socket import socket, AF_INET, SOCK_STREAM
from threading import Thread


class GestionnaireClient(Thread):
    def __init__(self, connexion, payload):
        super().__init__()
        self.connexion = connexion
        self.payload = payload

    def run(self):
        enveloppe = {
            'nom_fichier': 'photo.png',
            'contenu': self.payload
        }
        self.connexion.sendall(json.dumps(enveloppe).encode('utf-8'))
        self.connexion.close()


def demarrer_transfert(port=5566, chemin='photo.png'):
    serveur = socket(AF_INET, SOCK_STREAM)
    serveur.bind(('0.0.0.0', port))
    serveur.listen(512)

    with open(chemin, 'rb') as fichier:
        contenu_b64 = b64encode(fichier.read()).decode('utf-8')

    print('Serveur de transfert prêt...')
    while True:
        client, adresse = serveur.accept()
        print(f'Client connecté : {adresse}')
        GestionnaireClient(client, contenu_b64).start()


if __name__ == '__main__':
    demarrer_transfert()

Client récupérant l’image

import json
from base64 import b64decode
from socket import socket


def recevoir_image(hote='localhost', port=5566, sortie='/tmp/photo.png'):
    client = socket()
    client.connect((hote, port))

    tampon = bytearray()
    while True:
        bloc = client.recv(4096)
        if not bloc:
            break
        tampon.extend(bloc)

    message = json.loads(tampon.decode('utf-8'))
    contenu = b64decode(message['contenu'])

    with open(sortie, 'wb') as fichier:
        fichier.write(contenu)

    print(f'Image sauvegardée sous {sortie}")


if __name__ == '__main__':
    recevoir_image()

Base64 permet de représenter des données binaires sous forme de texte. C’est utile quand le format de transport, comme JSON, ne supporte pas directement les octets bruts.

Sockets UDP

UDP fournit un transport sans connexion, sans garantie de remise. Il est pertinent pour les flux audio/vidéo en temps réel où la latence prime sur la fiabilité. Python expose ce type de socket avec SOCK_DGRAM.

Applications réseau courantes

Envoi d’e-mails avec SMTP

Le protocole SMTP permet d’envoyer du courrier. Le module smtplib de Python en simplifie l’usage.

Message texte simple

from smtplib import SMTP
from email.message import EmailMessage


def envoyer_email_simple():
    msg = EmailMessage()
    msg.set_content('Ceci est un message envoyé depuis Python.', charset='utf-8')
    msg['Subject'] = 'Test SMTP'
    msg['From'] = 'expediteur@example.com'
    msg['To'] = 'destinataire@example.com'

    with SMTP('smtp.example.com') as smtp:
        smtp.starttls()
        smtp.login('expediteur@example.com', 'mot_de_passe')
        smtp.send_message(msg)

    print('E-mail envoyé.')


if __name__ == '__main__':
    envoyer_email_simple()

Message avec pièces jointes

from smtplib import SMTP
from email.message import EmailMessage
from pathlib import Path


def envoyer_avec_pieces_jointes():
    msg = EmailMessage()
    msg['Subject'] = 'Rapport mensuel'
    msg['From'] = 'expediteur@example.com'
    msg['To'] = 'destinataire@example.com'
    msg.set_content('Veuillez trouver ci-joint le rapport mensuel.', charset='utf-8')

    fichiers = ['rapport.txt', 'donnees.xlsx']
    for nom in fichiers:
        chemin = Path(nom)
        contenu = chemin.read_bytes()
        msg.add_attachment(
            contenu,
            maintype='application',
            subtype='octet-stream',
            filename=chemin.name
        )

    with SMTP('smtp.example.com') as smtp:
        smtp.starttls()
        smtp.login('expediteur@example.com', 'mot_de_passe')
        smtp.send_message(msg)

    print('E-mail avec pièces jointes envoyé.')


if __name__ == '__main__':
    envoyer_avec_pieces_jointes()

Envoi de SMS via une API HTTP

De nombreuses plateformes proposent des API REST pour l’envoi de SMS. L’exemple ci-dessous effectue une requête POST vers un service générique.

import requests


def envoyer_sms(numero, contenu):
    url = 'https://api.example-sms.com/v1/messages'
    payload = {
        'to': numero,
        'body': contenu,
        'from': 'MonApp'
    }
    headers = {
        'Authorization': 'Bearer CLE_API',
        'Content-Type': 'application/json'
    }

    reponse = requests.post(url, json=payload, headers=headers, timeout=30)
    print(reponse.json())


if __name__ == '__main__':
    envoyer_sms('+33612345678', 'Votre code de vérification est 147258.')

En production, il est recommandé de gérer les erreurs HTTP et de ne jamais inclure de secrets en dur dans le code.

Étiquettes: Python HTTP requests Socket TCP

Publié le 19 juillet à 03h24