====== 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()