« Programmation Python/Threads » : différence entre les versions

Contenu supprimé Contenu ajouté
Tavernier (discussion | contributions)
pause
Tavernier (discussion | contributions)
pause
Ligne 95 :
* Lignes 20 à 22 : Troisième étape. Le socket étant relié à un port de communication, il peut à présent se préparer à recevoir les requêtes envoyées par les clients. C'est le rôle de la méthode <code>listen()</code>. L'argument qu'on lui transmet indique le nombre maximum de connexions à accepter en parallèle. Nous verrons plus loin comment gérer celles-ci.
 
* Lignes 24 à 26 : Quatrième étape. Lorsqu'on fait appel à sa méthode <code>accept()</code>, le socket attend indéfiniment qu'une requête se présente. Le script est donc interrompu à cet endroit, un peu comme il le serait si nous faisions appel à une fonction <code>input()</code> pour attendre une entrée clavier. Si une requête est réceptionnée, la méthode <code>accept()</code> renvoie un tuple de deux éléments : le premier est la référence d'un nouvel objet de la classe <code>socket()</code><ref>Nous verrons plus loin l'utilité de créer ainsi un nouvel objet socket pour prendre en charge la communication, plutôt que d'utiliser celui qui a déjà créé à la ligne 10. En bref, si nous voulons que notre serveur puisse prendre en charge simultanément les connexions de plusieurs clients, il nous faudra disposer d'un socket distinct pour chacun d'eux, indépendamment du premier que l'on laissera fonctionner en permanence pour réceptionner les requêtes qui continuent à arriver en provenance de nouveaux clients. </ref>, qui sera la véritable interface de communication entre le client et le serveur, et le second un autre tuple contenant les coordonnées de ce client (son adresse IP et le n° de port qu'il utilise lui-même).
 
* Lignes 28 à 30 : Cinquième étape. La communication proprement dite est établie. Les méthodes <code>send()</code> et <code>recv()</code> du socket servent évidemment à l'émission et à la réception des messages, qui doivent être de simples chaînes de caractères.<br />''Remarques'' : la méthode <code>send()</code> renvoie le nombre d'octets expédiés. L'appel de la méthode <code>recv()</code> doit comporter un argument entier indiquant le nombre maximum d'octets à réceptionner en une fois (Les octets surnuméraires sont mis en attente dans un tampon. Ils sont transmis lorsque la même méthode recv() est appelée à nouveau).
Ligne 107 :
Le script ci-dessous définit un logiciel client complémentaire du serveur décrit dans les pages précédentes. On notera sa grande simplicité.
 
{{todo|num linéaire}}
 
 
<pre>
1.# Définition d'un client réseau rudimentaire
2.# Ce client dialogue avec un serveur ad hoc
Ligne 140 ⟶ 144 :
32.print "Connexion interrompue."
33.mySocket.close()
</pre>
 
;Commentaires
 
* Le début du script est similaire à celui du serveur. L'adresse IP et le port de communication doivent être ceux du serveur.
 
Lignes 12 à 18 : On ne crée cette fois qu'un seul objet socket, dont on utilise la méthode connect() pour envoyer la requête de connexion.
* Lignes 12 à 18 : On ne crée cette fois qu'un seul objet socket, dont on utilise la méthode <code>connect()</code> pour envoyer la requête de connexion.
Lignes 20 à 33 : Une fois la connexion établie, on peut dialoguer avec le serveur en utilisant les méthodes send() et recv() déjà décrites plus haut pour celui-ci.
 
* Lignes 20 à 33 : Une fois la connexion établie, on peut dialoguer avec le serveur en utilisant les méthodes <code>send()</code> et <code>recv()</code> déjà décrites plus haut pour celui-ci.
 
== Gestion de plusieurs tâches en parallèle à l'aide des threads ==
 
Le système de communication que nous avons élaboré dans les pages précédentes est vraiment très rudimentaire : d'une part il ne met en relation que deux machines seulement, et d'autre part il limite la liberté d'expression des deux interlocuteurs. Ceux-ci ne peuvent en effet envoyer des messages que chacun à leur tour. Par exemple, lorsque l'un d'eux vient d'émettre un message, son système reste bloqué tant que son partenaire ne lui a pas envoyé une réponse. Lorsqu'il vient de recevoir une telle réponse, son système reste incapable d'en réceptionner une autre, tant qu'il n'a pas entré lui-même un nouveau message, ... et ainsi de suite.
 
Tous ces problèmes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose à la fois. Lorsque le flux d'instructions rencontre une fonction input(), par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnée attendue. Et même si cette attente dure très longtemps, il n'est habituellement pas possible que le programme effectue d'autres tâches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et même programme : vous savez certainement que vous pouvez exécuter d'autres applications entretemps sur votre ordinateur, car les systèmes d'exploitation modernes sont « multi-tâches ».
Tous ces problèmes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose à la fois. Lorsque le flux d'instructions rencontre une fonction <code>input()</code>, par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnée attendue. Et même si cette attente dure très longtemps, il n'est habituellement pas possible que le programme effectue d'autres tâches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et même programme : vous savez certainement que vous pouvez exécuter d'autres applications entretemps sur votre ordinateur, car les systèmes d'exploitation modernes sont ''multi-tâches''.
Les pages qui suivent sont destinées à vous expliquer comment vous pouvez introduire cette fonctionnalité multi-tâches dans vos programmes, afin que vous puissiez développer de véritables applications réseau, capables de communiquer simultanément avec plusieurs partenaires.
 
Veuillez à présent considérer le script de la page précédente. Sa fonctionnalité essentielle réside dans la boucle while des lignes 23 à 29. Or, cette boucle s'interrompt à deux endroits :
Les pages qui suivent sont destinées à vous expliquer comment vous pouvez introduire cette fonctionnalité multi-tâches dans vos programmes, afin que vous puissiez développer de véritables applications réseau, capables de communiquer simultanément avec plusieurs partenaires.
à la ligne 27, pour attendre les entrées clavier de l'utilisateur (fonction raw_input()) ;
 
à la ligne 29, pour attendre l'arrivée d'un message réseau.
Veuillez à présent considérer le script de la page précédente. Sa fonctionnalité essentielle réside dans la boucle <code>while</code> des lignes 23 à 29. Or, cette boucle s'interrompt à deux endroits :
Ces deux attentes sont donc successives, alors qu'il serait bien plus intéressant qu'elles soient simultanées. Si c'était le cas, l'utilisateur pourrait expédier des messages à tout moment, sans devoir attendre à chaque fois la réaction de son partenaire. Il pourrait également recevoir n'importe quel nombre de messages, sans l'obligation d'avoir à répondre à chacun d'eux pour recevoir les autres.
* À la ligne 27, pour attendre les entrées clavier de l'utilisateur (fonction raw_input()) ;
Nous pouvons arriver à ce résultat si nous apprenons à gérer plusieurs séquences d'instructions en parallèle au sein d'un même programme. Mais comment cela est-il possible ?
 
* À la ligne 29, pour attendre l'arrivée d'un message réseau.
 
Ces deux attentes sont donc ''successives'', alors qu'il serait bien plus intéressant qu'elles soient ''simultanées''. Si c'était le cas, l'utilisateur pourrait expédier des messages à tout moment, sans devoir attendre à chaque fois la réaction de son partenaire. Il pourrait également recevoir n'importe quel nombre de messages, sans l'obligation d'avoir à répondre à chacun d'eux pour recevoir les autres.
 
Nous pouvons arriver à ce résultat si nous apprenons à gérer plusieurs séquences d'instructions ''en parallèle'' au sein d'un même programme. Mais comment cela est-il possible ?
 
Au cours de l'histoire de l'informatique, plusieurs techniques ont été mises au point pour partager le temps de travail d'un processeur entre différentes tâches, de telle manière que celles-ci paraissent être effectuées en même temps (alors qu'en réalité le processeur s'occupe d'un petit bout de chacune d'elles à tour de rôle). Ces techniques sont implémentées dans le système d'exploitation, et il n'est pas nécessaire de les détailler ici, même s'il est possible d'accéder à chacune d'elles avec Python.
 
Dans les pages suivantes, nous allons apprendre à utiliser celle de ces techniques qui est à la fois la plus facile à mettre en oeuvre, et la seule qui soit véritablement portable (elle est en effet supportée par tous les grands systèmes d'exploitation) : on l'appelle la technique des processus légers ou threads3.
Dans les pages suivantes, nous allons apprendre à utiliser celle de ces techniques qui est à la fois la plus facile à mettre en oeuvre, et la seule qui soit véritablement portable (elle est en effet supportée par tous les grands systèmes d'exploitation) : on l'appelle la technique des processus légers ou threads<ref>Dans un système d'exploitation de type Unix (comme Linux), les différents ''threads'' d'un même programme font partie d'un seul ''processus''. Il est également possible de gérer différents processus à l'aide d'un même script Python (opération ''fork''), mais l'explication de cette technique dépasse largement le cadre de ce cours.</ref>.
 
Dans un programme d'ordinateur, les threads sont des flux d'instructions qui sont menés en parallèle (quasi-simultanément), tout en partageant le même espace de noms global.
 
En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le thread principal. À partir de celui-ci, d'autres threads « enfants » peuvent être amorcés, qui seront exécutés en parallèle. Chaque thread enfant se termine et disparaît sans autre forme de procès lorsque toutes les instructions qu'il contient ont été exécutées. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui.
En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le ''thread principal''.
 
À partir de celui-ci, d'autres threads « enfants » peuvent être amorcés, qui seront exécutés en parallèle. Chaque thread enfant se termine et disparaît sans autre forme de procès lorsque toutes les instructions qu'il contient ont été exécutées. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui.
 
== Client gérant l'émission et la réception simultanées ==
 
Nous allons maintenant mettre en pratique la technique des threads pour construire un système de « chat »4<ref>Le ''chat'' est l'occupation qui consiste à « papoter » par l'intermédiaire d'ordinateurs. Les canadiens francophones ont proposé le terme de ''clavardage'' pour désigner ce « bavardage par claviers interposés ».</ref> simplifié. Ce système sera constitué d'un seul serveur et d'un nombre quelconque de clients. Contrairement à ce qui se passait dans notre premier exercice, personne n'utilisera le serveur lui-même pour communiquer, mais lorsque celui-ci aura été mis en route, plusieurs clients pourront s'y connecter et commencer à s'échanger des messages.
 
Chaque client enverra tous ses messages au serveur, mais celui-ci les ré-expédiera immédiatement à tous les autres clients connectés, de telle sorte que chacun puisse voir l'ensemble du trafic. Chacun pourra à tout moment envoyer ses messages, et recevoir ceux des autres, dans n'importe quel ordre, la réception et l'émission étant gérées simultanément, dans des threads séparés.
Le script ci-après définit le programme client. Le serveur sera décrit un peu plus loin. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire à celle de l'exemple précédent. Seule la partie « Dialogue avec le serveur » a été remplacée. Au lieu d'une boucle while, vous y trouvez à présent les instructions de création de deux objets threads (aux lignes 49 et 50), dont on démarre la fonctionnalité aux deux lignes suivantes. Ces objets threads sont crées par dérivation, à partir de la classe Thread() du module threading. Ils s'occuperont indépendamment de la réception et le l'émission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsulés dans des objets distincts, ce qui facilite la compréhension du mécanisme.
 
Le script ci-après définit le programme client. Le serveur sera décrit un peu plus loin. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire à celle de l'exemple précédent. Seule la partie « Dialogue avec le serveur » a été remplacée. Au lieu d'une boucle <code>while</code>, vous y trouvez à présent les instructions de création de deux objets threads (aux lignes 49 et 50), dont on démarre la fonctionnalité aux deux lignes suivantes. Ces objets threads sont crées par dérivation, à partir de la classe <code>Thread()</code> du module ''threading''. Ils s'occuperont indépendamment de la réception et le l'émission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsulés dans des objets distincts, ce qui facilite la compréhension du mécanisme.
 
{{todo|num à droite}}
 
<pre>
1.# Définition d'un client réseau gérant en parallèle l'émission
2.# et la réception des messages (utilisation de 2 THREADS).
Ligne 219 ⟶ 243 :
50.th_R = ThreadReception(connexion)
51.th_E.start()
52.th_R.start()
</pre>
 
;Commentaires
 
Remarque générale : Dans cet exemple, nous avons décidé de créer deux objets threads indépendants du thread principal, afin de bien mettre en évidence les mécanismes. Notre programme utilise donc trois threads en tout, alors que le lecteur attentif aura remarqué que deux pourraient suffire. En effet : le thread principal ne sert en définitive qu'à lancer les deux autres ! Il n'y a cependant aucun intérêt à limiter le nombre de threads. Au contraire : à partir du moment où l'on décide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unités bien distinctes.
 
Ligne 7 : Le module threading contient la définition de toute une série de classes intéressantes pour gérer les threads. Nous n'utiliserons ici que la seule classe Thread(), mais une autre sera exploitée plus loin (la classe Lock()), lorsque nous devrons nous préoccuper de problèmes de synchronisation entre différents threads concurrents.
Ligne 7 : Le module ''threading'' contient la définition de toute une série de classes intéressantes pour gérer les threads. Nous n'utiliserons ici que la seule classe <code>Thread()</code>, mais une autre sera exploitée plus loin (la classe <code>Lock()</code>), lorsque nous devrons nous préoccuper de problèmes de synchronisation entre différents threads concurrents.
Lignes 9 à 25 : Les classes dérivées de la classe Thread() contiendront essentiellement une méthode run(). C'est dans celle-ci que l'on placera la portion de programme spécifiquement confiée au thread. Il s'agira souvent d'une boucle répétitive, comme ici. Vous pouvez parfaitement considérer le contenu de cette méthode comme un script indépendant, qui s'exécute en parallèle avec les autres composants de votre application. Lorsque ce code a été complètement exécuté, le thread se referme.
 
Lignes 9 à 25 : Les classes dérivées de la classe <code>Thread()</code> contiendront essentiellement une méthode <code>run()</code>. C'est dans celle-ci que l'on placera la portion de programme spécifiquement confiée au thread. Il s'agira souvent d'une boucle répétitive, comme ici. Vous pouvez parfaitement considérer le contenu de cette méthode comme un script indépendant, qui s'exécute en parallèle avec les autres composants de votre application. Lorsque ce code a été complètement exécuté, le thread se referme.
 
Lignes 16 à 20 : Cette boucle gère la réception des messages. À chaque itération, le flux d'instructions s'interrompt à la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas figé pour autant : les autres threads continuent leur travail indépendamment.
 
Ligne 19 : La sortie de boucle est provoquée par la réception d'un message 'fin' (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupée par le partenaire). Quelques instructions de « nettoyage » sont alors exécutées, et puis le thread se termine.
Ligne 19 : La sortie de boucle est provoquée par la réception d'un message <code>'fin'</code> (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupée par le partenaire). Quelques instructions de « nettoyage » sont alors exécutées, et puis le thread se termine.
Ligne 23 : Lorsque la réception des messages est terminée, nous souhaitons que le reste du programme se termine lui aussi. Il nous faut donc forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gérer l'émission des messages. Cette fermeture forcée peut être obtenue à l'aide de la méthode _Thread__stop()5.
 
Ligne 23 : Lorsque la réception des messages est terminée, nous souhaitons que le reste du programme se termine lui aussi. Il nous faut donc forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gérer l'émission des messages. Cette fermeture forcée peut être obtenue à l'aide de la méthode <code>_Thread__stop()</code><ref>Que les puristes veuillent bien me pardonner : j'admets volontiers que cette astuce pour forcer l'arrêt d'un thread n'est pas vraiment recommandable. Je me suis autorisé ce raccourci afin de ne pas trop alourdir ce texte, qui se veut seulement une initiation. Le lecteur exigeant pourra approfondir cette question en consultant l'un ou l'autre des ouvrages de référence mentionnés dans la bibliographie située en fin d'ouvrage.</ref>.
 
Lignes 27 à 36 : Cette classe définit donc un autre objet thread, qui contient cette fois une boucle de répétition perpétuelle. Il ne se pourra donc se terminer que contraint et forcé par méthode décrite au paragraphe précédent. À chaque itération de cette boucle, le flux d'instructions s'interrompt à la ligne 35 dans l'attente d'une entrée clavier, mais cela n'empêche en aucune manière les autres threads de faire leur travail.
 
Lignes 38 à 45 : Ces lignes sont reprises à l'identique des scripts précédents.
 
Lignes 47 à 52 : Instanciation et démarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommandé de provoquer ce démarrage en invoquant la méthode intégrée start(), plutôt qu'en faisant appel directement à la méthode run() que vous aurez définie vous-même. Sachez également que vous ne pouvez invoquer start() qu'une seule fois (une fois arrêté, un objet thread ne peut pas être redémarré).
Lignes 47 à 52 : Instanciation et démarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommandé de provoquer ce démarrage en invoquant la méthode intégrée <code>start()</code>, plutôt qu'en faisant appel directement à la méthode <code>run()</code> que vous aurez définie vous-même. Sachez également que vous ne pouvez invoquer <code>start()</code> qu'une seule fois (une fois arrêté, un objet thread ne peut pas être redémarré).
 
== Serveur gérant les connexions de plusieurs clients en parallèle ==
 
Le script ci-après crée un serveur capable de prendre en charge les connexions d'un certain nombre de clients du même type que ce que nous avons décrit dans les pages précédentes.
 
Ce serveur n'est pas utilisé lui-même pour communiquer : ce sont les clients qui communiquent les uns avec les autres, par l'intermédiaire du serveur. Celui-ci joue donc le rôle d'un relais : il accepte les connexions des clients, puis attend l'arrivée de leurs messages. Lorsqu'un message arrive en provenance d'un client particulier, le serveur le ré-expédie à tous les autres, en lui ajoutant au passage une chaîne d'identification spécifique du client émetteur, afin que chacun puisse voir tous les messages, et savoir de qui ils proviennent.
 
{{todo|idem}}
 
<pre>
1.# Définition d'un serveur réseau gérant un système de CHAT simplifié.
2.# Utilise les threads pour gérer les connexions clientes en parallèle.
Ligne 296 ⟶ 333 :
57. # Dialogue avec le client :
58. connexion.send("Vous êtes connecté. Envoyez vos messages.")
</pre>
 
;Commentaires
 
{{todo|puces qui semblent avoir été oubliées dans le livre}}
 
Lignes 35 à 43 : L'initialisation de ce serveur est identique à celle du serveur rudimentaire décrit au début du présent chapitre.
 
Ligne 46 : Les références des différentes connexions doivent être mémorisées. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La première est que nous devrons pouvoir ajouter ou enlever ces références dans n'importe quel ordre, puisque les clients se connecteront et se déconnecteront à leur guise. La seconde est que nous pouvons disposer aisément d'un identifiant unique pour chaque connexion, lequel pourra servir de clé d'accès dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe Thread().
Ligne 46 : Les références des différentes connexions doivent être mémorisées. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La première est que nous devrons pouvoir ajouter ou enlever ces références dans n'importe quel ordre, puisque les clients se connecteront et se déconnecteront à leur guise. La seconde est que nous pouvons disposer aisément d'un identifiant unique pour chaque connexion, lequel pourra servir de clé d'accès dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe <code>Thread()</code>.
Lignes 47 à 51 : Le programme commence ici une boucle de répétition perpétuelle, qui va constamment attendre l'arrivée de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClient() est créé, lequel pourra s'occuper d'elle indépendamment de toutes les autres.
 
Lignes 52 à 54 : Obtention d'un identifiant unique à l'aide de la méthode getName(). Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique à chaque nouveau thread : ce nom convient bien comme identifiant (ou clé) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaîne de caractères, de la forme : « Thread-N » (N étant le numéro d'ordre du thread).
Lignes 47 à 51 : Le programme commence ici une boucle de répétition perpétuelle, qui va constamment attendre l'arrivée de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet <code>ThreadClient()</code> est créé, lequel pourra s'occuper d'elle indépendamment de toutes les autres.
Lignes 15 à 17 : Gardez bien à l'esprit qu'il se créera autant d'objets ThreadClient() que de connexions, et que tous ces objets fonctionneront en parallèle. La méthode getName() peut alors être utilisée au sein de l'un quelconque de ces objets pour retrouver son identité particulière. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26).
 
Lignes 52 à 54 : Obtention d'un identifiant unique à l'aide de la méthode <code>getName()</code>. Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique à chaque nouveau thread : ce nom convient bien comme identifiant (ou clé) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaîne de caractères, de la forme : « Thread-N » (N étant le numéro d'ordre du thread).
 
Lignes 15 à 17 : Gardez bien à l'esprit qu'il se créera autant d'objets <code>ThreadClient()</code> que de connexions, et que tous ces objets fonctionneront en parallèle. La méthode <code>getName()</code> peut alors être utilisée au sein de l'un quelconque de ces objets pour retrouver son identité particulière. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26).
 
Lignes 18 à 23 : L'utilité du thread est de réceptionner tous les messages provenant d'un client particulier. Il faut donc pour cela une boucle de répétition perpétuelle, qui ne s'interrompra qu'à la réception du message spécifique : « fin », ou encore à la réception d'un message vide (cas où la connexion est coupée par le partenaire).
 
Lignes 24 à 27 : Chaque message reçu d'un client doit être ré-expédié à tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des clés du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-mêmes. Un simple test (à la ligne 26) nous évite de ré-expédier le message au client dont il provient.
 
Ligne 31 : Lorsque nous fermons un socket de connexion, il est préférable de supprimer sa référence dans le dictionnaire, puisque cette référence ne peut plus servir. Et nous pouvons faire cela sans précaution particulière, car les éléments d'un dictionnaire ne sont pas ordonnés (nous pouvons en ajouter ou en enlever dans n'importe quel ordre).
 
Ligne 311 ⟶ 358 :
 
Au chapitre 15, nous avons commenté le développement d'un petit jeu de combat dans lequel des joueurs s'affrontaient à l'aide de bombardes. L'intérêt de ce jeu reste toutefois fort limité, tant qu'il se pratique sur un seul et même ordinateur. Nous allons donc le perfectionner, en y intégrant les techniques que nous venons d'apprendre. Comme le système de « chat » décrit dans les pages précédentes, l'application complète se composera désormais de deux programmes distincts : un logiciel serveur qui ne sera mis en fonctionnement que sur une seule machine, et un logiciel client qui pourra être lancé sur toute une série d'autres. Du fait du caractère portable de Python, il vous sera même possible d'organiser des combats de bombardes entre ordinateurs gérés par des systèmes d'exploitation différents (MacOS <> Linux <> Windows !).
 
{{ajouter images}}
 
=== Programme serveur : vue d'ensemble ===
 
Les programmes serveur et client exploitent la même base logicielle, elle-même largement récupérée de ce qui avait déjà été mis au point tout au long du chapitre 15. Nous admettrons donc pour la suite de cet exposé que les deux versions précédentes du jeu ont été sauvegardées dans les fichiers-modules <code>canon03.py</code> et <code>canon04.py</code>, installés dans le répertoire courant. Nous pouvons en effet réutiliser une bonne partie du code qu'ils contiennent, en nous servant judicieusement de l'importation et de l'héritage de classes.
 
Du module canon04, nous allons réutiliser la classe Canon() telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce même module, nous importerons également la classe AppBombardes(), dont nous ferons dériver la classe maîtresse de notre application serveur : AppServeur(). Vous constaterez plus loin que celle-ci produira elle-même la sous-classe AppClient(), toujours par héritage.
Du module ''canon04'', nous allons réutiliser la classe <code>Canon()</code> telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce même module, nous importerons également la classe <code>AppBombardes()</code>, dont nous ferons dériver la classe maîtresse de notre application serveur : <code>AppServeur()</code>. Vous constaterez plus loin que celle-ci produira elle-même la sous-classe <code>AppClient()</code>, toujours par héritage.
Du module canon03, nous récupérerons la classe Pupitre() dont nous tirerons une version plus adaptée au « contrôle à distance ».
 
Enfin, deux nouvelles classes viendront s'ajouter aux précédentes, chacune spécialisée dans la création d'un objet thread : la classe ThreadClients(), dont une instance surveillera en permanence le socket destiné à réceptionner les demandes de connexion de nouveaux clients, et la classe ThreadConnexion(), qui servira à créer autant d'objets sockets que nécessaire pour assurer le dialogue avec chacun des clients déjà connectés.
Du module ''canon03'', nous récupérerons la classe <code>Pupitre()</code> dont nous tirerons une version plus adaptée au « contrôle à distance ».
Ces nouvelles classes seront inspirées de celles que nous avions développées pour notre serveur de « chat » dans les pages précédentes. La principale différence par rapport à celui-ci est que nous devrons activer un thread spécifique pour le code qui gère l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps.
 
A partir de là, notre plus gros travail consistera à développer un protocole de communication pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de définir la teneur des messages que vont s'échanger les machines connectées. Rassurez-vous : la mise au point de ce « langage » peut être progressive. On commence par établir un dialogue de base, puis on y ajoute petit à petit un « vocabulaire » plus étendu.
Enfin, deux nouvelles classes viendront s'ajouter aux précédentes, chacune spécialisée dans la création d'un objet thread : la classe <code>ThreadClients()</code>, dont une instance surveillera en permanence le socket destiné à réceptionner les demandes de connexion de nouveaux clients, et la classe <code>ThreadConnexion()</code>, qui servira à créer autant d'objets sockets que nécessaire pour assurer le dialogue avec chacun des clients déjà connectés.
L'essentiel de ce travail peut être accompli en s 'aidant du logiciel client développé précédemment pour le système de « chat ». On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de développement, et on corrige celui-ci jusqu'à ce qu'il « obéisse » : en clair, les procédures que l'on met en place progressivement sur le serveur sont testées au fur et à mesure, en réponse aux messages correspondants émis « à la main » à partir du client.
 
Ces nouvelles classes seront inspirées de celles que nous avions développées pour notre serveur de ''chat'' dans les pages précédentes. La principale différence par rapport à celui-ci est que nous devrons activer un thread spécifique pour le code qui gère l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps.
 
À partir de là, notre plus gros travail consistera à ''développer un protocole de communication'' pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de définir la teneur des messages que vont s'échanger les machines connectées. Rassurez-vous : la mise au point de ce « langage » peut être progressive. On commence par établir un dialogue de base, puis on y ajoute petit à petit un « vocabulaire » plus étendu.
 
L'essentiel de ce travail peut être accompli en s 'aidant du logiciel client développé précédemment pour le système de ''chat''. On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de développement, et on corrige celui-ci jusqu'à ce qu'il « obéisse » : en clair, les procédures que l'on met en place progressivement sur le serveur sont testées au fur et à mesure, en réponse aux messages correspondants émis « à la main » à partir du client.
 
=== Protocole de communication ===
 
{{todo|pause ici}}
 
Il va de soi que le protocole décrit ci-après est tout à fait arbitraire. Il serait parfaitement possible de choisir d'autres conventions complètement différentes. Vous pouvez bien évidemment critiquer les choix effectués, et vous souhaiterez peut-être même les remplacer par d'autres, plus efficients ou plus simples.