11.6. TCP SOCKETS

 

 

EINFÜHRUNG

 

Der Austausch von Daten zwischen Rechnersystemen spielt in unserer vernetzten Welt eine ausserordentlich wichtige Rolle.  Darum wird oft gemeinsam von den Computer- und Kommunikationstechnologien gesprochen, die es zu beherrschen gilt. In diesem Kapitel erarbeitest du Basiswissen in der Computerkommunikation zwischen zwei Rechnern mit dem Protokoll TCP/IP, auf dem auch alle Internetverbindungen aufbauen, also beispielsweise das Web und alle Streaming-Dienste (Datenclouds, Sprach-, Musik-, Videoübertragung).

 

 

TCPCOM: EINE EVENTGESTEUERTE SOCKET-BIBLIOTHEK

 

Die Socket-Programmierung baut auf dem Client-Server-Modell auf, das bereits im Kapitel 6.2 beschrieben wurde. Dabei laufen auf dem Client und dem Server Programme, die nicht vollständig symmetrisch sind. Vielmehr muss zuerst das Server-Programm gestartet sein, bevor das Client-Programm eine Verbindung zum Server aufbauen kann. Um die beiden Computer auf dem Internet zu identifizieren, wird ihre IP-Adresse verwendet. Zudem verfügen beide über 65536 Kommunikationskanäle (IP ports), die man mit einer Zahl im Bereich 0..65535 auswählt.

Beim Start des Server erstellt dieser einen Server-Socket (wie eine Steckdose) auf einem bestimmten Port und begibt sich in einen Wartezustand. Anschaulich spricht man davon, dass der Server jetzt auf die Anmeldung eines Clients "hört",  sich also im LISTENING-Zustand befindet. Der Client erstellt seinerseits beim Start  einen Client-Socket (das Steckdosen-Gegenstück) und versucht unter Verwendung der IP-Adresse und der Portnummer eine Verbindung mit dem Server zu erstellen.

Die Bibliothek tcpcom vereinfacht die Socket-Programmierung wesentlich, da sie den aktuellen Zustand von Client und Server mit einer Zustandsvariablen state beschreibt und die Zustandsänderungen und der Datenaustausch durch Ereignisse (Events) erfasst werden. Dabei wird der Callback stateChanged(state, msg) aufrufen. Das Python-Modul ist in TigerJython integriert, kann aber auch von hier heruntergeladen werden, um es  zu studieren oder ausserhalb von TigerJython zu verwenden.

Der Server wird mit der Erzeugung eines TCPServer-Objekts unter Angabe des Ports und der Callbackfunktion onStateChanged() gestartet und begibt sich in LISTENING-Zustand.

from tcpcom import TCPServer 
server = TCPServer(port, stateChanged = onStateChanged)

Der Callback onStateChanged(state, msg) hat zwei String-Parameter state und msg, die den Zustandswechsel des Servers beschreiben:

state msg Beschreibung
TCPServer.LISTENING
port
Eine bestehende Verbindung wurde abgebrochen (oder der Server gestartet) und der Server hört auf eine neue Verbindung
Server.PORT_IN_USE
port
Server kann nicht in LISTENING gehen, weil der Port belegt ist
TCPServer.CONNECTED
IP-Adresse Client
Ein Client hat sich angemeldet und wurde akzeptiert
TCPServer.MESSAGE
empfangene Nachricht
Der Server hat eine Message empfangen
TCPSever.TERMINATED
(leer)
Der Server ist beendet und hört nicht mehr

Der Client wird mit der Erzeugung eines TCPClient-Objekts unter Angabe der IP-Adresse des Servers, des Ports und der Callbackfunktion onStateChanged() gestartet. Mit dem Aufruf connect() versucht er eine Verbindungsaufnahme.

from tcpcom import TCPClient
client = TCPClient(host, port, stateChanged = onStateChanged)
client.connect()
Auch hier hat der Callback onStateChanged(state, msg) zwei String-Parameter state und msg, die den Zustandswechsel des Clients beschreiben:

state msg Beschreibung
TCPClient.CONNECTING
IP-Adresse des Servers
Verbindungsversuch eingeleitet
TCPClient.CONNECTION_FAILED
IP-Adresse des Servers
Verbindungsversuch misslungen
TCPClient.CONNECTED
IP-Adresse des Servers

Verbindung erfolgreich erstellt

TCPClient.MESSAGE
empfangene Nachricht
Der Client hat eine Message empfangen
TCPClient.DISCONNECTED
(leer)
Verbindung unterbrochen (abgebrochen durch Client oder Server)

Der Aufruf von connect() ist blockierend, das heisst, dass die Funktion mit dem Rückgabewert True zurückkehrt, sobald die Verbindungsaufnahme gelungen ist oder aber erst nach einer bestimmten Timeout-Zeit (von rund 10 s) mit False, falls die Verbindungsaufnahme misslingt. Die Information über den Erfolg bzw. Misserfolg der Verbindungsaufnahme kann aber auch über den Callback erfasst werden.

Client-Server-Programme kannst du durch Starten von zwei TigerJython-Fenstern auf dem gleichen PC austesten.  Als IP-Adresse wählst du in diesem Fall den Alias localhost. Verwendest du etwas wirklichkeitsnaher zwei verschiedene Computer, so müssen diese mit einem Netzwerkkabel oder über Wireless LAN verbunden und der gewählte Port für Socket-Kommunikation offen sein. Klappt es mit einer WLAN-Verbindung über deinen normalen Hotspot (WLAN Access Point) nicht, so kannst du auf deinem Smartphone einen Mobilen Hostspot starten und dich mit beiden Computern darauf  verbinden. Ein Zugang des Handys zum Internet ist nicht notwendig.

Für den Einstieg in die Socketprogrammierung erstellst du einen Server, der einen Zeitservice anbietet. Wenn sich ein Client anmeldet, so sendet er die aktuelle Zeit (mit Datum) an den Client zurück. Es gibt auf dem Internet zahlreiche solcher Zeitserver und du kannst stolz sein, dass du bereits jetzt in der Lage bist, eine professionelle Serveranwendung zu erstellen.

Um den Zeitserver wieder abschalten zu können, greifst du zu einem bekannten Trick: Das Hauptprogramm "hängt" in einer blockierenden Funktion, die nach der Rückkehr den Server mit terminate() beendet. Der in TigerJython vordefinierte Message-Dialog ist dazu bestens geeignet.

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()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Der Client fragt zuerst nach der IP-Adresse und versucht mit connect() eine Verbindung mit dem Server herzustellen. Die vom Server zurück erhaltene Zeitinformation schreibst du in ein Dialog-Fenster.

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()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Sowohl der Server wie der Client verwenden das Event-Modell mit einer Callbackfunktion onStateChanged(state, msg). Diese liefert in den zwei Parameterwerten wichtige Informationen über die Art des Ereignisses. Achte darauf, dass du den Server mit terminate() beendest, damit der IP-Port freigegeben wird.

 

 

ECHO-SERVER

 

Das nächste Übungsbeispiel ist berühmt, da es sich um den Archetyp einer Client-Server-Kommunikation handelt. Der Server macht dabei nichts anderes, als die vom Client erhaltene Message unverändert wieder zurück zu senden. Aus diesem Grund wird er Echo-Server genannt. Das System kann leicht so verändert werden, dass der Server die erhaltene Message analysiert und eine entsprechende Antwort zurück sendet. Wichtigstes Anwendungsbeispiel sind Webserver, die auf einen Request des Clients (Browser) mit einem Reply gemäss dem HTTP-Protokoll antworten.

Du schreibst als erstes den Echo-Server und kannst ihn auch gleich starten. Er unterscheidet sich nur unwesentlich vom Zeitserver. Zur Illustration und zum Debugging schreibst du noch state und msg in das Ausgabefenster.

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()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Beim Client gibst du dir bereits etwas mehr Mühe und verwendest mit der Klasse EntryDialog einen nicht-modalen Dialog, um Statusinformationen auszuschreiben.  EntryDialog ist auch in anderen Zusammenhängen sehr praktisch, da du neben editierbaren und nicht-editierbaren Textfeldern auch noch andere GUI-Elemente wie verschiedene Sorten von Buttons und Schieberegler einbauen kannst.

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.")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

MEMO

 

Im Client-Programm verwendest du die Funktion sendMessage(msg, timeout) mit einem zusätzlichen Parameter timeout. Diese blockiert während maximal der Zeit timeout (in s), bis der Server eine Antwort zurücksendet. sendMessage() liefert als Rückgabewert die Antwort des Servers (oder None, wenn innerhalb von timeout keine Antwort empfangen wird).

Es ist für dich wichtig, den Unterschied zwischen modalen und nicht-modalen Dialogfenstern zu kennen. Während das modale Fenster das Programm blockiert, bis es geschlossen wird, läuft das Programm bei einem nicht-modalen Fenster weiter und es können laufend Informationen hineingeschrieben oder Eingabewerte ausgelesen werden.

 

 

ZWEIPERSONEN-SPIELE MIT TURTLEGRAFIK

 

Das Spiel "Schiffchen versenken" ist ein bekanntes und beliebtes Spiel zwischen zwei Personen, bei dem das Erinnerungsvermögen eine wichtige Rolle spielt. In den Zellen eines ein- oder zweidimensionalen Gitters jedes Spielers werden gleiche oder verschiedenartige und damit verschiedenwertige Schiffe angeordnet, die auch mehrere Zellen belegen können. Ein Zug besteht darin, eine Zelle des Gegners zu bezeichnen, in der sozusagen eine Bombe abgeworfen wird. Befindet sich ein Teil eines Schiff darin, wird dieses bei diesem Abwurf versenkt, also vom Spielbrett entfernt und dem Bombenwerfer die entsprechende Wertigkeit gutgeschrieben. Wenn einer der Spieler keine Schiffe mehr hat, ist das Spiel beendet und es gewinnt der Spieler mit der grösseren Punktezahl.

In der einfachsten Version werden die Schiffe in einer eindimensionalen Anordnung durch gefüllte quadratische Zellen dargestellt und alle Schiffe haben die gleiche Wertigkeit. Es gewinnt derjenige Spieler, der zuerst alle Schiffe des Gegners versenkt hat.

Was die Spiellogik angeht, sind Client und Server weitgehend identisch. Für die Grafik genügt es, elementare Zeichnungsoperationen aus der Turtlegrafik zu verwenden, die du schon lange kennst. Du wählst eine Zellennummerierung von 1 bis 10 und musst für die zufällige Wahl des Standorts der Schiffe 4 verschiedene zufällige Zahlen von 1..10 erzeugen. Es ist elegant, dazu die Funktion random.sample() heranzuziehen. Beim Bombenabwurf sendest du den ganzen Python-Programmbefehl an den Partner, der ihn mit exec() ausführen kann. Dies ist nur in wenigen Programmiersprachen möglich. Um herauszufinden, ob getroffen wurde, prüfst du mit getPixelColorStr()  die Hintergrundfarbe.

Beim Spiel sind die beiden Spieler gleichwertig, ihre Programme unterscheiden sich aber etwas, je nachdem wer Server und wer Client ist. Zudem muss der Server zuerst starten.

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
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Das Programm des Clients ist fast identisch:

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")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)



 

MEMO

 

Bei Zweipersonenspielen können beide Spieler dasselbe oder auch unterschiedliche Spielfelder haben. Im zweiten Fall, zu denen auch die meisten Kartenspiele gehören, muss es notwendigerweise mit zwei Computern gespielt werden, da die Spielsituation vor dem Gegner geheim gehalten wird. Statt das immer der Client mit dem ersten Zug beginnt, könnte der Spielbeginn auch zufällig oder durch Aushandeln erfolgen.

Beide Programme werden durch Klicken des Close-Buttons in der Titelzeile beendet. Dabei müssen aber noch zusätzliche "Aufräumarbeiten" wie das Beenden des Servers oder die Unterbrechung der Verbindung ausgeführt werden. Darum registrierst du den Callback onCloseClicked(), wodurch das normale Beenden unterbunden wird. Es liegt nun an dir, im Callback die richtigen Operationen, einschliesslich dem Schliessen des Turtlefensters mit dispose(), durchzuführen. Unterlässt du dies, so bleibt dir nur noch der Griff zum Taskmanager, um das Programm abzuschiessen.

 

 

ZWEIPERSONEN-ONLINE-SPIELE MIT GAMEGRID

 

Für etwas komplexere Spiele ist es von grossem Vorteil, eine Game-Library heranzuziehen, wodurch der Programmcode wesentlich vereinfacht wird. Du hast in Kapitel 7 bereits GameGrid kennen gelernt und gesehen, wie man Spiele damit programmiert. Kombinierst du GameGrid mit tcpcom, so kannst du anspruchsvolle Mehrpersonen-Online-Spiele erstellen, bei denen sich die Spielpartner irgendwo auf der Welt befinden. Zur Illustration erweiterst du das Spiel Schiffchen versenken auf 2 Dimensionen. Die Spiellogik kannst du fast unverändert übernehmen. Allerdings überträgst du nun beim Bombenabwurf die x- und y-Zellenkoordinaten. Der Partner kann damit herausfinden, ob ein Schiff getroffen wurde und hit oder miss zurückmelden.

Beim Programmieren von Games ist es wichtig, sich Mühe zu geben, dass sich das Game "vernünftig" abläuft, selbst wenn die beiden Spieler sich etwas unvernünftig verhalten. So muss das Abschiessen von Bomben nur gestattet sein, wenn der Spieler tatsächlich am Zug ist. Bei unvorhergesehenem Beenden des Clients oder des Servers  sollte der Partner davon unterrichtet werden. Diese Überprüfungen blähen zwar den Code etwas auf, sind aber das Markenzeichen solider und gewissenhafter Programmierung.

Da ein grosser Teil des Codes für den Server und Client identisch ist und die Codeduplikationen zu den grössten Sünden des Programmierens gehört, wird hier im Gegensatz zum vorhergehenden Beispiel der gemeinsame Code in ein Modul shiplib.py ausgelagert, das von beiden Applikationen durch einen import verwendet wird. Unterschiedliches Verhalten berücksichtigt man durch Parameter wie hier node, der entweder auf ein TCPServer- oder TCPClient-Objekt verweist.

Library:

# 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!")
 
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Der Code des Server und des Clients wird durch die Verwendung von shiplib.py wesentlich einfacher und übersichtlicher.

Server:

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
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

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()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

  Client Server
 

 

 

MEMO

 

Auch das Ende des Spiels muss sorgfältig programmiert werden. Da es sich um einen "Ausnahmezustand" handelt, verwendest du ein globales Flag isOver, das bei Spielende auf True gesetzt wird. Es stellt sich auch die Frage, ob das Spiel ohne Neustart des Programm noch einmal gespielt werden kann. In dieser Implementierung ist es so, dass der Client sein Programm beenden muss, der Server aber dann wieder  in den LISTENING-Zustand geht, und auf einen neuen Client wartet.

Die Programme sind in vielerlei Hinsicht noch verbesserungsfähig. Bei der Spielprogrammierung sind der Phantasie und dem Einfallsreichtum des Entwicklers kaum Grenzen gesetzt. Aber nach erfolgreicher Arbeit ist auch das Spielen ein Vergnügen.

Damit die beiden Spielpartner ihre Programme nicht gemäss der Vorschrift  "Zuerst der Server und dann der Client" starten müssen, könnte auch der folgende Trick angewendet werden: Ein Programm startet immer zuerst als Client und versucht einen Verbindung zum Server zu erstellen. Misslingt dies, so startet es einen Server [mehr... Man nennt ein solches System auch "Peer-to-Peer-Communication"]  Die beiden Programme sind in diesem Fall identisch].

 

 

KOMMUNIKATION MIT HANDSHAKE

 

Bei der Kommunikation zwischen zwei Rechnersystemen ist es wichtig, dass die zeitliche Abfolge der Aktionen zwischen den beiden Knoten synchronisiert wird. Insbesondere dürfen Daten erst dann gesendet werden, wenn der Empfänger auch tatsächlich für die Empfang und die Weiterverarbeitung bereit ist. Die Missachtung dieser Regel kann zum Datenverlust oder gar zum Blockieren der Programme führen. Unterschiedliche Rechenleistung der beiden Knoten und eine variable Übertragungszeit müssen dabei berücksichtigt werden.

Ein bekanntes Verfahren, um diese Timing-Probleme in den Griff zu kriegen, besteht darin, Rückmeldungen des Empfängers an den Sender vorzusehen, die man mit einem einvernehmlichen Händedruck (Handshake) zwischen zwei Partnern vergleichen kann. Das Verfahren ist grundsätzlich einfach:  Daten werden blockweise übertragen und der Empfänger quittiert den richtigen Empfang und und die Bereitschaft für den nächsten Block mit einer Rückmeldung. Erst wenn diese eintrifft, wird der nächste Block gesendet. Die Rückmeldung kann bei einem fehlerhaften Empfang auch dazu führen, dass der Sender den gleichen Block nochmals überträgt [mehr... Unterschiedliches zeitliches Verhalten kann auch durch Implementierung von Puffern (Queues, Fifo-Warteschlangen) ausgeglichen werden].

Um das Handshake zu demonstrieren, zeichnet in deinem Programm die Turtle des Servers relativ langsam Linien, die der Client mit Mausklicks vorgibt. Die Turtle des Clients bewegt sich dabei viel schneller als die des Servers. Der Client muss also von Klick zu Klick warten, bis der Server zurückmeldet, dass er die Linie fertig gezeichnet hat und bereit für ein nächstes Kommando ist.

Server:

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)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

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()    
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

  Server: Client:

 

 

MEMO

 

Um die Aktionen von Sender und Empfänger in eine geordnete zeitliche Abfolge zu bringen, führt man Rückmeldungen des Empfängers an den Sender ein und verhindert, dass der Sender weitere Daten senden kann, bevor die Rückmeldung eingetroffen ist.

 

 

AUFGABEN

 

1a.

Erstelle ein Client-Server-System, bei dem der Server als Dienstleistung gute Mathematik-Kenntnisse anbietet. Der Client kann in einem Input-Dialog eine Funktion aus dem Modul math eingeben, also zum Beispiel sqrt(2) und und erhält das Resultat  als Serverantwort, das im Ausgabefenster ausgeschrieben wird. Verwende beim Server die Python-Funktion exec().

   
1b.
Ergänze das Programm, dass der Server bei einem Auftrag, den er nicht ausführen kann, die Message "illegal" zurückmeldet. Fange dazu die Exception ab, die auftritt, wenn exec() nicht ausgeführt werden kann.

   
2.
Erstelle einen Remote-Commander: Der Server startet ein Turtlefenster und wartet auf einen Commander-Client. Dieser öffnet ein Dialogfenster, in das du Turtlebefehle eintippen kannst, die zum Server gesendet werden. Dieser führt die Befehle aus und meldet "OK" bzw. "Fail" zurück. Erst nach Erhalt dieser Antwort kann der Commander den nächsten Befehle senden. Beachte, dass du auch gleichzeitig mehrere Befehle, die durch Strickpunkte getrennt sind, senden kannst

   
3.
Ergänze das zweidimensionale Schiffchen-Versenken mit einer Tonausgabe. Sowohl beim Abschiessen, wie bei einem Treffer oder Fehlschuss soll ein unterschiedlicher kurzer Ton hörbar sein. Verwende dazu deine Kenntnisse aus dem Kapitel 4: Sound. Du kannst vordefinierte Geräusche verwenden, eigene Töne sind aber lustiger.

   
4*.
Verbessere das eindimensionale Schiffchen-Versenken so, dass statt der Zellenfärbung ein Bild des Schiffchens sichtbar ist, das bei einem Treffer verschwindet.    

 

 

 

ZUSATZSTOFF

 

REMOTE-SENSING MIT DEM RASPBERRY PI

 

Die Client-Server-Kommunikation über TCP/IP spielt auch in der Mess- und Steuerungstechnik eine grosse Rolle. Oft befindet sich ein Messgerät oder ein Roboter weit abgesetzt von einer Kommandozentrale und die Mess- und Steuerungsdaten werden über TCP/IP übertragen.  In Aufgabe 2 hast du bereits das Prinzip einer Remote-Steuerung kennengelernt. Im Folgenden verwendest du einen Raspberry Pi, der die Schnittstelle zwischen Messsensoren und dem Internet bildet und die Messdaten an eine Erfassungsstation sendet. Um den Aufbau einfach zu gestalten, sind hier keine eigentlichen Sensoren eingebaut, sondern es  wird lediglich erfasst, ob eine Taste gedrückt oder losgelassen ist.

Für den Raspberry Pi schreibst du mit Python ein Messerfassungsprogramm unter Verwendung des tcpcom-Moduls, das du ins gleiche Verzeichnis auf den Raspi kopieren musst. Dazu kannst du beispielsweise direkt mit angeschlossener Tastatur und Bildschirm oder remote mit  SSH, WinSCP oder VNC arbeiten. Anleitungen dazu findest du in der Literatur oder auf dem Internet.

Das Serverprogramm ist sehr einfach: In der Messschleife frägst du periodisch  mit GPIO.input() den Zustand der Taste ab, wobei hier der Wert 0 der gedrückten Taste entspricht. Nachfolgend wird die Information pressed oder released an den Client übertragen. Bleibt die Taste während 3 Messzyklen gedrückt, so beendest du das Serverprogramm.

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")
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Der Client schreibt nach der Verbindungsaufnahme lediglich die erhaltenen Daten in das Ausgabefenster.

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()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)