11.6. SOCKETS TCP

 

 

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:

state msg Description
TCPServer.LISTENING
port
Une connexion vient d’être terminée ou le serveur vient d’être démarré. Le serveur est en écoute d’une nouvelle connexion.
Server.PORT_IN_USE
port
Le serveur ne peut pas passer en mode écoute car le port est déjà occupé par un autre processus.
TCPServer.CONNECTED
Adresse IP du client

Un client a initié une connexion qui été acceptée

TCPServer.MESSAGE
Message reçu
Le serveur a reçu un message sur le socket
TCPSever.TERMINATED
(Vide)
Le serveur a terminé son exécution et n’est plus en écoute

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 :

state msg Description
TCPClient.CONNECTING
Adresse IP du serveur
Démarre une tentative de connexion
TCPClient.CONNECTION_FAILED
Adresse IP du serveur
La tentative de connexion a échoué
TCPClient.CONNECTED
Adresse IP du serveur

La connexion est établie

TCPClient.MESSAGE
Message reçu
Le client a reçu un message
TCPClient.DISCONNECTED
(Vide)
La connexion a été interrompue par le client ou le serveur

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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.")
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

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
à l’opposant de modifier son programme pour faire pratiquement n’importe quoi sur l’autre ordinateur.
] Pour savoir s’il y a eu un impact, il faut tester la couleur de fond avec getPixelColorStr().

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
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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")
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)



 

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!")
 
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

  client serveur
 

 

 

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)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

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()    
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

  Server: Client:

 

 

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

 

1a.

Développer un système client-serveur dans lequel le serveur dispose de bonnes connaissances mathématiques. Le client doit pouvoir saisir dans une boîte de dialogue une expression impliquant des fonctions du module math, par exemple sqrt(2), qui sera évaluée par le serveur. Le résultat de l’évaluation sera affiché dans la console du client. Pour réaliser l’évaluation du côté serveur, utilisez la fonction exec() intégrée à Python.

   
1b.

Améliorer le programme précédent pour que le serveur retourne le message « illegal » s’il n’est pas capable d’évaluer l’expression demandée. Indication : utiliser une structure try … except pour attraper l’exception générée lorsque exec() n’est pas capable d’exécuter l’instruction demandée.

   
2.

Développer un système client-serveur dans lequel le client peut envoyer des ordres que le serveur exécute. Le serveur démarre une fenêtre tortue et attend les ordres du client. Du côté client, l’utilisateur peut saisir une commande tortue qui sera envoyée au serveur pour exécution. Le serveur retourne le message « OK » en cas de succès et « fail » dans le cas contraire. Ce message sera affiché dans la console du client qui enverra l’instruction suivante uniquement sur réception de ce message de confirmation. Remarque : le client peut également envoyer plusieurs instructions séparées par un point-virgule en une seule fois.

   
3.

Ajouter des effets sonores au touché-coulé à deux dimensions. Suggestion : émettre un son lorsque la bombe et larguée et lorsque la cible est manquée ou touchée. Utiliser les compétences vues dans le chaptre 4: son. Vous pouvez utiliser des sons prédéfinis, mais vos propres sons seront plus amusants.

   
4*.

Améliorer le touché-coulé à une dimension de sorte que les bateaux ne soient plus représentés par des carrés colorés mais par des images qui disparaissent lorsqu’elles sont touchées.

   

 

 

 

MATÉRIEL SUPPLÉMENTAIRE

 

MESURES À DISTANCE AVEC LE RASPBERRY PI

 

Les communications client-serveur par TCP/IP jouent également un rôle très important dans la technologie de mesure et de contrôle. Il arrive bien souvent qu’un instruments de mesure ou qu’un robot se trouve éloigné de son centre de contrôle et que les données doivent donc être transmises par TCP/IP. Dans l’exercice 2, nous avons déjà réalisé un contrôle à distance. Dans l’exemple qui suit, nous utiliserons un Raspberry Pi agissant comme une interface entre le capteur de mesures et l’Internet en envoyant les données mesurées vers une station de collecte. Pour simplifier le système, nous n’utiliserons pas de réel capteur mais uniquement un bouton-poussoir dont l’état enfoncé ou relâché sera reporté.

Le programme de détection de mesures tournant sur le Raspberry Pi utilise le module tcpcom qu’il faut copier dans le même dossier que le programme sur le RPi. Pour tester le programme, on pourra utiliser un clavier et un écran directement rattachés au RPi ou piloter un PC distant à l’aide de SSH, WinSCP ou VNC. Pour plus d’informations, consulter la littérature ou faire une recherche sur Internet.

Le programme serveur est vraiment très simple : dans la boucle de mesure, on appelle périodiquement GPIO.input() pour interroger l’état du bouton-poussoir. La valeur 0 est retournée lorsque le bouton est enfoncé. L’information de l’état du bouton est ensuite transférée au client. Si le bouton est enfoncé durant trois cycles de mesure, le programme serveur est stoppé.

import time
import RPi.GPIO as GPIO
from tcpcom import TCPServer

def onStateChanged(state, msg):
    print("State:", state, "Msg:", msg)
P_BUTTON1 = 16 # Switch pin number
dt = 1  # 1 s period
port = 5000 # IP port

GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
GPIO.setup(P_BUTTON1, GPIO.IN, GPIO.PUD_UP)
server = TCPServer(port, stateChanged = onStateChanged)
n = 0
while True:
    if server.isConnected():
        rc = GPIO.input(P_BUTTON1)
        if rc == 0:
            server.sendMessage("pressed")
            n += 1
            if n == 3:
                break
        else:
            server.sendMessage("released")
            n = 0
        time.sleep(dt)
server.terminate()
print("Server terminated")
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Le client se contente d’afficher les données reçues sur la sortie standard.

from tcpcom import TCPClient

def onStateChanged(state, msg):
    print("State: " + state + ". Message: " + msg)
host = "192.168.0.5"
#host = inputString("Host Address?")           
port = 5000 # IP port
client = TCPClient(host, port, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    msgDlg("Connected. OK to terminate")
    client.disconnect()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)