====== Réseau L2 Info : Socket en Python 3 ======
Un peu de documentation :
* https://docs.python.org/3/howto/sockets.html
* https://docs.python.org/3/library/socket.html
* https://docs.python.org/3/library/select.html
* https://docs.python.org/3/library/threading.html
* https://docs.python.org/3/library/stdtypes.html#str
==== 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 socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("www.perdu.com", 80))
s.sendall(b'GET / HTTP/1.1\r\nHost: www.perdu.com\r\nConnection: close\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 TCP (version multithread) ====
#!/usr/bin/python3
import socket
import threading
def handle(addr, sclient):
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()
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(0)
while True:
sclient, addr = s.accept()
t = threading.Thread(None, handle, None, (addr, sclient))
t.start()
==== 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()