Interprozesskommunikation / Sockets – Ein Chatprogramm in Python implementieren – Teil 3
In den ersten beiden Teilen dieses Python-Tutorials haben wir ein kleines Chatprogramm geschrieben, bei dem sich ein Client mit einem Server verbinden und Nachrichten versenden kann. Der Server leitet die Nachrichten dann an alle anderen Clients, die aktuell mit ihm verbunden sind, weiter. Das Problem mit unserer bisherigen Implementierung ist allerdings, dass wir lediglich den Standard-Output-Stream von Python verwenden. Dadurch können wir das gleichzeitige Empfangen und Senden von Nachrichten nur schwer darstellen. In diesem letzten Teil des Tutorials wollen wir deshalb eine simple Nutzeroberfläche mit Tkinter schreiben, die ein eigenes Eingabe- und Ausgabefeld für die Nachrichten bereitstellt.
Was ist Tkinter?
Tkinter steht für Tk Interface und ist eine Anbindung an das GUI-Toolkit Tk, mit dem plattformübergreifend Benutzeroberflächen gestaltet werden können. Mit Tkinter können Fenster erstellt und verschiedene Bedienelemente, beispielsweise Buttons oder Labels, hinzugefügt werden. Für die Anordnung der Komponenten auf der Oberfläche gibt es drei verschiedene Manager: Den Grid-Manager, den Pack-Manager und den Place-Manager. Der Grid-Manager ordnet, wie von anderen Programmiersprachen bekannt, die Komponenten in Zeilen und Spalten an. Mit dem Pack-Manager muss der Entwickler keine genauen Anweisungen geben, wo sich ein Element befinden soll. Stattdessen werden die Komponenten von Python selbst angeordnet. Zuletzt gibt es den Place-Manager, bei dem die Position eines Elementes durch explizite x- und y-Koordinaten angegeben wird.
Die Oberfläche des Clients
Unser Server benötigt keine eigene Oberfläche – diese ist nur für die Clients notwendig. Unsere Clients sollen relativ einfach gehalten werden: An der oberen Seite soll sich ein Eingabefeld für den Nutzernamen befinden. Darunter eine readonly Textarea, in der alle Nachrichten, die empfangen und gesendet werden, in der richtigen Reihenfolge angezeigt werden. An der unteren Seite soll es schlussendlich ein Eingabefeld geben, in dem Nachrichten zum Versenden eingegeben werden können. Erst, wenn ein Name eingegeben ist, sollen Nachrichten versendet werden können.
Implementierung der Oberfläche
Beginnen wir damit, diese Oberfläche zu implementieren. Importieren wir dazu Tkinter und erstellen ein Fenster. Den Titel des Fensters können wir über das Attribut className festlegen.
Anschließend erstellen wir nacheinander alle Komponenten und positionieren diese mit dem Grid-Manager. In der ersten Zeile benötigen wir ein Label, um anzuzeigen, dass hier der Name eingegeben werden soll, sowie ein Eingabefeld und einen Button, um die Eingabe zu bestätigen.
Um eine Oberflächenkomponente zu erstellen, wird zuerst der Klassenname mit dem Objekt, in dem die Komponente liegen soll, aufgerufen und anschließend mit weiteren Attributen konfiguriert. Diese Attribute können beispielsweise Hintergrundfarbe, Schriftfarbe, Höhe, Breite oder die Beschriftung sein.
Danach positionieren wir die Komponente mit .grid(row=row, column=column), .pack() oder .place(x=x, y=y). Die Anordnung kann mit Parametern wie side=tkinter.RIGHT oder LEFT weiter angepasst werden.
In der nächsten Zeile soll das Feld für den Chatverlauf liegen. Es soll sich dabei um ein Text-Objekt handeln, da dieses mehrzeilig sein kann. Das Text-Objekt muss den Zustand (state) DISABLED haben, da der Nutzer hier nichts eintragen darf. Außerdem soll sich das Element über alle drei Columns, die wir in der ersten Zeile erstellt haben, erstrecken. Dazu wird dem Grid-Manager das Attribut columnspan übergeben.
In der letzten Zeile benötigen wir für die Eingabe der Nachricht wieder ein Label, ein Eingabefeld und einen Button zum Absenden der Nachricht. Auf diesem Button können Sie über command bereits eine Methode, nämlich die send-Methode, verlinken.
Zum Schluss müssen wir die bisher genutzte while-Schleife zum Senden der Nachrichten entfernen und stattdessen das Fenster mit dem Befehl mainloop starten. Das Fenster befindet sich durch diesen Aufruf in einer Endlosschleife, in der es auf Nutzereingaben wartet und diese verarbeitet, bis es geschlossen wird.
Diese Schleife darf erst nach dem receive-Thread, in dem wir Nachrichten vom Server empfangen, gestartet werden – da der Code von oben nach unten interpretiert wird, würden wir das Thread sonst niemals erstellen.
Insgesamt sollte der neue Code am Ende des Skriptes so aussehen:
Beachten Sie, dass alle Methoden, die beispielsweise als command verlinkt werden sollen, bereits definiert sein müssen, sich also nicht an diesen Teil des Skriptes anschließen dürfen.
Entfernen Sie nun die Schleife im Skript, die auf den Input des Nutzernamens wartet, damit der Start des Fensters nicht durch die Konsoleneingabe blockiert wird.
Öffnen Sie das Fenster, um zu prüfen, ob alle Komponenten korrekt angeordnet sind.
Nutzernamen eingeben
Implementieren wir als Nächstes den Code, mit dem der Nutzer seinen Namen eingeben kann. Wir benötigen dazu eine Funktion lock_username, die die Eingabe des Nutzernamens deaktiviert und die Eingabe einer Nachricht ermöglicht, insofern der Nutzer einen Wert eingegeben hat. Dazu werden die Zustände der jeweiligen Komponenten auf DISABLED bzw. NORMAL gesetzt.
Wir können noch einen else-Block hinzufügen, in dem eine Messagebox erscheint, wenn die Eingabe leer ist. Dazu müssen wir messagebox separat aus dem Tkinter-Modul importieren:
Setzen Sie die Eingabe als command des name_confirm_button:
name_confirm_button = Button(window, width=20, text='Confirm', bg='white', command=lock_username)
Starten Sie das Programm und testen Sie, ob die Eingabe funktioniert.
Testen Sie außerdem, dass Sie keine leere Eingabe bestätigen können.
Nachrichten versenden und anzeigen
Schreiben wir abschließend den Code für das Versenden und Anzeigen der Nachrichten.
In der Methode send müssen wir dazu die Art der Eingabe ändern. Bisher lesen wir den Userinput mit dieser Codezeile aus:
Ersetzen Sie den Aufruf input mit dem Inhalt des name– und message_inputs. Diesen können Sie über die Methode get erhalten.
Um die gesendete Nachricht anzuzeigen, haben wir bisher die print-Methode verwendet, die den String in den Standard-Output schreibt. Stattdessen können wir nun die insert-Methode des Text-Objektes nutzen. Das geht allerdings nicht, wenn sich das Objekt im Zustand DISABLED befindet. Befindet sich das Objekt nicht im Zustand DISABLED, kann der Nutzer aber Eingaben tätigen und auch das wollen wir nicht. Wir schreiben also eine neue Methode print_message, die das Feld erst durch den Aufruf configure in den Zustand NORMAL setzt, die Nachricht an das Ende schreibt, und anschließend das Feld wieder DISABLED.
Ersetzen Sie alle Vorkommen von print in der Funktion send mit print_message. Leeren Sie zudem das Eingabefeld mit der Methode delete. Diese Methode erhält als Parameter den Start- und Endindex des zu löschenden Teils der Eingabe. In diesem Fall übergeben wir 0 und END.
Nutzen Sie die Methode print_message auch in der receive-Methode.
Die Ausgabe im Fehlerfall kann auf der Konsole erfolgen, da es sich hier nicht um eine für den Nutzer relevante Information, sondern eher um eine log-Nachricht handelt.
Starten Sie mehrere Clients und testen Sie, ob die Nachrichten korrekt versendet werden.
Damit ist die Implementierung des Chatprogramms abgeschlossen. Rekapitulieren wir noch einmal, was wir in diesem dreiteiligen Tutorial gelernt haben:
- Wir haben gelernt, was Sockets sind, und erfolgreich eine Verbindung zwischen mehreren Sockets hergestellt. Wir haben mithilfe der Sockets Nachrichten verschickt und empfangen.
- Wir haben gelernt, wie wir ein eigenes Nachrichten- bzw. Header-Format definieren, um Metainformationen über die Nachrichten (beispielsweise deren Länge oder den Nutzernamen des Senders) zu übermitteln.
- Wir haben gelernt, wie wir Sockets in einem bestimmten Zustand über das select-Modul ermitteln können.
- Wir haben einen eigenen Thread geschrieben, um Nachrichten zu empfangen, ohne dass sich Empfangen und Versenden gegenseitig blockieren.
- Wir haben für den Client eine eigene kleine Nutzeroberfläche mit Tkinter geschrieben.
Abschließender Client-Code:
Super, dass es dieses coole Tutorial hier kostenlos gibt.
Ich hätte allerdings eine Frage, denn bei mir funktioniert der Code nicht ganz so, wie er sollte. Wenn ich aus einem Client eine Nachricht senden will und auf Senden drücke (das Problem gab es aber auch schon vor der Implementierung der GUI), dann kommt die Nachricht zwar bei den anderen Clients an, löst aber im Server-Log eine Exception aus, die den Server abstürzen lässt. Das Server-Script stört sich irgendwie am letzten Zeichen der Nachricht, versucht, dieses als size_header aufzufassen und scheitert dann natürlich daran, daraus einen Integer zu konstruieren. Das löst dann einen ValueError aus:
Traceback (most recent call last):
File “./server.py”, line 43, in
message = receive(socket)
File “./server.py”, line 21, in receive
message_size = int(size_header.strip())
ValueError: invalid literal for int() with base 10: ”
Auch wenn ich den Code copy-paste, tritt der Fehler auf. Kann mir da jemand einen Tipp geben? Darüber würde ich mich sehr freuen!
Viele Grüße,
Nina
Hallo Nina,
Danke für Ihre Nachricht!
Wir haben das Programm nochmals komplett anhand des Codes im Tutorial nachgestellt. Dabei hat alles bestens funktioniert. Da dabei kein Fehler auftrat, war es uns nicht möglich, das Problem genau zu reproduzieren. Unsere Vermutung ist jedoch, dass es zu einem Fehler bei der Formatierung der Nachricht kam. Vergleichen Sie nochmals genau den Code der Datei header_utils.py mit dem verlinkten Quellcode (https://gist.github.com/BMU-Verlag/a76b97a4ee462bf47f6a4350333c7cb9#file-chatprogramm_tutorial_18-py). Vielleicht haben Sie hierbei etwas abgeändert.
Mit freundlichen Grüßen,
BMU Verlag Team