INTRODUCTION |
L’échange de données entre ordinateurs joue un rôle extrêmement important dans notre monde connecté. On parle donc souvent des technologies de l’information et de la communication qui devraient être maitrisées par tous. Dans ce chapitre, nous allons apprendre à gérer les échanges de données entre deux systèmes informatiques en utilisant le protocole TCP/IP qui constitue la base de toutes les connexions Internet comme le Web et les services des streaming (données dans le nuage, transmission de la voix, de musique, de vidéo). |
TCPCOM: UNE BIBLIOTHÈQUE SOCKET ORIENTÉE ÉVÉNEMENTS |
La programmation socket est basée sur le modèle client-serveur que nous avons déjà traité dans la section 6.2. Le rôle du serveur et du client ne sont pas complètement symétriques. En effet, le serveur doit être démarré en premier avant qu’un client ne puisse s’y connecter. Pour identifier le serveur sur l’Internet, on utilise son adresse IP. De plus, le serveur dispose de 65536 canaux de communication (ports IP) qui sont désignés par un nombre compris entre 0 et 64535. Lorsque le serveur démarre, il crée un socket serveur utilisant un port bien défini et passant ensuite en attente. On pourrait comparer un socket réseau à une prise murale à laquelle on peut « brancher » une connexion. On dit que le serveur écoute sur le port : il est donc en état d’attente de la connexion d’un client. Pour se connecter au serveur, le client crée un socket client semblable à une fiche électrique et tente d’établir un lien de communication avec le serveur en utilisant l’adresse et le port IP appropriés. La bibliothèque tcpcom simplifie la programmation socket de manière drastique puisqu’elle décrit l’état courant du serveur et du client à l’aide de variables d’état. Le changement d’état est causé par un événement. Ce modèle de programmation correspond parfaitement à l’idée que l’on se fait naturellement de la communication entre deux partenaires comme une suite d’événements chronologiques. Comme d’ordinaire dans le modèle événementiel, une fonction de rappel (callback), en l’occurrence appelée stateChanged(state, msg), est invoquée par le système lorsqu’un événement survient. Le module Python est intégré dans TigerJython mais peut également être téléchargé depuis ce lien pour être étudié ou utilisé en dehors de TigerJython. Le serveur est démarré par la création d’un objet TCPServer qui spécifie le port IP sur lequel écouter ainsi que la fonction de rappel onStateChanged() à utiliser pour gérer les événements. Il passe ensuite en mode écoute. from tcpcom import TCPServer server = TCPServer(port, stateChanged = onStateChanged) La fonction de rappel onStateChanged (state, msg) prend deux chaines de caractères en paramètre : state et msg qui décrivent le changement d’état du serveur:
Le client démarre avec la création d’un objet TCPClient spécifiant l’adresse et le port IP du serveur ainsi que la fonction de rappel onStateChanged (). En invoquant connect(), le client démarre un tentative de connexion. from tcpcom import TCPClient client = TCPClient(host, port, stateChanged = onStateChanged) client.connect() Là encore, la fonction de rappel onStateChanged (state, msg) prend deux chaines de caractères state et msg en paramètre qui décrivent le changement d’état du client :
L’appel à la fonction connect() est bloquant, ce qui signifie qu’elle va retourner True une fois la connexion établie ou False après un temps d’attente d’environ 10 secondes si la connexion échoue. L’information de succès ou d’échec de la connexion peut également être détectée à l’aide de la fonction de rappel. On peut tester le programme client / serveur suivant sur la même machine en démarrant deux fenêtre TigerJython différentes et en exécutant le programme dans chacune d’elles. Dans ce cas, on choisit localhost comme nom d’hôte à savoir l’adresse IP 127.0.0.1 correspondant à l’interface réseau locale. Il est bien entendu plus réaliste d’utiliser deux machines différentes. Elles doivent alors être connectées par un réseau filaire ou Wi-Fi et la communication TCP/IP doit être autorisée sur le port sélectionné (attention au pare-feu). Un échec de la connexion par l’intermédiaire d’une borne Wi-Fi est le plus souvent dû à des restrictions du pare-feu qui y est intégré. En cas de problème, il faut se connecter à l’interface d’administration de la borne si vous en avez la possibilité ou activer le partage de la connexion de votre smartphone, ce qui va transformer ce dernier en un point d’accès mobile qui agira comme une borne Wi-Fi sans protection particulière. Il n’est pas nécessaire pour cela d’être connecté à Internet par le réseau mobile 3G ou similaire. Votre première tâche de programmation réseau est de mettre en place un service réseau qui indique l’heure. Lorsqu’un client se connecte, ce service retourne le temps et la date actuels au client. Il existe des tas de tels serveurs de temps sur Internet et vous pouvez être fiers d’être déjà en mesure de coder une application serveur professionnelle. Pour éteindre le serveur de temps, on utilise une astuce bien connue qui consiste à « figer » le programme serveur dans une boîte de dialogue modale ouverte avec la fonction bloquante msgDlg(). Lorsque la fonction retourne lors d’un clic sur le bouton OK, le serveur est arrêté par un appel à terminate(). from tcpcom import TCPServer import datetime def onStateChanged(state, msg): print(state, msg) if state == TCPServer.CONNECTED: server.sendMessage(str(datetime.datetime.now())) server.disconnect() port = 5000 server = TCPServer(port, stateChanged = onStateChanged) msgDlg("Time Server running. OK to stop") server.terminate() Le client commence par demander l’adresse IP sur laquelle se connecter et effectue une tentative de connexion par un appel à connect(). L’information de temps reçue du serveur est écrite dans une boîte de dialogue. from tcpcom import TCPClient def onStateChanged(state, msg): print(state, msg) if state == TCPClient.MESSAGE: msgDlg("Server reports local date/time: " + msg) if state == TCPClient.CONNECTION_FAILED: msgDlg("Server " + host + " not available") host = inputString("Time Server IP Address?") port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMENTO |
Le serveur et le client utilisent le modèle de la programmation événementielle avec la fonction de rappel onStateChanged(state, msg). Les deux paramètres pris par onStateChanged contiennent des informations importantes concernant l’événement survenu. Il faut s’assurer de terminer l’exécution du serveur avec terminate() pour libérer le port IP utilisé. |
SERVEUR ECHO |
Vos prochains pas en programmation socket touchent à un scénario célèbre puisqu’il constitue l’archétype même de la communication client-serveur. Le serveur ne fait rien d’autre que de renvoyer au client les données qu’il a lui-même envoyées, sans les modifier. On appelle cela un serveur « écho ». Ce serveur peut ensuite facilement être modifié pour analyser les messages reçus du client et lui retourner des réponses en conséquence. C’est d’ailleurs exactement ce qui se passe avec tous les serveurs Web qui retournent une réponse à une requête HTTP initiée par le navigateur client. On commence par coder le serveur pour le démarrer immédiatement. Le code est très similaire à celui du serveur de temps. Pour bien comprendre la communication et dans un but de débogage, le programme affiche dans la console les paramètres state et msg reçus par la fonction de rappel. from tcpcom import TCPServer def onStateChanged(state, msg): print(state, msg) if state == TCPServer.MESSAGE: server.sendMessage(msg) port = 5000 server = TCPServer(port, stateChanged = onStateChanged) msgDlg("Echo Server running. OK to stop") server.terminate() Le client demande un peu plus d’efforts et utilise la classe EntryDialog pour afficher une boîte de dialogue non modale mettant en évidence les informations de changement d’état. La classe EntryDialog est également très appropriée dans d’autres situations puisqu’elle permet facilement d’ajouter des champs textuels éditables ou non éditables ainsi que d’autres composants graphiques tels que les boutons ou les curseurs. from tcpcom import TCPClient from entrydialog import * import time def onStateChanged(state, msg): print(state, msg) if state == TCPClient.MESSAGE: status.setValue("Reply: " + msg) if state == TCPClient.DISCONNECTED: status.setValue("Server died") def showStatusDialog(): global dlg, btn, status status = StringEntry("Status: ") status.setEditable(False) pane1 = EntryPane(status) btn = ButtonEntry("Finish") pane2 = EntryPane(btn) dlg = EntryDialog(pane1, pane2) dlg.setTitle("Client Information") dlg.show() host = "localhost" port = 5000 showStatusDialog() client = TCPClient(host, port, stateChanged = onStateChanged) status.setValue("Trying to connect to " + host + ":" + str(port) + "...") time.sleep(2) rc = client.connect() if rc: time.sleep(2) n = 0 while not dlg.isDisposed(): if client.isConnected(): status.setValue("Sending: " + str(n)) time.sleep(0.5) client.sendMessage(str(n), 5) # block for max 5 s n += 1 if btn.isTouched(): dlg.dispose() time.sleep(0.5) client.disconnect() else: status.setValue("Connection failed.") |
MEMENTO |
Dans le programme client, on utilise la fonction sendMessage(msg, timeout) avec un paramètre additionnel timeout. L’appel est bloquant pour un délai maximal spécifié en secondes en attendant que le serveur retourne une réponse. La fonction sendMessage() retourne la réponse du serveur ou None si aucune réponse n’est reçue dans le délai fixé. Il est important de connaître la différence entre une boîte de dialogue modale et une boîte de dialogue non modale. Alors que la fenêtre modale bloque le programme jusqu’à ce qu’elle soit fermée, le programme poursuit son exécution sans problème avec une boîte de dialogue non modale, ce qui permet au programme d’afficher à tout moment des informations d’état ou de lire une saisie utilisateur. |
JEU À DEUX JOUEURS EN LIGNE AVEC LES GRAPHIQUES TORTUE |
Le « touché coulé » est un jeu populaire joué par deux joueurs dans lequel la mémorisation joue un rôle de premier plan. Le plateau de jeu de chacun des joueurs est un arrangement à une ou deux dimensions de cellules sur lesquelles sont disposés des vaisseaux qui occupent une ou plusieurs cases et qui possèdent une valeur différente selon le navire. À tour de rôle, chacun des joueurs désigne une case du jeu adverse sur laquelle une bombe va être larguée. Si la cellule visée contient une partie de navire, celle-ci va être touchée et supprimée du plateau de jeu (ou, dans certaines variantes, marquée comme touchée) et sa valeur sera créditée à l’attaquant. Si l’un des joueurs n’a plus de navire, la partie touche à sa fin et le joueur disposant du plus de points remporte la partie. Dans sa version la plus simple, les navires sont affichés comme des carrés colorés dans un tableau unidimensionnel. Tous les navires sont de rang égal et le gagnant est le premier jour à éliminer tous les vaisseaux ennemis. Du point de vue de la logique du jeu, le client et le serveur sont pratiquement identiques. Afficher le plateau de jeu ne nécessite rien d’autre que des primitives graphiques des tortues graphiques que vous maitrisez en principe depuis bien longtemps. On sélectionne d’abord aléatoirement un numéro de cellule entre 1 et 10 puis quatre nombres aléatoires différents désignant les cellules comportant un navire. La fonction random.sample() permet de faire ceci de manière élégante. Pour larguer une bombe, on envoie directement l’instruction Python appropriée sous forme de chaine de caractères qui sera évaluée par le partenaire à l’aide de la fonction exec(). Une telle évaluation du code n’est possible que dans quelques rares langages de programmation dynamiques. [plus...
Il faut savoir que cette technique présente de gros problèmes de sécurité et pourrait permettre Dans le jeu, les deux joueurs sont sur un pied d’égalité mais leur programme diffère légèrement suivant qu’il joue le rôle du serveur ou du client. De plus, le serveur doit démarrer en premier. from gturtle import * from tcpcom import TCPServer from random import sample def initGame(): clear("white") for x in range(-250, 250, 50): setPos(x, 0) setFillColor("gray") startPath() repeat 4: forward(50) right(90) fillPath() def createShips(): setFillColor("red") li = sample(range(1, 10), 4) # 4 unique random numbers 1..10 for i in li: fill(-275 + i * 50, 25) def onMouseHit(x, y): global isMyTurn setPos(x, y) if getPixelColorStr() == "white" or isOver or not isMyTurn: return server.sendMessage("setPos(" + str(x) + "," + str(y) + ")") isMyTurn = False def onCloseClicked(): server.terminate() dispose() def onStateChanged(state, msg): global isMyTurn, myHits, partnerHits if state == TCPServer.LISTENING: setStatusText("Waiting for game partner...") initGame() if state == TCPServer.CONNECTED: setStatusText("Partner entered my game room") createShips() if state == TCPServer.MESSAGE: if msg == "hit": myHits += 1 setStatusText("Hit! Partner's remaining fleet size " + str(4 - myHits)) if myHits == 4: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's remaining fleet size " + str(4 - myHits)) else: exec(msg) if getPixelColorStr() != "gray": server.sendMessage("hit") setFillColor("gray") fill() partnerHits += 1 if partnerHits == 4: setStatusText("Game over, Play partner won!") isOver = True return else: server.sendMessage("miss") setStatusText("Make your move") isMyTurn = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) addStatusBar(30) hideTurtle() port = 5000 server = TCPServer(port, stateChanged = onStateChanged) isOver = False isMyTurn = False myHits = 0 partnerHits = 0 The client program is almost the same: from gturtle import * from random import sample from tcpcom import TCPClient def initGame(): for x in range(-250, 250, 50): setPos(x, 0) setFillColor("gray") startPath() repeat 4: forward(50) right(90) fillPath() def createShips(): setFillColor("green") li = sample(range(1, 10), 4) # 4 unique random numbers 1..10 for i in li: fill(-275 + i * 50, 25) def onMouseHit(x, y): global isMyTurn setPos(x, y) if getPixelColorStr() == "white" or isOver or not isMyTurn: return client.sendMessage("setPos(" + str(x) + "," + str(y) + ")") isMyTurn = False def onCloseClicked(): client.disconnect() dispose() def onStateChanged(state, msg): global isMyTurn, myHits, partnerHits if state == TCPClient.DISCONNECTED: setStatusText("Partner disappeared") initGame() elif state == TCPClient.MESSAGE: if msg == "hit": myHits += 1 setStatusText("Hit! Partner's remaining fleet size " + str(4 - myHits)) if myHits == 4: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's remaining fleet size " + str(4 - myHits)) else: exec(msg) if getPixelColorStr() != "gray": client.sendMessage("hit") setFillColor("gray") fill() partnerHits += 1 if partnerHits == 4: setStatusText("Game over, Play partner won") isOver = True return else: client.sendMessage("miss") setStatusText("Make your move") isMyTurn = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) addStatusBar(30) hideTurtle() initGame() host = "localhost" port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) setStatusText("Client connecting...") isOver = False myHits = 0 partnerHits = 0 if client.connect(): setStatusText("Connected. Make your first move!") createShips() isMyTurn = True else: setStatusText("Server game room closed")
|
MEMENTO |
Dans le cas des jeux à deux joueurs, les joueurs peuvent partager un plateau commun ou, au contraire, avoir chacun une vision du jeu différente. Dans le second scénario qui inclut la plupart des jeux de cartes, le jeu doit nécessairement se jouer sur deux machines différentes puisqu’il faut garder son jeu secret. Au lieu de toujours laisser le client commencer la partie, le premier joueur pourrait être choisi au hasard ou par une négociation. Les deux programmes se terminent par un clic sur le bouton « fermer » de la barre de titre. Il faut cependant encore faire explicitement le ménage pour terminer le serveur ou fermer le lien de communication. Pour ce faire, il faut enregistrer la fonction de rappel onCloseClicked() pour inhiber le comportement par défaut et implémenter un comportement personnalisé qui fait le ménage et ferme la fenêtre tortue avec un appel à dispose(). Le programmeur sans scrupule qui aura omis de faire le ménage correctement devra recourir au gestionnaire de tâches du système d’exploitation pour faire le ménage et pouvoir réutiliser le jeu sans encombre. |
JEU EN LIGNE À DEUX JOUEURS AVEC GAMEGRID |
Pour coder des jeux plus complexes, il est avantageux d'utiliser une bibliothèque de jeu plus perfectionnée qui simplifie considérablement le code. Nous avons déjà vu dans le chapitre 7 comment utiliser GameGrid, un moteur de jeux complet intégré à TigerJython. En combinant la bibliothèque GameGrid avec tcpcom, il est possible de créer des jeux en ligne multijoueurs sophistiqués dans lesquels les partenaires de jeux sont situés aux extrémités du monde. En guise d'illustration, nous allons étendre le touché-coulé en deux dimensions. La logique du jeu demeure inchangée mais on transfère cette fois-ci les coordonnées X et Y de la cellule sélectionnée. Du côté de la réception, le partenaire peut déterminer si l'un de ses bateaux a été touché et retourner une réponse “hit” ou “miss”. Dans le développement de jeux, il est important de consacrer un effort spécial pour faire en sorte que le jeu se comporte de manière raisonnable même si les deux joueurs ont un comportement quelque peu déraisonnable. Il faut par exemple interdire de larguer des bombes si ce n'est pas le tour du joueur. D'autre part, si l'un des joueurs quitte le jeu de manière inattendue, son partenaire devrait en être informé. Bien que ces précautions accroissent significativement le nombre de lignes de code, c’est là une marque distinctive d’une programmation scrupuleuse. Puisqu'une grande partie du code est identique côté serveur et côté client et comme la duplication de code est l'un des plus grands péchés en programmation, le code commun est exporté dans un module shiplib.py qui peut ensuite être importé par les deux programmes. Les différences de comportement sont prises en compte par des paramètres supplémentaires tels que node qui fait référence à un objet TCPServer ou TCPClient. Module séparé: # shiplib.py from gamegrid import * isOver = False isMyMove = False dropLoc = None myHits = 0 partnerHits = 0 nbShips = 2 class Ship(Actor): def __init__(self): Actor.__init__(self, "sprites/boat.gif") def handleMousePress(node, loc): global isMyMove, dropLoc dropLoc = loc if not isMyMove or isOver: return node.sendMessage("" + str(dropLoc.x) + str(dropLoc.y)) # send location setStatusText("Bomb fired. Wait for result...") isMyMove = False def handleMessage(node, state, msg): global isMyMove, myHits, partnerHits, first, isOver if msg == "hit": myHits += 1 setStatusText("Hit! Partner's fleet size " + str(nbShips - myHits) + ". Wait for partner's move!") addActor(Actor("sprites/checkgreen.gif"), dropLoc) if myHits == nbShips: setStatusText("Game over, You won!") isOver = True elif msg == "miss": setStatusText("Miss! Partner's fleet size " + str(nbShips - myHits) + ". Wait for partner's move!") addActor(Actor("sprites/checkred.gif"), dropLoc) else: x = int(msg[0]) y = int(msg[1]) loc = Location(x, y) bomb = Actor("sprites/explosion.gif") addActor(bomb, loc) delay(2000) bomb.removeSelf() refresh() actor = getOneActorAt(loc, Ship) if actor != None: actor.removeSelf() refresh() node.sendMessage("hit") partnerHits += 1 if partnerHits == nbShips: setStatusText("Game over! Partner won") isOver = True return else: node.sendMessage("miss") isMyMove = True setStatusText("You fire!") L'utilisation du module externe shiplib.py simplifie grandement le code du serveur et du client. Voici le code serveur : from gamegrid import * from tcpcom import TCPServer import shiplib def onMousePressed(e): loc = toLocationInGrid(e.getX(), e.getY()) shiplib.handleMousePress(server, loc) def onStateChanged(state, msg): global first if state == TCPServer.PORT_IN_USE: setStatusText("TCP port occupied. Restart IDE.") elif state == TCPServer.LISTENING: setStatusText("Waiting for a partner to play") if first: first = False else: removeAllActors() for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) elif state == TCPServer.CONNECTED: setStatusText("Client connected. Wait for partner's move!") elif state == TCPServer.MESSAGE: shiplib.handleMessage(server, state, msg) def onNotifyExit(): server.terminate() dispose() makeGameGrid(6, 6, 50, Color.red, False, mousePressed = onMousePressed, notifyExit = onNotifyExit) addStatusBar(30) for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) show() port = 5000 first = True server = TCPServer(port, stateChanged = onStateChanged) shiplib.node = server Et voici le code client : from gamegrid import * from tcpcom import TCPClient import shiplib def onMousePressed(e): loc = toLocationInGrid(e.getX(), e.getY()) shiplib.handleMousePress(client, loc) def onStateChanged(state, msg): if state == TCPClient.CONNECTED: setStatusText("Connection established. You fire!") shiplib.isMyMove = True elif state == TCPClient.CONNECTION_FAILED: setStatusText("Connection failed") elif state == TCPClient.DISCONNECTED: setStatusText("Server died") shiplib.isMyMove = False elif state == TCPClient.MESSAGE: shiplib.handleMessage(client, state, msg) def onNotifyExit(): client.disconnect() dispose() makeGameGrid(6, 6, 50, Color.red, False, mousePressed = onMousePressed, notifyExit = onNotifyExit) addStatusBar(30) for i in range(shiplib.nbShips): addActor(shiplib.Ship(), getRandomEmptyLocation()) show() host = "localhost" port = 5000 client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMENTO |
La condition de fin de jeu est une situation spéciale qui demande toujours au programmeur une pensée scrupuleuse. Puisqu'il s'agit d'une situation d'urgence, on utilise un fanion global isOver qui est mis à True lorsque le jeu est terminé. Il faut également se poser la question de savoir si une nouvelle partie doit pouvoir être démarrée sans qu’il faille redémarrer le programme serveur et le programme client. Dans l'implémentation présentée ci-dessous, le client doit être fermé et le serveur retourne en état d’attente de connexion pour démarrer une nouvelle partie. Les programmes pourraient être améliorés de plusieurs façons : de manière générale, la programmation est un domaine très attractif puisqu'il n'y a aucune limite à l'imagination et à l'ingéniosité des développeurs. De plus, après tout, se relaxer et jouer au jeu développé avec tant de peine est également très amusant. Jusqu'à présent, les deux partenaires de jeu doivent tous deux démarrer leur propre programme selon la règle suivante: “le serveur en premier et ensuite le client”. Pour contourner cette restriction, on peut user de l’astuce suivante: un programme démarre toujours en tant que client et essaie de créer une connexion au serveur. S'il échoue, il démarre en tant que serveur [plus... Un tel scénario constitue de la communication pair à pair (peer to peer): les deux programmes sont dans ce cas complètement identiques et sur un même pied d’égalité]. |
COMMUNICATION AVEC POIGNÉE DE MAINS |
Même dans la vie de tous les jours, la communication entre deux partenaires nécessite une certaine forme de synchronisation. En particulier, l'envoi de données ne peut se faire que si le destinataire est prêt à la réception et au traitement des données. Ne pas observer cette règle pourrait conduire à une perte de données ou même au blocage des programmes. Il faut également prendre en compte la différence de puissance de calcul entre les deux nœuds ainsi qu’un délai de transmission changeant. Une technique bien connue pour surmonter ces difficultés consiste, pour le destinataire, à retourner à l'émetteur une confirmation de réception, ce qui peut être comparé à une amicale poignée de mains. Le processus est relativement simple : des données sont transmises en bloc et le destinataire confirme qu'il les a bien reçues et qu'il est prêt pour la réception du prochain bloc. L'émetteur renverra alors le prochain bloc uniquement lorsqu'il aura reçu la confirmation de réception. Ce mécanisme de confirmation peut également conduire l'émetteur à renvoyer le même bloc de données une seconde fois [plus... Les différences de comportement temporel entre les deux systèmes peuvent également être compensées par des tampons (buffer) implémentés sous forme de files d’attentes FIFO].Pour illustrer ce principe de la poignée de mains, le programme turtle serveur dessine assez lentement des lignes dictées par les clics de souris du client. Les mouvements de la tortue du client sont bien plus rapides. Le client doit de ce fait attendre entre chaque clic que le serveur confirme l’achèvement de son opération de dessin et qu'il soit prêt à accepter les prochaines commandes. Serveur : from gturtle import * from tcpcom import TCPServer def onCloseClicked(): server.terminate() dispose() def onStateChanged(state, msg): if state == TCPServer.MESSAGE: li = msg.split(",") x = float(li[0]) y = float(li[1]) moveTo(x, y) dot(10) server.sendMessage("ok") makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) port = 5000 server = TCPServer(port, stateChanged = onStateChanged) Client: from gturtle import * from tcpcom import TCPClient def onMouseHit(x, y): global isReady if not isReady: return isReady = False client.sendMessage(str(x) + "," + str(y)) moveTo(x, y) dot(10) def onCloseClicked(): client.disconnect() dispose() def onStateChanged(state, msg): global isReady if state == TCPClient.MESSAGE: isReady = True makeTurtle(mouseHit = onMouseHit, closeClicked = onCloseClicked) speed(-1) host = "localhost" port = 5000 isReady = True client = TCPClient(host, port, stateChanged = onStateChanged) client.connect()
|
MEMENTO |
Afin de garantir que les actions de l’émetteur et du récepteur se déroulent dans le bon ordre, l’émetteur n’envoie le prochain paquet de données que lorsque le récepteur lui a confirmé qu’il est prêt à traiter les données suivantes.. |
EXERCICES |
|
|