TCP-Kommunikation


Das im TigerJython integrierte Modul tcpcom ermöglicht es, Anwendungen und Spiele, bei welchen zwei Benutzer über das Internet miteinander kommunizieren, zu programmieren. Damit können auch einfache Turtlegrafik-Programme erstellt werden, die über TCP/IP miteinander kommunizieren. Dabei spielt das eine Programm die Rolle eines Servers und das andere die eines Clients.

 
Die Verbindung zwischen den beiden Computern erfolgt über das Internet. In einem Klassenverband kann auch ein lokaler WLAN-Router oder ein mobiler WLAN-Hotspot, der auf einem Smartphone aktiviert ist, verwendet werden. Mehr zur Einrichtung der Kommunikation findet man hier.

Die Verbindung wird immer auf gleiche Art aufgebaut: Zuerst muss das Serverprogramm gestartet werden. Der Server wartet nun auf einem bestimmten IP-Port auf einen Client. Der Client erstellt nachher unter der Verwendung der IP-Adresse des Servers und der Portnummer, welche im Serverprogramm festgelegt ist, eine Verbindung zum Server. Zum Testen kann man das Server- und das Client-Programm auf dem gleichen Computer ausführen, indem man die TigerJython-IDE zweimal startet und die IP-Adresse localhost verwendet.

Das Modul tcpcom ist eventgesteuert aufgebaut. Der Status der Verbindung wird mit der Callbackfunktion onStateChanged(state, msg) überwacht. Diese wird bei der Erzeugung von TCPServers bzw. TCPClients registriert.

Der Parameter state des Servers kennt folgende Zustände:
  TCP.Server.LISTENING
TCP.Server.CONNECTED
TCP.Server.MESSAGE
TCP.Server.TERMINATE
Server wartet auf eine neue Verbindung
Die Verbindung zum Client wurde aufgebaut
Der Server hat eine Message msg empfangen
Der Server ist beendet

Der Client kennt folgende Zustände:
  TCPClient.CONNECTED
TCPClient.MESSAGE
TCPClient.DISSCONNECTED
Verbindung erfolgreich erstellt
Der Client hat eine Message msg empfangen
Verbindung unterbrochen

Mit der Funktion sendMessage() können Messages in Form eines Strings vom Server zum Client und umgekehrt gesendet werden.

Beispiel 1: Die Turtle auf dem Client-Computer wird durch Mausklicks auf dem Server gesteuert. Nach der Aufbau der Verbindung werden auf dem Server per Mausklick Punkte gewählt, zu welchen sich die Turtle bewegen soll. Die Zeichnungsbefehle werden vom Server zum Client gesendet und ebenfalls in seinem Turtlefenster ausgeführt.

  Server Client
 

Programmcode Server: 

# TcpEx1Server.py

from gturtle import *
from tcpcom import TCPServer
                                                                          
def onMousePressed(x, y):
    server.sendMessage("moveTo(" + str(x) + "," + str(y) + ")")
    moveTo(x, y)
    dot(10)   

def onCloseClicked():
    server.terminate()
    dispose()
    
def onStateChanged(state, msg):

    if state == TCPServer.CONNECTED:
        setStatusText("Partner ready. Click to draw a dot!") 
    
makeTurtle(mousePressed = onMousePressed, closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
port = 5000
server = TCPServer(port, stateChanged = onStateChanged)
setStatusText("Waiting for a partner...")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

Programmcode Client: 

# TcpEx1Client.py

from gturtle import *
from tcpcom import TCPClient

def onCloseClicked():
    client.disconnect()
    dispose()
    
def onStateChanged(state, msg):
    if state == TCPClient.MESSAGE:              
        exec(msg)
        dot(10)               
    elif state == TCPClient.DISCONNECTED:
        setStatusText("Server died")
 
makeTurtle(closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
host = "localhost"
port = 5000
client = TCPClient(host, port, stateChanged = onStateChanged)
setStatusText("Client connecting...")
if client.connect():
    setStatusText("Connected")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

Erklärungen zum Programmcode:

server.sendMessage("moveTo(" + str(x) + "," + str(y) + ")"): gesendet wird ein String, der nicht nur die Koordinaten des Mausklicks, sondern den ganzen Befehl enthält. Die Koordinaten x und y werden dabei in Strings konventiert
def onCloseClicked(): beim Schliessen des Turtlefensters wird der Server beendet. Beim Client wird beim Schliessen des Turtlefensters die Verbindung zum Server unterbrochen
exec(msg): führt den Befehl msg aus

 

Beispiel 2: Auf dem Server wird eine Freihandzeichnung mit gedrückten Maustaste erstellt. Die Mauskoordinaten werden fortlaufend zum Client gesendet, wodurch die gleiche Figur erscheint. In diesem Beispiel werden nicht die ganzen Befehle, sondern nur die aktuellen Mauskoordinaten und "p" für setPos() und "m" für moveTo() gesendet. Beim Client wird der empfangene Strings in Teile zerlegt und damit ein Turtlebefehl ausgeführt (siehe Erklärungen zum Programmcode).

  Server Client
 



# TcpEx2Server.py

from gturtle import *
from tcpcom import TCPServer
                                                                          
def onMousePressed(x, y):
    setPos(x, y)
    server.sendMessage("p," + str(x) + "," + str(y))

def onMouseDragged(x, y):
    moveTo(x, y)         
    server.sendMessage("m," + str(x) + "," + str(y))

def onCloseClicked():
    server.terminate()
    dispose()
    
def onStateChanged(state, msg):
    if state == TCPServer.CONNECTED:
        setStatusText("Partner ready, Drag the mouse!")        
  
makeTurtle(mousePressed = onMousePressed, mouseDragged = onMouseDragged, 
           closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
port = 5000
server = TCPServer(port, stateChanged = onStateChanged, )
setStatusText("Waiting for a client...")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

# TcpEx2Client.py

from gturtle import *
from tcpcom import TCPClient

def onCloseClicked():
    client.disconnect()
    dispose()
    
def onStateChanged(state, msg):
    if state == TCPClient.MESSAGE:
        li = msg.split(",")
        if li[0] == "p":
            setPos(float(li[1]), float(li[2]))
        if li[0] == "m":
            moveTo(float(li[1]), float(li[2]))
    if state == TCPClient.DISCONNECTED:
        setStatusText("Server died")          
   
makeTurtle(closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
host = "localhost"
port = 5000
client = TCPClient(host, port, stateChanged = onStateChanged)
setStatusText("Client connecting...")
if client.connect():
    setStatusText("Connected")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

Erklärungen zum Programmcode:

li = msg.split(","): separiert die durch Komma getrennten String-Elemente und speichert sie in einer Liste
if li[0] == "p": wenn das erste Listenelement "p" ist, wird der Befehl setPos() verwendet
setPos(float(li[1]), float(li[2])): das zweite Listenelement ist die x-Koordinate, das dritte ist die y-Koordinate. Diese müssen in Floats konvertiert werden

 

Beispiel 3: Server und Client wählen abwechslungsweise mit einem Mausklick eine Fläche aus. Diese wird auf beiden Computern gefüllt. Zuerst muss das Serverprogramm gestartet werden. Nach der Aufbau der Verbindung wählt Client die erste Fläche. Die Mausklicks der beiden Partner müssen nacheinander erfolgen, d.h. der Client muss mit der Wahl der nächsten Fläche warten, bis der Server seinen Mausklick ausgeführt hat. Dies ereicht man hier mit einem Flag isMyTurn, das auf False bzw. True gesetzt wird. Der nächste Mausklick kann nur im True-Zustand erfolgen.

  Server Client  
 

 

# TcpEx3Server.py

from gturtle import *
from tcpcom import TCPServer
                                                                          
def onMousePressed(x, y):
    global isMyTurn
    if not isMyTurn:
        return   
    setPos(x, y)
    setFillColor("green")
    fill(x, y)      
    server.sendMessage("fill(" + str(x) + "," + str(y) + ")")
    isMyTurn = False 
    setStatusText("Wait!") 

def onCloseClicked():
    server.terminate()
    dispose()
    
def onStateChanged(state, msg):
    global isMyTurn 
    if state == TCPServer.CONNECTED:
        setStatusText("Partner entered my game room")        
    if state == TCPServer.MESSAGE:        
        setFillColor("red")
        exec(msg)      
        setStatusText("Click to fill any part!")         
        isMyTurn = True
    
makeTurtle(mousePressed = onMousePressed, closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
for k in range(12):
   for i in range(6):
      forward(50)
      right(60)
   left(30) 
port = 5000
server = TCPServer(port, stateChanged = onStateChanged)
setStatusText("Waiting for a partner...")
isMyTurn = False
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

# TcpEx3Client.py

from gturtle import *
from tcpcom import TCPClient

def onMousePressed(x, y):
    global isMyTurn
    if not isMyTurn:
        return 
    setPos(x, y)
    setFillColor("red")
    fill(x, y)     
    client.sendMessage("fill(" + str(x) + "," + str(y) + ")")
    isMyTurn = False
    setStatusText("Wait!")  

def onCloseClicked():
    client.disconnect()
    dispose()
    
def onStateChanged(state, msg):
    global isMyTurn  
    if state == TCPClient.MESSAGE:               
        setFillColor("green")
        exec(msg)      
        setStatusText("Click to fill any part!")         
        isMyTurn = True
    elif state == TCPClient.DISCONNECTED:
        setStatusText("Server died")
        isMyTurn = False

makeTurtle(mousePressed = onMousePressed, closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
for k in range(12):
   for i in range(6):
      forward(50)
      right(60)
   left(30)
host = "localhost"
port = 5000
client = TCPClient(host, port, stateChanged = onStateChanged)
setStatusText("Client connecting...")
if client.connect():
    setStatusText("Click to fill any part!")
    isMyTurn = True
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

Erklärungen zum Programmcode:

client.sendMessage("fill(" + str(x) + "," + str(y) + ")"): der gesendete String enthält den Füll-Befehl, welcher die markierte Fläche füllt
isMyTurn = False: Zugberechtigung deaktiviert
setStatusText("Wait!"): in der Statuszeile wird jedem Benutzer jeweils mitgeteilt, dass er warten muss, bis der Partner eine Fläche gewählt hat


Beispiel 4: Ein einfaches Schiffchen-Versenken-Spiel.
In einem eindimensionalen Gitter, welches aus 10 Zellen besteht, werden zufällig 4 Schiffe angeordnet. Die Spieler wählen abwechslungsweise eine Zelle und versuchen die Position der Schiffe des Gegners zu erraten. Wenn sich in der gewählten Zelle ein Schiff befindet, wird es versenkt (mit Hintergrundfarbe übermalt). Ist die Zelle leer, hat man Pech gehabt. Das Spiel ist beendet, wenn einer der beiden Spieler keine Schiffe mehr hat.

Die Position der Schiffe wird mit Hilfe von Zufallszahlen bestimmt. Es ist vorteilhaft, dafür die Funktion random.sample() zu verwenden, die verschiedene Zufallszahlen erzeugt. Übertragen werden hier nicht die Koordinaten des Mausklicks, sondern die Nummer der getroffenden Zelle. Diese lässt sich aus den Mausklick-Koordinaten berechen. Um herauszufinden, ob getroffen wurde, prüft man mit getPixelColorStr() die Hintergrundfarbe im Zellenmittelpunkt. Die Anzahl der getroffenen Schiffe wird in den Variaben myHits und partnersHits gezäht.

 
Server

 
 
Client
 

 

# ShipServer.py

from gturtle import *
from tcpcom import TCPServer
import random 

def drawBoard():    
    setPenColor("gray")
    clear("aliceblue")
    for x in range(-250, 250, 50):   
        setPos(x, 0)
        setFillColor("blue")
        startPath()
        repeat 4:
            forward(50)
            right(90)
        fillPath()  
     
def createShips():    
    li = random.sample(range(1, 10), 4) # 4 unique random numbers 1..10
    for i in li:       
        setPos(-275 + i * 50, 25)
        drawImage("sprites/boat.gif")                                 
                                                                             
def onMousePressed(x, y):
    global isMyTurn
    setPos(x, y) 
    if getPixelColorStr() == "aliceblue" or isOver or not isMyTurn:
        return
    nb = int((x + 250) / 50) + 1 # cell number
    server.sendMessage(str(nb))
    isMyTurn = False

def onCloseClicked():
    server.terminate()
    dispose()
    
def onStateChanged(state, msg):
    global isMyTurn, myHits, partnerHits 
    if state == TCPServer.CONNECTED:
        setStatusText("Partner entered my game room")        
    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:        
            nb = int(msg) # cell number          
            x = -275 + nb * 50            
            setPos(x , 25)  
            if getPixelColorStr() != "blue":
                server.sendMessage("hit")                
                setPenColor("blue")
                dot(45) # erase ship
                partnerHits += 1                     
                if partnerHits == 4:
                    setStatusText("Game over! Partner won!")
                    isOver = True
                    return                                        
            else:
                server.sendMessage("miss")
            setStatusText("Make your move!")         
            isMyTurn = True
    
makeTurtle(mousePressed = onMousePressed, closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
drawBoard()
createShips()
port = 5000
server = TCPServer(port, stateChanged = onStateChanged)
setStatusText("Waiting for game partner...")
isOver = False
isMyTurn = False
myHits = 0
partnerHits = 0
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

# ShipClient.py

from gturtle import *
import random
from tcpcom import TCPClient

def drawBoard():
    clear("aliceblue")
    setPenColor("gray")    
    for x in range(-250, 250, 50):   
        setPos(x, 0)
        setFillColor("blue")
        startPath()
        repeat 4:
            forward(50)
            right(90)
        fillPath()    

def createShips():    
    li = random.sample(range(1, 10), 4) # 4 unique random numbers 1..10
    for i in li:       
        setPos(-275 + i * 50, 25) 
        drawImage("sprites/boat.gif")   

def onMousePressed(x, y):
    global isMyTurn
    setPos(x, y) 
    if getPixelColorStr() == "aliceblue" or isOver or not isMyTurn:
        return
    nb = int((x + 250) / 50) + 1 # cell number
    client.sendMessage(str(nb))
    isMyTurn = False  

def onCloseClicked():
    client.disconnect()
    dispose()
    
def onStateChanged(state, msg):
    global isMyTurn, myHits, partnerHits  
    if 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:        
            nb = int(msg) # cell number          
            x = -275 + nb * 50            
            setPos(x , 25)  
            if getPixelColorStr() != "blue":
                client.sendMessage("hit")                
                setPenColor("blue")
                dot(45) # erase ship
                partnerHits += 1                      
                if partnerHits == 4:
                    setStatusText("Game over! Partner won")
                    isOver = True 
                    return                            
            else:
                client.sendMessage("miss")
            setStatusText("Make your move!")          
            isMyTurn = True
    elif state == TCPClient.DISCONNECTED:
        setStatusText("Server died")
        drawBoard()
        isMyTurn = False

makeTurtle(mousePressed = onMousePressed, closeClicked = onCloseClicked)
addStatusBar(30)
hideTurtle()
drawBoard()
host = "localhost"
port = 5000
client = TCPClient(host, port, stateChanged = onStateChanged)
setStatusText("Client connecting...")
isOver = False
myHits = 0
partnerHits = 0
if client.connect():
    setStatusText("Make a move!")
    createShips()
    isMyTurn = True
else:
    setStatusText("Server game room closed")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)
   

Erklärungen zum Programmcode:

li = random.sample(range(1, 10), 4): erzeugt 4 verschiedene Zufallszahlen zwischen 1 und 10
setPos(-275 + i * 50, 25): setzt die Turtle in die Mitte der i-ten Zelle
drawImage("sprites/boat.gif") : stellt das Bild boat.gif an der Turtleposition dar


Weiterführende Beispiele zur TCP-Programmierung mit dem tcpcom-Modul findet man unter www.tcpcom.ch.



Aufgaben: Serie 17

1)

Ein Auto (Turle-Bild) befindet sich im Client-Turtlefenster. Klickt man in das Server-Turtlefenster, so fährt das Auto im Client-Turtlefenster geradeaus an die entsprechende Position.

Anleitung:
Wähle als Vorlage TcpEx1Server.py und TcpEx1Client.py. Im Client-Fenster muss die Turtle sichtbar sein. Im Serverfenster ist die Turtle versteckt und es wird an der Stelle des Mausklicks ein Punkt gezeichnet. Das Bild des Autos gibst du in makeTurtle() an:

  Server
  Client
 

makeTurtle("sprites/car3.png", closeClicked = onCloseClicked)

Verwende den Befehl penUp(), damit die Turtlespuren nicht gezeichnet werden.


2)

Zeichne ein Gitter mit 7 horizontalen und 7 vertikalen Zellen. Server und Client wählen abwechslungsweise mit einem Mausklick eine Zelle aus. Die durch Client gewählten Zellen werden rot, die durch Server gewählten gelb gefärbt.

Verwende als Vorlage TcpEx3Server.py und TcpEx3Client.py (Beispiel 3).

Du kannst dann mit deinem Partner ein "Four in a row Game" spielen.

Spielregeln:

  Server

Client
- Die neu gewählten Zellen müssen unten an bereits gefärbte Zellen anschliessen
- Wer zuerst 4 Zellen in einer Reihe, Spalte oder Diagonale mit eigener Farbe gefärbt hat, hat gewonnen.
Ein "richtiges" Four in a row Game findest du im Kapitel Spielprogrammierung.

3)
Auf dem Server wird mit gedrückter Maustaste eine Figur gezeichnet. Dabei werden forlaufend die Mauskoordinaten zum Client gesendet, so dass beim Client die gleiche Figur entsteht. Beim Loslassen der Maus ist der Client an der Reihe. Dieser füllt mit einem Mausklick eine beliebige geschlossene Fläche aus und sendet die Mauskoordinaten zum Server, damit die gleiche Fläche auch auf dem Server ausgefüllt wird. Danach kommt wieder der Server zum Zug.
Diese Aufgabe ist also eine Kombination der Beispiele 2 und 3.
  Server
Client

4)

Programmiere ein einfaches Nimmspiel.
Spielregeln:
- In einem Zug kann ein Spieler 1, 2, oder 3 Quadrate mit dem linken Mausklick entfernen
- Der Zug wird mit dem rechten Mausklick beendet
- Danach kommt der zweite Spieler zum Zug
- Wer das letzte Quadrat entfernen muss, hat verloren.

Server

Client