Python réseau socket

Le sujet de ce chapitre est de pouvoir utiliser un service sur une machine distante.

Comment communiquent des machines distantes?

Des machines distantes peuvent communiquer entre elles grâce à leur adresse IP. Une machine cliente (c'est à dire qui demande un service) contacte une machine serveur qui répondra à sa demande. On a donc une logique de client-serveur. L'un fait une demande, l'autre lui apporte une réponse.

Comment atteindre le bon service?

Un serveur peut cependant héberger plusieurs services. Par exemple un serveur peut héberger un serveur web mais également un serveur de messagerie.

Alors comment se connecter au bon service? En utilisant les ports. Les ports les plus connus sont 21 pour le FTP, 80 pour le HTTP, 443 pour le HTTPS, le 22 pour le SSH, 110 pour le service POP, etc.

Si vous voulez voir sur quels ports tournent vos services vous pouvez exécuter la commande suivante:

sudo cat /etc/services

L'idée c'est que le client fasse une demande de ce type: 192.168.0.1:8888 (adresse IP 192.168.0.1 et port 8888) puis de créer un lien entre ce port 8888 et notre programme.

Pour réaliser ce besoin on utilise des stockets.

Un socket c'est quoi?

En anglais un socket est un "trou" qui laisse passer des choses, comme une prise électrique, l'emplacement du processeur, ou une bouche -on va s'arrêter la pour les exemples -

Le socket est donc dans notre cas une association au niveau de l'OS entre un programme qui tourne en boucle et le port de la machine qui lui a été dédié. On dit d'ailleurs que le programme écoute le port qui lui a été réservé. Il écoute le port et répond aux demandes faites par ce port.

Socket et python

Pour comprendre le fonctionnement des sockets avec python, nous allons travailler avec deux fichiers: server.py et client.py
Le premier script sera -comme son nom l'indique- sur le serveur et écoutera les demandes des clients.
Le script client.py sera donc exécuté sur la machine cliente, c'est lui qui fera la demande du service du serveur distant.

Inutile de préciser que la logique client/serveur fonctionne également sur une même machine.

server.py

#!/usr/bin/env python
# coding: utf-8

import socket

socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.bind(('', 15555))

while True:
        socket.listen(5)
        client, address = socket.accept()
        print "{} connected".format( address )

        response = client.recv(255)
        if response != "":
                print response

print "Close"
client.close()
stock.close()

client.py

#!/usr/bin/env python
# coding: utf-8

import socket

hote = "localhost"
port = 15555

socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.connect((hote, port))
print "Connection on {}".format(port)

socket.send(u"Hey my name is Olivier!")

print "Close"
socket.close()

Si vous exécutez ces deux programmes vous verrez donc la demande du client se réaliser côté serveur.

Socket et Threading

Le problème de l'exemple précédent est que chaque connection doit être traitée avant de pouvoir passer à la suivante. Si pour un socket maison cela peut être acceptable, pour des projets ambitieux il est plutôt conseillé de passer par des threading.

Exemple:

server.py

#!/usr/bin/env python
# coding: utf-8 

import socket
import threading

class ClientThread(threading.Thread):

    def __init__(self, ip, port, clientsocket):

        threading.Thread.__init__(self)
        self.ip = ip
        self.port = port
        self.clientsocket = clientsocket
        print("[+] Nouveau thread pour %s %s" % (self.ip, self.port, ))

    def run(self): 
   
        print("Connection de %s %s" % (self.ip, self.port, ))

        r = self.clientsocket.recv(2048)
        print("Ouverture du fichier: ", r, "...")
        fp = open(r, 'rb')
        self.clientsocket.send(fp.read())

        print("Client déconnecté...")

tcpsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcpsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcpsock.bind(("",1111))

while True:
    tcpsock.listen(10)
    print( "En écoute...")
    (clientsocket, (ip, port)) = tcpsock.accept()
    newthread = ClientThread(ip, port, clientsocket)
    newthread.start()

client.py

#!/usr/bin/env python
# coding: utf-8

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("", 1111))

print("Le nom du fichier que vous voulez récupérer:")
file_name = input(">> ") # utilisez raw_input() pour les anciennes versions python
s.send(file_name.encode())
file_name = 'data/%s' % (file_name,)
r = s.recv(9999999)
with open(file_name,'wb') as _file:
    _file.write(r)
print("Le fichier a été correctement copié dans : %s." % file_name)

Alors que font ces deux scripts? Le script server.py a pour vocation d'être exécuté en boucle pour pouvoir écouter le port 1111. Ce genre de script peut être implémenté comme daemon, c'est à dire un script qui tourne en arrière-plan comme le ferait un serveur web par exemple.

Une fois le script server.py lancé sur le serveur, vous pouvez exécuter le script client.py - sur votre machine cliente - qui demandera à accéder au service du serveur via le port 1111.

Un input demande alors quel fichier l'utilisateur veut copier du serveur vers sa machine ; ce nom est envoyé aussitôt au serveur. Ce dernier envoie le contenu du fichier au client qui récupère ce contenu pour créer une copie du fichier dans le dossier data.

On utilise le module threading pour pouvoir gérer plusieurs demandes en même temps. Dans le cas contraire il faudrait attendre que chaque demande soit executée dans l'ordre pour pouvoir profiter du service.

Les méthodes de l'objet socket:

accept()                               : accepte une connection, retourne un nouveau socket et une adresse client 
bind(addr)                             : associe le socket à une adresse locale
close()                                : ferme le socket
connect(addr)                          : connecte le socket à une adresse distante 
connect_ex(addr)                       : connect, retourne un code erreur au lieu d'une exception
dup()                                  : retourne un nouveau objet socket identique à celui en cours
fileno()                               : retourne une description du fichier
getpeername()                          : retourne l'adresse distante
getsockname()                          : retourne l'adresse locale
getsockopt(level, optname[, buflen])   : retourne les options du socket
gettimeout()                           : retourne le timeout ou none
listen(n)                              : commence à écouter les connections entrantes
makefile([mode, [bufsize]])            : retourne un fichier objet pour le socket
recv(buflen[, flags])                  : recoit des données
recv_into(buffer[, nbytes[, flags]])   : recoit des données (dans un buffer)
recvfrom(buflen[, flags])              : reçoit des données et l'adresse de l'envoyeur
recvfrom_into(buffer[,nbytes,[,flags]) : reçoit des données et l'adresse de l'envoyeur (dans un buffer)
sendall(data[, flags])                 : envoye toutes les données
send(data[, flags])                    : envoye des données mais il se peut que pas toutes le soit
sendto(data[, flags], addr)            : envoye des données à une adresse donnée
setblocking(0 | 1)                     : active ou désactive le blocage le flag I/O
setsockopt(level, optname, value)      : définit les options du socket
settimeout(None | float)               : active ou désactive le timeout
shutdown(how)                          : fermer les connections dans un ou les deux sens.