« Programmation Python/Threads » : différence entre les versions
Contenu supprimé Contenu ajouté
Aucun résumé des modifications |
|||
Ligne 593 :
=== Programme serveur : suite et fin ===
Les deux classes ci-dessous complètent le script serveur. Le code implémenté dans la classe <code>ThreadClients()</code> est assez similaire à celui que nous avions développé précédemment pour le corps d'application du logiciel de « Chat ». Dans le cas présent, toutefois, nous le plaçons dans une classe dérivée de <code>Thread()</code>, parce que devons faire fonctionner ce code dans un thread indépendant de celui de l'application principale. Celui-ci est en effet déjà complètement accaparé par la boucle <code>mainloop()</code> de l'interface graphique
La classe <code>AppServeur()</code> dérive de la classe <code>AppBombardes()</code> du module ''canon04''. Nous lui avons ajouté un ensemble de méthodes complémentaires destinées à exécuter toutes les opérations qui résulteront du dialogue entamé avec les clients. Nous avons déjà signalé plus haut que les clients instancieront chacun une version dérivée de cette classe (afin de profiter des mêmes définitions de base pour la fenêtre, le canevas, etc.).
<source lang=python line start=138>
class ThreadClients(threading.Thread):
"""objet thread gérant la connexion de nouveaux clients"""
def __init__(self, boss, connex):
threading.Thread.__init__(self)
self.boss = boss # réf. de la fenêtre application
self.connex = connex # réf. du socket initial
def run(self):
"attente et prise en charge de nouvelles connexions clientes"
txt ="Serveur prêt, en attente de requêtes ...\n"
self.boss.afficher(txt)
self.connex.listen(5)
# Gestion des connexions demandées par les clients :
while 1:
nouv_conn, adresse = self.connex.accept()
# Créer un nouvel objet thread pour gérer la connexion :
th = ThreadConnexion(self.boss, nouv_conn)
th.start()
it = th.getName() # identifiant unique du thread
# Mémoriser la connexion dans le dictionnaire :
self.boss.enregistrer_connexion(nouv_conn, it)
# Afficher :
txt = "Client %s connecté, adresse IP %s, port %s.\n" %\
(it, adresse[0], adresse[1])
self.boss.afficher(txt)
# Commencer le dialogue avec le client :
nouv_conn.send("serveur OK")
class AppServeur(AppBombardes):
"""fenêtre principale de l'application (serveur ou client)"""
def __init__(self, host, port, larg_c, haut_c):
# veiller à quitter proprement si l'on referme la fenêtre :
self.bind('<Destroy>',self.fermer_threads)
def specificites(self):
self.avis =Text(st, width =65, height =5)
self.avis.pack(side =LEFT)
self.avis.configure(yscrollcommand =scroll.set)
scroll.pack(side =RIGHT, fill =Y)
st.pack()
self.conn_client = {} # dictionn. des connexions clients
self.verrou =threading.Lock() # verrou pour synchroniser threads
# Initialisation du serveur - Mise en place du socket :
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
connexion.bind((self.host, self.port))
except socket.error:
txt ="La liaison du socket à l'hôte %s, port %s a échoué.\n" %\
self.accueil =None
else:
self.accueil.start()
def depl_aleat_canon(self, id):
"déplacer aléatoirement le canon <id>"
x, y = AppBombardes.depl_aleat_canon(self, id)
for cli in self.conn_client:
message = "mouvement_de,%s,%s,%s," % (id, x, y)
"le canon <i> signale qu'il a atteint l'adversaire <j>"
# Signaler les nouveaux scores à tous les clients :
for cli in self.conn_client:
msg ='scores,'
msg = msg +"%s;%s," % (id, sc)
self.conn_client[cli].send(msg)
def ajouter_canon(self, id):
"instancier un canon et un pupitre de nom <id> dans 2 dictionn."
# on alternera ceux des 2 camps :
if n %2 ==0:
sens = -1
else:
sens = 1
x, y = self.coord_aleat(sens)
coul =('dark blue', 'dark red', 'dark green', 'purple',
'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet')[n]
return (x, y, sens, coul)
def enlever_canon(self, id):
"retirer le canon et le pupitre dont l'identifiant est <id>"
if self.active == 0: # la fenêtre a été refermée
return
def orienter_canon(self, id, angle):
"régler la hausse du canon <id> à la valeur <angle>"
self.pupi[id].reglage(angle)
def tir_canon(self, id):
def enregistrer_connexion(self, conn, it):
"Mémoriser la connexion dans un dictionnaire"
def afficher(self, txt):
"afficher un message dans la zone de texte"
def fermer_threads(self, evt):
"couper les connexions existantes et fermer les threads"
# couper les connexions établies avec tous les clients :
for id in self.conn_client:
self.conn_client[id].send('fin')
# forcer la terminaison du thread serveur qui attend les requêtes :
if self.accueil != None:
self.active =0 # empêcher accès ultérieurs à Tk
if __name__ =='__main__':
AppServeur(host, port, largeur, hauteur).mainloop()
</source>
;Commentaires
Ligne 751 ⟶ 749 :
* Ligne 173 : Il vous arrivera de temps à autre de vouloir « intercepter » l'ordre de fermeture de l'application que l'utilisateur déclenche en quittant votre programme, par exemple parce que vous voulez forcer la sauvegarde de données importantes dans un fichier, ou fermer aussi d'autres fenêtres, etc. Il suffit pour ce faire de détecter l'événement <Destroy>, comme nous le faisons ici pour forcer la terminaison de tous les threads actifs.
* Lignes 179 à 186 : Au passage, voici comment vous pouvez associer une barre de défilement (widget ''Scrollbar'') à un widget ''Text'' (vous pouvez faire de même avec un widget ''Canvas''), sans faire appel à la bibliothèque ''Pmw''<ref>Voir : [[../Tkinter#Installation_des_Python_m.C3.A9ga-widgets|Python Mega Widgets
* Ligne 190 : Instanciation de l'obet « verrou » permettant de synchroniser les threads.
Ligne 769 ⟶ 767 :
C'est dans la méthode <code>run()</code> de la classe <code>ThreadSocket()</code> (lignes 86 à 126) que se trouve le code traitant les messages échangés avec le serveur. Nous y avons d'ailleurs laissé une instruction print (à la ligne 88) afin que les messages reçus du serveur apparaissent sur la sortie standard. Si vous réalisez vous-même une forme plus définitive de ce jeu, vous pourrez bien évidemment supprimer cette instruction.
<source lang=python line>
#######################################################
# Jeu des bombardes - partie cliente #
# (C) Gérard Swinnen, Liège (Belgique) - Juillet 2004 #
# Licence : GPL #
# Avant d'exécuter ce script, vérifiez que l'adresse, #
# le numéro de port et les dimensions de l'espace de #
# jeu indiquées ci-dessous correspondent exactement #
# à ce qui a été défini pour le serveur. #
#######################################################
from Tkinter import *
import socket, sys, threading, time
from canon_serveur import Canon, Pupitre, AppServeur
host, port = '192.168.0.235', 35000
largeur, hauteur = 700, 400 # dimensions de l'espace de jeu
class AppClient(AppServeur):
def __init__(self, host, port, larg_c, haut_c):
AppServeur.__init__(self, host, port, larg_c, haut_c)
def specificites(self):
"préparer les objets spécifiques de la partie client"
self.master.title('<<< Jeu des bombardes >>>')
self.connex =ThreadSocket(self, self.host, self.port)
self.connex.start()
self.id =None
def ajouter_canon(self, id, x, y, sens, coul):
"instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires"
self.pupi[id] = Pupitre(self, self.guns[id])
self.pupi[id].inactiver()
def activer_pupitre_personnel(self, id):
self.id =id # identifiant reçu du serveur
def tir_canon(self, id):
r = self.guns[id].feu() # renvoie False si enrayé
if r and id == self.id:
def imposer_score(self, id, sc):
self.pupi[id].valeur_score(int(sc))
def deplacer_canon(self, id, x, y):
"note: les valeurs de x et y sont reçues en tant que chaînes"
self.guns[id].deplacer(int(x), int(y))
def orienter_canon(self, id, angle):
self.connex.signaler_angle(angle)
else:
self.pupi[id].reglage(angle)
def fermer_threads(self, evt):
"couper les connexions existantes et refermer les threads"
self.connex.terminer()
self.active =0 # empêcher accès ultérieurs à Tk
def depl_aleat_canon(self, id):
pass # => méthode inopérante
pass # => méthode inopérante
class ThreadSocket(threading.Thread):
"""objet thread gérant l'échange de messages avec le serveur"""
def __init__(self, boss, host, port):
threading.Thread.__init__(self)
self.app = boss # réf. de la fenêtre application
# Mise en place du socket - connexion avec le serveur :
self.connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connexion.connect((host, port))
except socket.error:
print "La connexion a échoué."
sys.exit()
while 1:
print "*%s*" % msg_recu
# le message reçu est d'abord converti en une liste :
if t[0] =="" or t[0] =="fin":
self.connexion.send("client OK")
elif t[0] =="canons":
self.connexion.send("OK") # accusé de réception
# éliminons le 1er et le dernier élément de la liste.
# ceux qui restent sont eux-mêmes des listes :
# chacune est la description complète d'un canon :
elif t[0] =='angle':
# éliminons le 1er et le dernier élément de la liste.
elif t[0] =="mouvement_de":
# Le thread <réception> se termine ici.
print "Client arrêté. Connexion interrompue."
self.connexion.close()
def signaler_tir(self):
def signaler_angle(self, angle):
self.connexion.send('orienter,%s,' % angle)
def terminer(self):
self.connexion.send('fin')
# Programme principal :
if __name__ =='__main__':
AppClient(host, port, largeur, hauteur).mainloop()
</source>
;Commentaires
Ligne 933 ⟶ 928 :
{{Exercices}}
#Simplifiez le script correspondant au client de « chat » décrit à la page 287
#Modifiez le jeu des bombardes (version monoposte) du chapitre 15 (voir pages 229
{{solution}}
<ol>
Ligne 1 149 ⟶ 1 144 :
Vous devez bien comprendre que pendant l'écoulement de l'intervalle de temps programmé à l'aide de la méthode <code>after()</code>, votre application n'est pas du tout « figée ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenêtre, effectuer une entrée clavier, etc. Comment cela est-il rendu possible ?
Nous avons mentionné déjà à plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tâche de fond : ce dispositif se met en route lorsque vous activez la méthode <code>mainloop()</code> de votre fenêtre principale. Comme son nom l'indique fort bien, cette méthode met en oeuvre une boucle répétitive perpétuelle, du même type que les boucles <code>while</code> que vous connaissez bien. De nombreux mécanismes sont intégrés à ce « moteur ». L'un d'entre eux consiste à réceptionner tous les événements qui se produisent, et à les signaler ensuite à l'aide de messages appropriés aux programmes qui en font la demande (voir : Programmes pilotés par des événements
La technique d'animation utilisant la méthode <code>after()</code> est la seule possible pour une application fonctionnant toute entière sur un seul thread, parce que c'est la boucle <code>mainloop()</code> qui dirige l'ensemble du comportement d'une telle application de manière absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenêtre chaque fois que cela s'avère nécessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redéfinirait les coordonnées d'un objet graphique à l'intérieur d'une simple boucle <code>while</code>, par exemple, parce que pendant tout ce temps l'exécution de <code>mainloop()</code> resterait suspendue, ce qui aurait pour conséquence que pendant tout ce temps aucun objet graphique ne serait redessiné (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaîtrait figée, aussi longtemps que la boucle while ne serait pas interrompue.
Ligne 1 173 ⟶ 1 168 :
[[Image:Apprendre à programmer avec Python 71.png|center|capture d'écran de l'application]]
<source lang=python line>
from Tkinter import *
from math import sin, cos
import time, threading
class App(Frame):
def __init__(self):
Frame.__init__(self)
self.pack()
can =Canvas(self, width =400, height =400,
bg ='ivory', bd =3, relief =SUNKEN)
can.pack(padx =5, pady =5)
cercle = can.create_oval(185, 355, 215, 385, fill ='red')
# arrêter l'autre thread si l'on ferme la fenêtre :
self.bind('<Destroy>', tb.stop)
class Thread_balle(threading.Thread):
def __init__(self, canevas, dessin):
threading.Thread.__init__(self)
self.anim =1
x, y = 200 + 170*sin(a), 200 +170*cos(a)
self.can.coords(self.dessin, x-15, y-15, x+15, y+15)
self.anim =0
App().mainloop()
</source>
;Commentaires
Ligne 1 220 ⟶ 1 213 :
* Ligne 15 : Vous ne pouvez par redémarrer un thread qui s'est terminé. De ce fait, vous ne pouvez lancer cette animation qu'une seule fois (tout au moins sous la forme présentée ici). Pour vous en convaincre, activez la ligne n° 15 en enlevant le caractère <code>#</code> situé au début (et qui fait que Python considère qu'il s'agit d'un simple commentaire) : lorsque l'animation est lancée, un clic de souris sur le bouton ainsi mis en place provoque la sortie de la boucle <code>while</code> des lignes 27-31, ce qui termine la méthode <code>run()</code>. L'animation s'arrête, mais le thread qui la gérait s'est terminé lui aussi. Si vous essayez de le relancer à l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'erreur.
* Lignes 26 à 31 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuellement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les coordonnées x et y du point de la circonférence qui correspond à cet angle
== Notes ==
|