Am Ende dieses Zusatzes
- fliegt auf Ihrem Bildschirm das Raumschiff einer anderen Person, gesteuert von deren Laptop.
- haben Sie selbst eine TCP-Verbindung aufgebaut und gesehen, wie zwei Programme miteinander reden.
Sie brauchen dafür Ihr Spiel, jemanden mit einem eigenen Spiel und ein gemeinsames Netzwerk. Der Laser aus dem letzten Kapitel sollte schon funktionieren.
Was ist ein Socket?
Im Kapitel zu den Netzwerken stand ein Programm «in der Tür» eines Ports. Ein Socket ist genau diese Tür aus Sicht Ihres Programms: Sie öffnen sie, und danach können Sie Text hindurchschicken und Text empfangen. Um Pakete, Routing und Bestätigungen kümmert sich das Betriebssystem.
Eine TCP-Verbindung braucht zwei Rollen:
- Eine Seite wartet an einem Port (
accept). Sie ist für diesen Moment der Server. - Die andere Seite ruft an (
create_connection). Sie ist der Client und muss die IP-Adresse kennen.
Beim Anruf läuft der TCP-Handshake (SYN, SYN-ACK, ACK), ohne dass Sie etwas dazu tun müssen. Danach sind beide Seiten gleichberechtigt: Beide können jederzeit senden und empfangen.
Schritt 1: Die Datei netz.py
Legen Sie neben main.py eine neue Datei netz.py an:
import socket
PORT = 9000
ip = input("IP der anderen Person (leer lassen = ich warte): ")
if ip == "":
print("Warte auf Verbindung ...")
server = socket.create_server(("", PORT))
conn, adresse = server.accept()
print("Verbunden mit", adresse)
else:
conn = socket.create_connection((ip, PORT))
conn.setblocking(False) # recv() soll nicht warten, sonst friert das Spiel ein
puffer = ""
def senden(text):
conn.sendall((text + "\n").encode())
def empfangen():
global puffer
try:
daten = conn.recv(1024)
except BlockingIOError:
return [] # gerade nichts angekommen
if daten == b"":
raise ConnectionError("Die andere Seite hat die Verbindung beendet")
puffer += daten.decode()
*zeilen, puffer = puffer.split("\n")
return zeilen
Zwei Stellen verdienen einen zweiten Blick:
Wiesosetblocking(False)?Normalerweise wartet
recv(), bis etwas ankommt. Ihrewhile-Schleife würde stehen bleiben und das Spiel einfrieren. Ohne Warten kehrtrecv()sofort zurück, und wenn nichts da ist, gibt es einenBlockingIOError, den wir abfangen.
Wieso\nund derpuffer?TCP garantiert, dass alle Bytes in der richtigen Reihenfolge ankommen. Es kennt aber keine Nachrichten, sondern nur einen Datenstrom. Senden Sie
"12"und dann"13", kann"1213"ankommen, oder"121"und später"3". Darum beenden wir jede Nachricht mit einem Zeilenumbruch und heben ein unvollständiges Ende impufferauf, bis der Rest eintrifft.
Schritt 2: Das Spiel anpassen
Die Änderungen in main.py. Die Verbindung kommt ganz nach oben, damit das Fenster nicht einfriert, während Sie warten. Das zweite Raumschiff bekommt eine eigene Form, damit Sie die beiden unterscheiden können.
from netz import *
import turtle
# ...bisherige Imports und Fenster...
# Das Raumschiff der anderen Person
other_ship = turtle.Turtle()
other_ship.shape("triangle")
other_ship.color("lightgreen")
other_ship.setheading(90)
other_ship.penup()
other_ship.setposition(0, GROUND)
# create_laser() bekommt neu ein Raumschiff als Parameter
def create_laser():
def create_laser(shooter):
l = turtle.Turtle()
l.penup()
l.color(1,0,0)
l.hideturtle()
l.setheading(90)
l.setposition(ship.xcor(), ship.ycor() + 20)
l.setposition(shooter.xcor(), shooter.ycor() + 20)
l.pendown()
l.pensize(5)
laser_liste.append(l)
def shoot():
create_laser(ship)
senden("laser")
window.onkeypress(create_laser, "space")
window.onkeypress(shoot, "space")
letzte_x = None
running = True
while running:
# ...bisheriger Code der while-Schleife...
try:
if ship.xcor() != letzte_x:
senden(str(ship.xcor()))
letzte_x = ship.xcor()
for zeile in empfangen():
if zeile == "laser":
create_laser(other_ship)
else:
other_ship.setx(float(zeile))
except ConnectionError:
print("Verbindung verloren")
running = False
window.update()
time.sleep(0.001)
Damit haben Sie ein eigenes kleines Protokoll erfunden. Es hat zwei Sorten Nachrichten: eine Zahl heisst «Ich bin jetzt hier», und laser heisst «Ich habe geschossen». Mehr ist HTTP im Grunde auch nicht: eine Abmachung, wie der Text aussieht.
Schritt 3: Verbinden
- Finden Sie die IP-Adresse der wartenden Person heraus (
ipconfigunter Windows,ipconfig getifaddr en0auf dem Mac). - Die eine Person startet das Spiel und drückt im Terminal nur Enter. Sie wartet nun.
- Die andere Person startet das Spiel und tippt die IP ein.
Alleine testen?Starten Sie das Spiel zweimal auf demselben Laptop und tippen Sie beim zweiten Mal
127.0.0.1ein.
Es klappt nicht?
ConnectionRefusedError: Niemand wartet an diesem Port. Wurde zuerst gewartet? Stimmt die IP?- Es passiert lange nichts, dann
TimeoutError: Die Firewall oder das Netz lässt Sie nicht durch. Unter Windows Zugriff für private Netzwerke erlauben. Manche Schul-WLANs lassen Geräte nicht miteinander reden; dann hilft ein Hotspot vom Handy.Address already in use: Warten Sie kurz oder wählen Sie innetz.pyauf beiden Seiten einen anderen Port.
Herausforderung: Wer entscheidet?
Sie werden etwas Seltsames bemerken: Die Invaders sehen auf beiden Bildschirmen verschieden aus. Jedes Spiel würfelt seine eigenen, und jedes rechnet seine eigenen Treffer aus.
Das ist das Kernproblem jedes Online-Spiels: Wenn zwei Computer sich uneinig sind, wer hat recht?
✏️ Zum Tüfteln
- Duell: Lassen Sie die Invaders weg und drehen Sie das andere Raumschiff an den oberen Rand. Trifft Sie ein fremder Laser, senden Sie
treffer.- Ein Chef: Nur die wartende Seite erzeugt Invaders und schickt
invader 120für die Position. Die andere Seite erzeugt ihre Invaders nur noch, wenn eine solche Nachricht eintrifft. Was passiert mit den Treffern?- Betrug: Was müssten Sie ändern, um bei Ihrer Kollegin mit einem Raumschiff ausserhalb des Bildschirms aufzutauchen? Wie könnte sich ihr Spiel davor schützen?