Les sockets et la programmation réseau

Le module socket permet de programmer des accès réseau bas niveau en Python. Dans ce chapitre, nous présenterons uniquement l’utilisation des sockets TCP/IP mais le module socket permet, en utilisant la même API, de créer et d’utiliser des sockets Unix, des sockets Bluetooth…

L’API socket suppose une architecture client/serveur. Le serveur est en attente d’une connexion d’un client. Des données peuvent être lues et écrites aussi bien par le client que par le serveur.

Exemple d’implémentation d’un serveur

Pour implémenter un serveur, il suffit d’appeler la fonction socket() en spécifiant le type. Les constantes socket.AF_INET et socket.SOCK_STREAM passées en paramètres permettent de spécifier respectivement que l’on veut créer une socket IP pour une transmission par paquets, c’est-à-dire une socket TCP.

Une socket doit être fermée après usage. À la ligne 6, la socket est crée dans une instruction with pour s’assurer qu’elle sera fermée à la fin du bloc.

On appelle les méthodes suivantes :

setsockopt()

Pour spécifier les paramètres pour la socket. Ici, on passe 1 pour la valeur socket.SO_REUSEADDR afin d’indiquer au système que le port utilisé par la socket peut être immédiatement réutilisé dès que cette dernière est fermée. Sinon, le système temporise la réutilisation et la réexécution du programme peut entraîner un échec.

bind()

Pour spécifier sous la forme d’un n-uplet le nom ou l’adresse de l’hôte et le port utilisés par cette socket.

listen()

Pour spécifier qu’il s’agit d’une socket serveur et le nombre de connexions en attente qui peuvent être tolérées avant rejet par le système.

accept()

Cet appel suspend l’exécution du thread jusqu’à ce qu’une connexion entrante se produise pour cette socket. Alors, le programme est réveillé et cette méthode retourne une socket de communication avec le client ainsi que l’adresse du client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import socket

host = 'localhost'
port = 8000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((host, port))
    s.listen(1)
    while True:
        conn, address = s.accept()
        with conn:
            buff = conn.recv(512)
            message = buff.decode('utf-8')
            conn.sendall(f"echo : {message}".encode('utf-8'))

À partir de la socket de connexion du client, il est possible de lire les données reçues grâce à la méthode recv() et de répondre avec la méthode send(). Les données lues et écrites par une socket sont des objets de type bytes.

Exemple d’implémentation d’un client

On utilise la même fonction socket() pour créer une socket cliente. Pour le client, on appelle la méthode :

connect()

Pour se connecter au serveur en indiquant son adresse et son port.

1
2
3
4
5
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((host, port))
    s.sendall("Hello world".encode('utf-8'))
    buff = s.recv(512)
    print(buff.decode())

À partir de la socket client, il est possible d’envoyer des données avec la méthode send() et de lire la réponse du serveur grâce à la méthode recv(). Les données lues et écrites par une socket sont des objets de type bytes.

Exemple d’un serveur multi-thread

Il est possible d’améliorer l’implémentation du serveur en créant des worker threads pour prendre en charge la connexion de chaque client. Il est ainsi possible pour le serveur de traiter simultanément avec plusieurs clients. On peut utiliser le module multiprocessing et sa classe Pool qui gère en groupe de threads réutilisables :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import socket, multiprocessing

host = 'localhost'
port = 8000
nb_workers = 10

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((host, port))
    s.listen(1)
    with multiprocessing.Pool(nb_workers) as pool:
        while True:
            conn, address = s.accept()
            pool.apply(handle, (conn, address))

def handle(conn, address):
    with conn:
        buff = conn.recv(512)
        message = buff.decode('utf-8')
        conn.sendall(f"echo : {message}".encode('utf-8'))