User Tools

Site Tools


rx3:index

Réseau L2 Info : Socket en Python 3

Tips en Python

String vs Byte-Array

Les fonctions de la famille send()/recv() ne manipulent pas des string classiques, mais des byte-array :

string = "coucou"     # string classique       
byterray = b"coucou"  # byte array (notez le prefixe b)
sock.send(bytearray)

Pour convertir une string en byte-array (et inversement), vous pouvez utiliser les fonctions suivantes :

bytearray = "coucou".encode()
string = b"coucou".decode()

Pour convertir une string “bonjour la terre” en tableau de mots [ “bonjour”, “la”, “terre” ], la fonction split() est votre amie :

sentence = "bonjour la terre"
words = sentence.split(" ")   # avec " " comme séparateur  

A l'inverse, si vous disposez d'un tableau de mots, vous pouvez utiliser join() pour effectuer l'opération inverse :

sentence = " ".join(words)
Un mot sur les classes...

Un mot maintenant sur les classes en Python… Plûtot qu'un long discours, voici un exemple de classe :

class Dog:
 
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
    def addYear(self):
        self.age += 1
 
    def print(self):
        print("I am a dog. My name is {} and I am {} years old.".format(self.name,self.age))
 

A la manière d'une structure, une classe encapsule des données (name et age). De plus, la classe permet de définir des méthodes (addYear et print), qui sont des fonctions permettant de manipuler les données de la classe (au travers la variable self, qui désigne l'objet courant).

Un objet est une instance particulière de la classe, que l'on peut créer en appellant le constructeur Dog(). Ce constructeur appelle implcitement la méthode init de la classe. Dans l'exemple ci-dessous, deux objets de type Dog sont créés et affectés aux variables milou et rantanplan.

milou = Dog("milou", 4)
rantanplan = Dog("rantanplan", 5)
milou.print()
rantanplan.addYear()
rantanplan.print()

Pour aller plus loin : https://docs.python.org/fr/3.5/tutorial/classes.html

Un mot sur le flux TCP !

Considérons deux sockets TCP s1 et s2, correspondant à une connexion déjà établie entre un client et un serveur. La gestion du flux TCP implique un mécanisme de bufferisation à l'envoi et à la réception.

# client             # server
s1.send(b"A")        msg1 = s2.recv(1000) # recv 1000 bytes max
s1.send(b"B")        msg2 = s2.recv(1000) # recv 1000 bytes max

On souhaiterait récupérer b”A” dans msg1 et b”B” dans msg2. Hélas, il y a quelques difficultés liés à la notion de flux (ou stream) en TCP.

Tout d'abord, le mécanisme de bufferisation à l'envoi va faire en sorte, qu'il n'y aura en fait qu'un seul message b”AB” envoyé sur le réseau… Pour éviter ce problème, il faut remplacer l'utilisation de la fonction send() par la fonction sendall() !

# client             # server
s1.sendall(b"A")     msg1 = s2.recv(1000) 
s1.sendall(b"B")     msg2 = s2.recv(1000)

Côté réception, le mécanisme de bufferisation pose également des difficultés. En effet, comme les deux envois sont très proches, il est fort probable qu'ils soient bufferisés à la réception avant l'appel du premier recv(). Dans ce cas, msg1 contiendra b”AB”.

Une astuce pour contourner ce problème consiste à acquiter les messages :

# client             # server
s1.sendall(b"A")     msg1 = s2.recv(1000)
ack = s1.recv(1000)  s2.send(b"ACK") 
s1.sendall(b"B")     msg2 = s2.recv(1000)
ack = s1.recv(1000)  s2.send(b"ACK")

La fonction recv() étant bloquante, l'envoi du second message b”B” ne pourra avoir lieu qu'après la réception du premier… Ainsi, on reçoit bien les deux messages séparemment comme attendu… Ouf :-)

Recv non bloquant

Par défaut, la fonction recv() des sockets est bloquante… Plus précisément, la fonction recv() se met en attente jusqu'à la réception effective de données. Cela peut poser quelques difficultés dans notre jeu, notamment si l'on considère comment communiquer le déplacement des joueurs. En effet, un recv() bloquant côté serveur risque de bloquer indéfiniment la boucle principale du serveur ! Comment faire ?

Une première solution consiste à utiliser des sockets non bloquante, mais cela implique de gérer une exception dans le cas où il n'y a rien à recevoir !

import errno
sock = ...
sock.setBlocking(False)
try:
    msg = sock.recv(1000)
except socket.error as e:
    if e.args[0] == errno.EWOULDBLOCK:
        print("nothing received")
    else
        print("error:", e)

Une autre façon (plus élégante) consiste à utiliser select() avec un timeout de 0 !

import select
sock = ...
sock.setBlocking(False)
lsock, _, _ = select.select([sock], [], [], 0)
if lsock:
    msg = sock.recv(1000)
else:
    print("nothing received")

Client DayTime en UDP

#!/usr/bin/python3
import sys
import socket
 
HOST = 'time-c.nist.gov'
PORT = 13                  # daytime  
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'', (HOST,PORT))
d = s.recvfrom(1024)
reply = d[0]
addr = d[1]
print 'Server reply : ' + reply
s.close()
print ('Received', data)

Client HTTP/GET (TCP)

#!/usr/bin/python3
import sys
import socket
 
HOST = 'www.labri.fr'     
PORT = 80                 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(b'GET /\r\n\r\n')
data = s.recv(1024)
s.close()
print ('Received', data)

Serveur Echo UDP

Voici un echo server en version UDP…

#!/usr/bin/python3
import socket
import sys
 
HOST = ''                 # Symbolic name meaning all available interfaces
PORT = 7777               # Arbitrary non-privileged port
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)    # ipv4 only
# s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # ipv4/ipv6
s.bind((HOST, PORT))
 
while True:
  reply, addr = s.recvfrom(1500)
  print (reply)  
  s.sendto(reply, addr)

On utilise netstat pour vérifier que son serveur écoute :

netstat -ulpn
  Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
  udp        0      0 0.0.0.0:7777            0.0.0.0:*                           3314/python3        

Voici un client netcat ipv4/udp:

nc -4 -u localhost 7777
 coucou
 coucou

Serveur TCP

Voici un echo server en version TCP…

#!/usr/bin/python3
import socket
 
HOST = ''                 # Symbolic name meaning all available interfaces
PORT = 7777               # Arbitrary non-privileged port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(0)
while True:
    sclient, addr = s.accept()
    print('Connected by', addr)
    while True:
        data = sclient.recv(1500)
        if data == b'' or data == b'\n' : break
        print(data)
        sclient.sendall(data)
    print('Disconnected by', addr)        
    sclient.close()

Serveur Echo TCP (version select)

La version select permet de gérer de multiples clients simultanément.

#!/usr/bin/python3
import socket
import select
 
HOST = ''
PORT = 7777
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
l = []
l.append(s)
m = {}
 
while True:
    l2, _, _ = select.select(l, [], [])
    for s2 in l2:
        # socket server (new client connection)
        if s2 == s:  
            sclient, addr = s.accept()
            l.append(sclient)
            m[sclient] = addr
            print('Connected by', addr)
        # socket client (new client message)
        else:        
            while True:
                data = s2.recv(1500)
                print(data)
                if data == b'' or data == b'\n' :
                    print('Disconnected by', m[s2])
                    l.remove(s2)
                    s2.close()
                    break
                s2.sendall(data)                  
s.close()
rx3/index.txt · Last modified: 2018/04/06 10:03 by orel